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在内部连到同一个引脚上。发送时,这个引脚输出数据;接收时,这个引脚输入数据。同一时间只能做一件事。
配置步骤:
- 使能USART时钟和GPIO时钟。
- 配置GPIO为复用开漏输出(AF_OD),因为半双工模式下引脚既要输出又要输入。
- 设置USART_CR3的HDSEL位为1。
- 配置波特率、数据位等常规参数。
代码示例:
// 使能半双工模式
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看似简单,但每个参数都影响通信质量。帧格式要对齐,波特率误差要算准,中断和DMA要根据场景选,半双工要注意收发切换。把这些搞透了,串口通信基本不会出大问题。