4. UART异步串口协议:帧格式解析、波特率误差计算、中断与DMA接收对比、单线半双工模式

UART,说白了就是串口。做嵌入式的人,没人能绕开它。我最早接触STM32时,第一个调通的外设就是UART。那时候觉得它简单,不就是发数据收数据嘛。后来项目做多了才发现,UART里藏着不少坑——波特率算不对,数据就乱码;中断和DMA选不对,CPU就被拖死;单线半双工用不好,总线就冲突。今天咱们把这几个点掰开揉碎了讲清楚。

4.1 UART帧格式解析

UART的帧格式,其实就几个参数:起始位、数据位、校验位、停止位。我见过不少新手上来就抄例程,结果通信双方参数没对齐,数据全错。

标准UART帧长这样:

空闲状态(高电平) → 起始位(低电平,1位) → 数据位(LSB先发,5~9位) → 校验位(可选,奇/偶/无) → 停止位(高电平,0.5/1/1.5/2位) → 空闲状态

我习惯用8N1——8位数据、无校验、1位停止位。这是最常用的配置,兼容性最好。但有些场景需要9位数据(比如多机通信),或者需要奇偶校验(比如工业现场抗干扰),这时候就得改。

关键点:发送方和接收方的帧格式必须完全一致。数据位、校验位、停止位,任何一个不匹配,数据就收不对。我曾经在调试一个GPS模块时,发现数据全是乱码,折腾了半天才发现模块默认是8E1(偶校验),而我用的是8N1。

STM32的USART寄存器里,通过USART_CR1的M位设置数据位长度,PCE和PS位设置校验模式,STOP位在USART_CR2里设置。配置时记得先关掉USART,改完再开。

4.2 波特率误差计算

波特率,就是每秒传输的位数。STM32的波特率由系统时钟分频得到。公式很简单:

波特率 = fCK / (16 × USARTDIV)

其中fCK是USART的时钟源,USARTDIV是一个16位寄存器,整数部分12位,小数部分4位。说白了,你只能设置有限个离散的波特率值。

那误差怎么算?举个例子:

假设fCK = 72 MHz(STM32F103常用配置),想要115200 bps:

USARTDIV = 72,000,000 / (16 × 115200) = 39.0625
整数部分 = 39 (0x27)
小数部分 = 0.0625 × 16 = 1 (0x1)
实际波特率 = 72,000,000 / (16 × 39.0625) = 115,200 → 误差0%

完美!但换个频率试试:fCK = 48 MHz(STM32L0系列常用),同样要115200:

USARTDIV = 48,000,000 / (16 × 115200) = 26.041666...
整数部分 = 26
小数部分 = 0.041666 × 16 = 0.666 → 只能取0或1
取0:实际波特率 = 48,000,000 / (16 × 26) = 115,384.6 → 误差 +0.16%
取1:实际波特率 = 48,000,000 / (16 × 26.0625) = 115,107.8 → 误差 -0.08%

我的经验:误差在±2%以内通常没问题。但如果你用长线通信(超过1米),或者环境干扰大,建议把误差控制在±1%以内。我有个项目用RS485在100米线上跑115200,误差0.5%就偶尔丢包,降到0.1%才稳定。

STM32的CubeMX会自动计算并显示误差,但手动算一下心里更有底。尤其是用内部RC振荡器时,时钟本身就有1~2%的误差,再加上分频误差,很容易超限。

4.3 中断与DMA接收对比

UART接收数据,主要有三种方式:轮询、中断、DMA。轮询太浪费CPU,基本不用。咱们重点对比中断和DMA。

对比项 中断接收 DMA接收
CPU占用率 每个字节都进中断,CPU被频繁打断 只在DMA传输完成时进一次中断,CPU几乎不参与
实时性 每个字节都能及时处理 需要等一帧数据收完才能处理
数据量 适合小数据量(几十字节以内) 适合大数据量(几百字节以上)
实现复杂度 简单,直接写中断服务函数 稍复杂,需要配置DMA通道和循环模式
典型场景 按键响应、短命令交互 文件传输、传感器数据流、日志输出

我个人的习惯是:

  • 数据量小、需要逐字节响应 → 用中断。比如和蓝牙模块通信,每次只发几条AT指令。
  • 数据量大、不要求逐字节处理 → 用DMA。比如从SD卡读文件通过UART发送,或者接收GPS的NMEA数据流。

中断接收的代码框架:

void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        uint8_t data = USART_ReceiveData(USART1);
        // 处理接收到的字节
        // 注意:中断服务函数要尽量短
    }
}

DMA接收的配置要点:

// 1. 使能USART的DMA接收请求
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);

// 2. 配置DMA通道
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)rx_buffer;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_BufferSize = RX_BUFFER_SIZE;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; // 循环模式,自动重置
DMA_Init(DMA1_Channel5, &DMA_InitStruct);

// 3. 使能DMA传输完成中断
DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE);

注意:DMA接收时,如果数据不是固定长度,需要配合空闲中断(IDLE interrupt)来判断一帧结束。我踩过这个坑——用DMA收不定长数据,结果一直等不到传输完成中断,后来才发现要开IDLE中断。

4.4 单线半双工模式

标准UART需要TX和RX两根线。但有些场景下,比如只有一根数据线可用,或者想节省IO口,就可以用单线半双工模式。

STM32的USART支持半双工模式,通过USART_CR3的HDSEL位使能。在这种模式下,TX和RX在内部连到同一个引脚上。发送时,这个引脚输出数据;接收时,这个引脚输入数据。同一时间只能做一件事。

配置步骤:

  1. 使能USART时钟和GPIO时钟。
  2. 配置GPIO为复用开漏输出(AF_OD),因为半双工模式下引脚既要输出又要输入。
  3. 设置USART_CR3的HDSEL位为1。
  4. 配置波特率、数据位等常规参数。

代码示例:

// 使能半双工模式
USART_SetHalfDuplex(USART1, ENABLE);

// GPIO配置为复用开漏
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; // 只用TX引脚
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

避坑指南:我曾经在一个智能家居项目里用单线半双工模式连接多个传感器。一开始没注意,发送完数据立刻切到接收模式,结果收到了自己发送的回声。后来加了延时——发送完等一个字节的时间再切接收,问题就解决了。因为UART没有收发切换的硬件自动控制,全靠软件管理。

半双工模式的应用场景:

  • RS485通信(本质就是半双工,通过DE/RE引脚控制收发方向)
  • 单总线协议(比如DHT11温湿度传感器)
  • IO口不够用的低成本设计

但要注意,半双工模式下,通信双方需要约定好谁先发、谁后收,避免冲突。我一般用主从模式——主机发完,从机才能发。

UART异步串口协议知识体系 UART协议 帧格式解析 波特率误差计算 中断 vs DMA 单线半双工模式 起始位/停止位 数据位/校验位 分频公式 误差计算 中断接收 DMA接收 HDSEL位 开漏输出 核心:帧格式对齐 → 波特率匹配 → 接收方式选择 → 半双工收发切换 每个环节出错都会导致通信失败,调试时按这个顺序排查

总结一下:UART看似简单,但每个参数都影响通信质量。帧格式要对齐,波特率误差要算准,中断和DMA要根据场景选,半双工要注意收发切换。把这些搞透了,串口通信基本不会出大问题。