4、串口通信协议基础:UART帧格式解析、波特率配置与误差分析、中断与DMA接收机制
串口通信,说白了就是嵌入式世界里最基础、最常用的通信方式。我做了这么多年MCU开发,几乎每个项目都离不开UART。调试打印、与传感器通信、甚至做简单的协议交互,UART都是首选。今天咱们就把它彻底聊透。
4.1 UART帧格式解析
UART的帧格式其实很简单,但很多人容易忽略细节。一个标准的UART帧包含以下几个部分:
- 起始位:1位,逻辑0。告诉接收端“我要开始发数据了”。
- 数据位:通常5~8位,最常见的是8位。注意,LSB(最低有效位)先发。
- 校验位:可选,奇校验或偶校验。我一般不用,除非通信环境比较恶劣。
- 停止位:1位、1.5位或2位,逻辑1。表示一帧结束。
关键点:UART是异步通信,没有时钟线。收发双方靠约定的波特率来同步。所以帧格式必须完全一致,否则数据就乱了。
我在项目中遇到过一个问题:两个设备明明波特率、数据位、停止位都设置一样,但通信就是不稳定。后来发现,一个设备用的是8位数据+奇校验,另一个用的是8位数据+无校验。嗯,这种低级错误,排查起来真的很头疼。
4.2 波特率配置与误差分析
波特率,就是每秒传输的比特数。常见的波特率有9600、115200、921600等。但问题来了:MCU的系统时钟不一定能精确分频出你想要的波特率。
举个例子,假设系统时钟是72MHz,你想配置115200波特率。UART外设通常有一个波特率发生器,公式大致是:
波特率 = 系统时钟 / (16 * (USARTDIV))
其中USARTDIV是一个分频系数。算一下:
USARTDIV = 72000000 / (16 * 115200) ≈ 39.0625
问题来了,分频器只能取整数值或者半整数(比如39.0或39.5)。如果你取39,实际波特率是:
实际波特率 = 72000000 / (16 * 39) ≈ 115384.6
误差 = (115384.6 - 115200) / 115200 ≈ 0.16%。这个误差在可接受范围内(通常要求<2%)。
避坑指南:我曾经在一个项目中用了921600的高波特率,系统时钟是50MHz。算出来的误差接近3.5%,结果通信时断时续。后来我把系统时钟改成48MHz,误差降到0.2%,问题就解决了。所以,高波特率下一定要算误差,别想当然。
为什么误差不能太大?因为UART的采样点通常在数据位的中间。如果误差累积,采样点会偏移,最终导致误码。你想想看,如果误差超过5%,连续传几个字节后,采样点可能就跑到数据位边缘了,那数据还能对吗?
4.3 中断接收机制
中断接收,是UART最常用的方式。原理很简单:每收到一个字节,硬件就触发一次中断,你在中断服务函数里把数据读走。
代码示例(以STM32为例):
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
// 把data放到环形缓冲区里
ring_buffer_push(&rx_buffer, data);
}
}
这里有个坑:中断服务函数里不要做耗时操作,比如打印、复杂的协议解析。你想想看,如果波特率是115200,每字节大约87微秒就来了。你在中断里耽误太久,下一个字节就丢了。
我的习惯:中断里只做数据搬运,把收到的字节放到环形缓冲区。主循环里再从缓冲区取数据做协议解析。这样既不会丢数据,也不会阻塞中断。
4.4 DMA接收机制
中断接收虽然好用,但高波特率下(比如1Mbps以上),频繁进中断会占用大量CPU时间。这时候就该DMA出场了。
DMA接收的原理:配置好DMA通道,让UART的接收数据寄存器直接连接到内存缓冲区。每收到一个字节,DMA自动把数据搬到缓冲区,完全不需要CPU干预。只有当缓冲区满了或者收到指定数量的字节时,才触发一次完成中断。
配置示例:
// 配置DMA,接收10个字节后触发中断
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buffer;
DMA_InitStructure.DMA_BufferSize = 10;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
// ... 其他配置省略
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
这样做的好处很明显:CPU可以专心做其他事情,比如跑算法、控制外设。只有收到一帧完整数据后,CPU才来处理。
实际经验:我在一个需要同时处理4路UART的项目中,如果都用中断接收,CPU负载接近40%。换成DMA后,负载降到5%以下。说白了,DMA就是让硬件帮你干活,CPU只管收结果。
4.5 中断 vs DMA:怎么选?
这个问题没有标准答案,我一般按以下原则选:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 波特率≤115200,数据量小 | 中断 | 实现简单,CPU占用可接受 |
| 波特率≥460800,数据量大 | DMA | 减少中断频率,降低CPU负载 |
| 多路UART同时工作 | DMA | 避免中断嵌套和优先级问题 |
| 需要接收不定长数据 | 中断+空闲中断 | DMA适合定长接收,不定长需要配合空闲中断 |
嗯,这里要注意:DMA接收不定长数据时,通常要配合UART的空闲中断。当总线空闲超过一个字节时间时,触发空闲中断,表示一帧数据结束。这时候CPU再去DMA缓冲区里取数据。
我曾经在一个项目中,用DMA+空闲中断的方式接收GPS模块的数据。GPS数据是变长的,每帧以$开头,以\r\n结尾。配置好DMA循环接收,空闲中断里判断帧头帧尾,完美解决了高波特率下的数据接收问题。
4.6 总结一下
UART看似简单,但用好它需要理解帧格式、算准波特率、选对接收方式。我个人建议:
- 调试阶段用中断接收,简单直观
- 产品阶段根据波特率和数据量,考虑是否切换到DMA
- 永远不要忽略波特率误差,尤其是高波特率下
- 环形缓冲区是UART接收的好搭档,建议每个项目都用
下一章咱们聊聊更高级的串口协议——Modbus,到时候会用到今天讲的这些基础。先把UART吃透,后面就轻松了。