STM32中的UART详解

发布于:2025-07-07 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

在嵌入式开发中,串口通信是最常用的调试与数据传输方式之一。UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)作为一种简单、可靠的异步通信协议,被广泛应用于STM32与传感器、上位机、蓝牙模块等外设的交互场景。本文将从协议基础到STM32实战,全面解析UART协议在STM32中的应用,包含硬件设计、软件配置、实战案例及调试技巧,适合嵌入式开发者入门与进阶参考。

一、UART协议基础

1.1 什么是UART?

UART是一种通用的异步串行通信协议,用于在两个设备之间实现全双工或半双工数据传输。其核心特点是“异步”——通信双方无需共享统一的时钟信号,而是通过约定的波特率(数据传输速率)同步数据帧,因此硬件上只需2根线(TX发送、RX接收)即可实现双向通信(全双工)。

与UART容易混淆的是USART(Universal Synchronous/Asynchronous Receiver/Transmitter),两者的区别在于:

  • UART仅支持异步通信;
  • USART既支持异步通信,也支持同步通信(需额外时钟线SCLK)。

STM32芯片中集成的是USART外设,但在大多数场景下,我们使用其异步模式(即UART功能),因此本文统一以“UART”代指STM32中基于USART外设的异步通信。

1.2 UART通信帧结构

UART的数据以“帧”为单位传输,每帧包含以下部分(从左到右传输):
在这里插入图片描述
(示意图:起始位+数据位+校验位+停止位)

  1. 空闲状态:通信线默认处于高电平(逻辑1),表示无数据传输。
  2. 起始位:1位低电平(逻辑0),标志一帧数据的开始,用于唤醒接收设备同步。
  3. 数据位:5~9位,存储实际传输的数据(通常为8位,即1字节),可选择高位在前或低位在前(STM32默认低位在前)。
  4. 校验位(可选):1位,用于数据校验,确保传输准确性:
    • 无校验(None):不使用校验位;
    • 奇校验(Odd):数据位+校验位中1的总数为奇数;
    • 偶校验(Even):数据位+校验位中1的总数为偶数;
    • 标记校验(Mark):校验位固定为1;
    • 空格校验(Space):校验位固定为0。
  5. 停止位:1~2位高电平(逻辑1),标志一帧数据的结束,可根据需求选择1位、1.5位(仅异步模式)或2位。

1.3 波特率与通信速率

波特率(Baud Rate)是UART通信的核心参数,定义为每秒传输的“码元数”(对于UART,每个码元对应1位数据),单位为bps(比特每秒)。例如:

  • 9600 bps:每秒传输9600位数据;
  • 115200 bps:每秒传输115200位数据(常用调试速率)。

通信双方必须使用相同的波特率,否则会出现数据解析错误。实际应用中,常用波特率为9600、19200、38400、57600、115200、256000等。

1.4 UART通信方式

  • 全双工:TX和RX独立工作,发送和接收可同时进行(如STM32与PC通过USB转串口通信)。
  • 半双工:同一时刻只能发送或接收,需通过一根线实现(如STM32的单线半双工模式)。

二、STM32 USART外设介绍

STM32系列芯片(如F1、F4、H7等)通常集成多个USART外设(如F103有3个USART和2个UART,F407有6个USART),不同型号资源不同,需参考对应的数据手册。

2.1 USART外设主要特性

STM32的USART外设支持以下关键特性(以F103为例):

  • 支持异步通信(UART)和同步通信;
  • 波特率范围:1200 bps ~ 4.5 Mbps(F103,取决于时钟频率);
  • 支持5/6/7/8/9位数据位;
  • 支持奇校验、偶校验或无校验;
  • 支持1/1.5/2位停止位;
  • 支持硬件流控制(RTS/CTS,需额外引脚);
  • 支持中断和DMA传输(减少CPU占用);
  • 支持多机通信(地址检测模式);
  • 支持单线半双工模式;
  • 支持智能卡协议和IrDA红外通信(部分型号)。

2.2 引脚映射

USART外设的TX/RX引脚需通过“复用功能”配置,不同型号的引脚映射不同。以STM32F103C8T6为例,USART1的默认引脚为:

  • TX:PA9(复用推挽输出);
  • RX:PA10(浮空输入或上拉输入)。

若默认引脚被占用,可通过“重映射”功能切换到其他引脚(如USART1可重映射到PB6/PB7),具体需参考数据手册的“复用功能映射表”。

2.3 时钟源

USART的时钟来自APB总线(APB1或APB2):

  • 高速USART(如USART1)通常挂载在APB2总线上,时钟频率较高(F103中APB2最高72MHz);
  • 其他USART(如USART2、3)挂载在APB1总线上(F103中APB1最高36MHz)。

时钟频率直接影响波特率精度,需根据总线时钟计算波特率寄存器的值。

2.4 核心寄存器

USART的配置主要通过以下寄存器实现(以寄存器级编程为例):

  1. USART_CR1(控制寄存器1)

    • UE:USART使能位(1=使能);
    • M:数据位长度(0=8位,1=9位);
    • PCE:校验位使能(1=使能);
    • PS:校验类型(0=偶校验,1=奇校验);
    • TE:发送使能(1=使能);
    • RE:接收使能(1=使能);
    • RXNEIE:接收非空中断使能(1=使能)。
  2. USART_CR2(控制寄存器2)

    • STOP:停止位配置(00=1位,01=0.5位,10=2位,11=1.5位)。
  3. USART_CR3(控制寄存器3)

    • CTSIE:CTS中断使能;
    • DMAT:发送DMA使能;
    • DMAR:接收DMA使能。
  4. USART_BRR(波特率寄存器)

    • 由16位整数部分(DIV_Mantissa)和4位小数部分(DIV_Fraction)组成,用于计算波特率。
  5. USART_SR(状态寄存器)

    • TXE:发送数据寄存器空(1=可发送下一字节);
    • TC:发送完成(1=一帧数据发送完毕);
    • RXNE:接收数据寄存器非空(1=有数据待读取)。
  6. USART_DR(数据寄存器)

    • 发送时写入数据,接收时读取数据(8位或9位)。

2.5 波特率计算

波特率由USART_BRR寄存器的值和APB总线时钟共同决定,公式如下:

波特率 = APB总线时钟频率 / (16 * USARTDIV)

其中,USARTDIV = DIV_Mantissa + DIV_Fraction / 16(DIV_Mantissa为整数部分,DIV_Fraction为小数部分,0~15)。

例如,若APB2时钟为72MHz,需配置波特率为115200:

USARTDIV = 72,000,000 / (16 * 115200) ≈ 39.0625
→ DIV_Mantissa = 39(0x27),DIV_Fraction = 1(0x1)
→ USART_BRR = 0x271

STM32CubeMX会自动计算BRR值,手动配置时需确保误差≤3%(否则可能通信失败)。

三、硬件电路设计

UART通信的硬件电路简单,核心是“电平匹配”和“抗干扰”。

3.1 电平标准

STM32的GPIO为3.3V电平,而PC的串口(RS232)为±15V电平,直接连接会损坏芯片,因此需通过“电平转换芯片”(如CH340、PL2303)实现3.3V与RS232/USB的转换。

典型电路如下:
在这里插入图片描述

  • STM32的TX(3.3V)连接到CH340的RXD;
  • STM32的RX(3.3V)连接到CH340的TXD;
  • CH340通过USB线连接到PC;
  • 共地:STM32与CH340的GND必须连接(确保电平参考一致)。

3.2 抗干扰设计

  • 若通信距离较长(>1米),可在TX/RX线上串联100Ω电阻(限流保护);
  • 可并联100pF电容(滤除高频噪声);
  • 远距离通信建议使用差分信号(如RS485),通过MAX485等芯片转换。

3.3 流控制(可选)

若需硬件流控制(防止数据溢出),需增加RTS(请求发送)和CTS(清除发送)引脚:

  • STM32的RTS(输出)连接到外设的CTS(输入);
  • STM32的CTS(输入)连接到外设的RTS(输出)。

大多数场景下无需流控制,可省略这两根线。

四、软件配置步骤

本节以STM32F103为例,分别介绍寄存器级HAL库的配置方法,实现UART基本通信。

4.1 寄存器级配置(USART1,115200 8N1)

“8N1”是最常用的配置:8位数据位,无校验位,1位停止位。

步骤1:使能时钟
// 使能GPIOA和USART1时钟(APB2总线)
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
步骤2:配置GPIO
// 配置PA9为复用推挽输出(TX)
GPIOA->CRH &= ~(GPIO_CRH_MODE9 | GPIO_CRH_CNF9);
GPIOA->CRH |= GPIO_CRH_MODE9_1 | GPIO_CRH_MODE9_0;  // 输出速率50MHz
GPIOA->CRH |= GPIO_CRH_CNF9_1;                       // 复用推挽

// 配置PA10为浮空输入(RX)
GPIOA->CRH &= ~(GPIO_CRH_MODE10 | GPIO_CRH_CNF10);
GPIOA->CRH |= GPIO_CRH_CNF10_0;                      // 浮空输入
步骤3:配置USART参数
// 复位USART1(可选,确保初始状态)
USART1->CR1 &= ~USART_CR1_UE;

// 配置数据位(8位)、无校验、使能发送和接收
USART1->CR1 &= ~(USART_CR1_M | USART_CR1_PCE);       // 8位数据,无校验
USART1->CR1 |= USART_CR1_TE | USART_CR1_RE;          // 使能发送和接收

// 配置停止位(1位)
USART1->CR2 &= ~USART_CR2_STOP;                      // 1位停止位

// 配置波特率(115200,APB2时钟72MHz)
USART1->BRR = 0x271;                                 // 计算值39.0625

// 使能USART1
USART1->CR1 |= USART_CR1_UE;
步骤4:发送与接收函数
// 发送1字节
void UART1_SendByte(uint8_t data) {
    while (!(USART1->SR & USART_SR_TXE));  // 等待发送寄存器为空
    USART1->DR = data;                     // 写入数据
}

// 接收1字节(查询方式)
uint8_t UART1_RecvByte(void) {
    while (!(USART1->SR & USART_SR_RXNE)); // 等待接收数据
    return USART1->DR;                     // 读取数据
}

// 发送字符串
void UART1_SendString(uint8_t *str) {
    while (*str) {
        UART1_SendByte(*str++);
    }
}

4.2 HAL库配置(基于STM32CubeMX)

步骤1:创建工程
  • 打开STM32CubeMX,选择芯片型号(如STM32F103C8T6);
  • 配置RCC:选择HSE时钟(外部晶振),配置系统时钟为72MHz。
步骤2:配置USART1
  • 在“Pinout & Configuration”中,左侧选择“Connectivity”→“USART1”;
  • 模式选择“Asynchronous”(异步模式);
  • 参数配置:
    • Baud Rate:115200;
    • Word Length:8 Bits;
    • Parity:None;
    • Stop Bits:1;
    • Flow Control:None;
  • 自动生成引脚(PA9/TX,PA10/RX),若需重映射可在“System Core”→“GPIO”中修改。
步骤3:生成代码
  • 配置工程路径和IDE(如Keil MDK);
  • 勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”;
  • 点击“GENERATE CODE”生成工程。
步骤4:发送与接收函数

HAL库提供了以下核心函数:

// 发送1字节(阻塞式)
HAL_UART_Transmit(&huart1, &data, 1, 100);  // 超时100ms

// 接收1字节(阻塞式)
HAL_UART_Receive(&huart1, &data, 1, 100);   // 超时100ms

// 发送字符串
void UART1_SendString(uint8_t *str) {
    HAL_UART_Transmit(&huart1, str, strlen((char*)str), 100);
}

五、实战案例

5.1 案例1:串口回环测试(查询方式)

功能:STM32接收PC发送的数据,并原样返回。

int main(void) {
    HAL_Init();
    SystemClock_Config();  // 系统时钟配置(CubeMX生成)
    MX_USART1_UART_Init(); // USART1初始化(CubeMX生成)

    uint8_t data;
    while (1) {
        // 接收1字节
        HAL_UART_Receive(&huart1, &data, 1, 1000);
        // 发送接收到的字节
        HAL_UART_Transmit(&huart1, &data, 1, 100);
    }
}

测试方法:

  1. 用USB线连接CH340到PC,打开串口助手(如XCOM);
  2. 配置波特率115200,无校验,1停止位;
  3. 发送任意字符,应收到相同字符。

5.2 案例2:中断接收(非阻塞)

查询方式会阻塞CPU,中断方式更高效。配置步骤:

步骤1:使能接收中断(CubeMX)
  • 在USART1配置中,勾选“NVIC Settings”→“Enabled”(使能中断);
  • 生成代码后,HAL库会自动配置中断向量表。
步骤2:重写中断回调函数
uint8_t rx_data;  // 全局变量,存储接收数据

// 中断回调函数(HAL库自动调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart == &huart1) {
        // 发送接收到的数据(回显)
        HAL_UART_Transmit(&huart1, &rx_data, 1, 100);
        // 重新开启中断接收(单次中断需手动重启)
        HAL_UART_Receive_IT(&huart1, &rx_data, 1);
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_USART1_UART_Init();

    // 开启中断接收
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);

    while (1) {
        // 主循环可执行其他任务
    }
}

5.3 案例3:DMA传输(高效批量收发)

DMA(直接存储器访问)可实现数据在USART与内存间的直接传输,无需CPU干预,适合大数据量传输。

步骤1:配置DMA(CubeMX)
  • 在USART1配置中,“DMA Settings”→“Add”:
    • 发送:Stream选择“DMA1 Stream4”(USART1_TX对应DMA1_Stream4),方向“Memory to Peripheral”;
    • 接收:Stream选择“DMA1 Stream5”(USART1_RX对应DMA1_Stream5),方向“Peripheral to Memory”;
  • 模式选择“Normal”(单次传输)或“Circular”(循环传输)。
步骤2:DMA发送示例
uint8_t tx_buf[] = "Hello, DMA!\r\n";

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_USART1_UART_Init();
    MX_DMA_Init();  // CubeMX生成的DMA初始化

    // DMA发送(非阻塞)
    HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf)-1);

    while (1) {
        // 等待发送完成(可选)
        if (HAL_UART_GetState(&huart1) == HAL_UART_STATE_READY) {
            // 发送完成后可执行其他操作
        }
    }
}
步骤3:DMA循环接收(固定长度)
uint8_t rx_buf[10];  // 接收缓冲区

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_USART1_UART_Init();
    MX_DMA_Init();

    // 开启DMA循环接收(每次接收10字节)
    HAL_UART_Receive_DMA(&huart1, rx_buf, 10);

    while (1) {
        // 若需处理数据,可在回调函数中实现
    }
}

// DMA接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart == &huart1) {
        // 处理接收数据(如打印)
        HAL_UART_Transmit_DMA(&huart1, rx_buf, 10);
        // 循环模式下无需手动重启,自动重新填充缓冲区
    }
}

5.4 案例4:printf重定向(调试信息输出)

将标准库的printf函数重定向到UART,方便输出调试信息。

步骤1:重写fputc函数(HAL库)
#include <stdio.h>

// 重定向printf到USART1
int fputc(int ch, FILE *f) {
    HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100);
    return ch;
}
步骤2:配置Keil(可选)
  • 勾选“Use Micro LIB”(工程→Options→Target→Use Micro LIB),否则可能编译报错。
步骤3:使用示例
int main(void) {
    // 初始化代码...

    int temp = 25;
    printf("Temperature: %d℃\r\n", temp);  // 输出到串口
}

六、高级功能应用

6.1 单线半双工模式

在资源紧张时,可通过1根线实现发送和接收(同一时刻只能单向传输)。

配置步骤(寄存器级):

// 使能单线半双工模式(CR3寄存器)
USART1->CR3 |= USART_CR3_HDSEL;  // 单线模式使能

// 引脚配置为复用推挽输出(同一引脚既作TX也作RX)
GPIOA->CRH &= ~(GPIO_CRH_MODE9 | GPIO_CRH_CNF9);
GPIOA->CRH |= GPIO_CRH_MODE9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_CNF9_1;

发送时直接调用发送函数,接收时需先切换为输入模式(或通过中断自动切换)。

6.2 多机通信(地址检测)

多个从机通过UART总线连接到主机,主机通过地址帧选择目标从机。

配置步骤:

  1. 从机使能地址检测:USART1->CR1 |= USART_CR1_MME;(多机模式使能);
  2. 主机发送地址帧(第9位为1),从机仅响应匹配的地址;
  3. 地址匹配后,从机进入数据接收模式。

七、常见问题与调试技巧

7.1 通信失败的常见原因

  1. 波特率错误

    • 检查时钟频率是否正确(如系统时钟配置错误);
    • 重新计算BRR值(误差需≤3%)。
  2. 引脚配置错误

    • 确认TX/RX引脚是否接反;
    • 复用功能是否正确(未配置为复用模式会导致无输出);
    • 输入引脚是否为浮空或上拉(下拉可能导致误触发)。
  3. 校验位/停止位不匹配

    • 双方必须配置相同的校验位和停止位(如主机8N1,从机也需8N1)。
  4. 中断未使能

    • 中断接收需使能RXNEIE(CR1寄存器)和NVIC中断。
  5. 接地问题

    • 未共地会导致电平参考不一致,通信不稳定。

7.2 调试工具与方法

  1. 串口助手

    • 用XCOM、SSCOM等工具发送数据,验证STM32的接收功能;
    • 查看STM32发送的数据是否正确。
  2. 示波器/逻辑分析仪

    • 测量TX引脚波形,检查是否有信号输出;
    • 观察波特率是否正确(1位时间=1/波特率,如115200对应约8.68μs)。
  3. printf调试

    • 在关键步骤输出变量值(如“已进入中断”“接收数据:0xXX”)。
  4. DMA调试

    • 检查DMA通道是否正确;
    • HAL_DMA_GetState()查看DMA状态。

7.3 提升通信可靠性的技巧

  1. 添加帧头帧尾

    • 数据帧格式:0xAA + 长度 + 数据 + 校验和 + 0x55,避免数据粘连。
  2. 软件滤波

    • 对连续接收的相同数据进行校验(如连续3次相同才确认有效)。
  3. 超时处理

    • 接收时设置超时时间,超过时间未收到完整数据则丢弃。
  4. 使用中断+缓冲区

    • 中断接收数据到环形缓冲区,主程序从缓冲区读取,避免数据丢失。

八、总结与扩展

UART作为STM32中最基础的通信方式,是嵌入式开发的必备技能。本文从协议基础到实战案例,覆盖了UART的核心知识点,包括:

  • UART协议的帧结构与波特率;
  • STM32 USART外设的配置与寄存器;
  • 硬件电路设计与电平转换;
  • 寄存器级、HAL库、中断、DMA的实现方法;
  • 常见问题与调试技巧。

实际开发中,需根据场景选择合适的传输方式(查询/中断/DMA),并注重通信的可靠性设计。进阶学习可探索:

  • 低功耗模式下的UART唤醒;
  • 高速波特率(如2Mbps)的稳定性优化;
  • 多UART外设的并发管理。

掌握UART后,可进一步学习I2C、SPI等其他通信协议,构建更复杂的嵌入式系统。

附录:常用代码片段

  1. 环形缓冲区(中断接收用)
#define BUF_SIZE 128
uint8_t uart_buf[BUF_SIZE];
uint8_t buf_head = 0, buf_tail = 0;

// 中断回调函数中写入缓冲区
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    uart_buf[buf_head] = rx_data;
    buf_head = (buf_head + 1) % BUF_SIZE;
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}

// 读取缓冲区数据
uint8_t UART1_ReadBuf(uint8_t *data) {
    if (buf_head == buf_tail) return 0; // 空
    *data = uart_buf[buf_tail];
    buf_tail = (buf_tail + 1) % BUF_SIZE;
    return 1;
}
  1. 校验和计算
// 计算字节数组的校验和(简单累加)
uint8_t CheckSum(uint8_t *data, uint8_t len) {
    uint8_t sum = 0;
    for (uint8_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return sum;
}