4. UART通信层:UART协议基础、波特率配置、环形缓冲区设计、DMA传输优化
UART,说白了就是串口通信。在POS机这种嵌入式设备里,它几乎是标配——跟打印机通信、跟密码键盘交互、甚至调试信息输出,都离不开它。我做了这么多年嵌入式,UART调试过的bug,少说也有几十个了。
这一章,咱们就聊聊UART通信层的设计。我会从协议基础讲起,然后深入到波特率配置、环形缓冲区,最后聊聊DMA优化。嗯,都是实战中踩过的坑。
4.1 UART协议基础
UART全称是Universal Asynchronous Receiver/Transmitter。异步的,意味着没有时钟线。收发双方靠约定的波特率来同步。
一个典型的UART数据帧长这样:
起始位(1) + 数据位(5~8) + 校验位(0/1) + 停止位(1/2)
比如最常见的配置:8N1,就是8位数据、无校验、1位停止位。加上1位起始位,一帧总共10位。
我在项目中遇到过一个问题:某款打印机要求7位数据位、偶校验。当时没仔细看手册,直接用8N1去发,结果打印机死活不认。折腾了半天才发现是数据位配置错了。所以啊,通信前一定要确认对方的协议参数。
关键参数速查表:
| 参数 | 常见值 | 说明 |
|---|---|---|
| 数据位 | 8 | 大多数场景用8位 |
| 校验位 | 无/奇/偶 | 噪声环境建议启用 |
| 停止位 | 1 | 2位用于低速设备 |
| 波特率 | 115200 | POS机常用这个 |
4.2 波特率配置
波特率决定了通信速度。但要注意,波特率不是越高越好。你想想看,线长了、干扰大了,高速就容易出错。
POS机里,跟打印机通信通常用9600或19200。跟主机通信,115200比较常见。我个人习惯是:能跑115200就不跑9600,但前提是硬件环境支持。
波特率配置的核心,其实是分频系数的计算。以STM32为例:
// 假设时钟频率为72MHz,目标波特率115200
// USART_BRR = 时钟频率 / (16 * 目标波特率)
// = 72000000 / (16 * 115200) = 39.0625
// 整数部分 = 39
// 小数部分 = 0.0625 * 16 = 1
USART->BRR = (39 << 4) | 1; // 实际波特率 = 72000000 / (16 * 39.0625) = 115200
这里有个坑:分频系数算出来如果不是整数,就会有误差。误差超过2%,通信就可能不稳定。我曾经在一个项目里,因为用了8MHz的外部晶振,算出来的分频系数误差刚好超过2%,结果波特率怎么调都不对。后来换成内部RC振荡器,反而好了。
避坑指南:
我曾经在批量生产时发现,同一批板子有的通信正常,有的乱码。查了半天,发现是晶振精度问题。便宜的晶振误差能达到±50ppm,两个设备一叠加,波特率就对不上了。所以,批量产品建议用内部RC或者高精度晶振。
4.3 环形缓冲区设计
UART接收数据,最怕的就是丢数据。你想想看,CPU正在处理别的事,串口数据来了,如果不及时读走,就会被覆盖。
环形缓冲区就是解决这个问题的。说白了,就是一个固定大小的数组,加上读指针和写指针。数据来了,写到写指针位置;CPU有空了,从读指针位置读。
我常用的环形缓冲区结构体:
typedef struct {
uint8_t *buffer; // 缓冲区指针
uint16_t size; // 缓冲区大小
volatile uint16_t head; // 写指针
volatile uint16_t tail; // 读指针
} ring_buffer_t;
关键操作就两个:写和读。
// 写一个字节
int ring_buffer_put(ring_buffer_t *rb, uint8_t data) {
uint16_t next = (rb->head + 1) % rb->size;
if (next == rb->tail) {
return -1; // 缓冲区满了
}
rb->buffer[rb->head] = data;
rb->head = next;
return 0;
}
// 读一个字节
int ring_buffer_get(ring_buffer_t *rb, uint8_t *data) {
if (rb->head == rb->tail) {
return -1; // 缓冲区空了
}
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
return 0;
}
这里要注意:head和tail要用volatile修饰。因为中断和主循环都会访问它们,编译器优化可能会出问题。我见过有人没加volatile,结果调试时正常,Release版本就丢数据。
个人经验:
缓冲区大小怎么定?我的经验是:至少能存下最大数据帧的2倍。比如打印机一帧数据256字节,缓冲区就设512。太小了容易丢,太大了浪费内存。POS机内存本来就紧张,别浪费。
4.4 DMA传输优化
用中断方式接收UART数据,每个字节都要进一次中断。波特率115200时,每秒约11520个字节,也就是11520次中断。CPU大部分时间都在处理中断,干不了正事。
DMA(直接存储器访问)就是干这个的。它可以在不经过CPU的情况下,直接把UART数据搬到内存里。CPU只需要在DMA传输完成时处理一次中断。
我建议的DMA配置思路:
// 以STM32为例,配置UART接收的DMA
// 1. 使能DMA时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 2. 配置DMA通道
DMA1_Channel5->CPAR = (uint32_t)&USART1->DR; // 外设地址
DMA1_Channel5->CMAR = (uint32_t)rx_buffer; // 内存地址
DMA1_Channel5->CNDTR = RX_BUFFER_SIZE; // 传输长度
DMA1_Channel5->CCR = DMA_CCR_MINC | // 内存地址递增
DMA_CCR_CIRC | // 循环模式
DMA_CCR_TCIE; // 传输完成中断
// 3. 使能DMA
DMA1_Channel5->CCR |= DMA_CCR_EN;
这里有个关键点:循环模式。开启循环模式后,DMA传输完指定长度会自动从头开始。配合环形缓冲区,简直完美。
DMA传输完成中断里,我一般这样处理:
void DMA1_Channel5_IRQHandler(void) {
if (DMA1->ISR & DMA_ISR_TCIF5) {
// 传输完成,更新环形缓冲区的写指针
// 注意:DMA传输的是半字还是字节,要跟UART数据位匹配
ring_buffer.head = RX_BUFFER_SIZE - DMA1_Channel5->CNDTR;
DMA1->IFCR |= DMA_IFCR_CTCIF5; // 清除中断标志
}
}
DMA vs 中断 性能对比:
| 方式 | CPU占用率 | 适用场景 |
|---|---|---|
| 轮询 | 100% | 调试用,别用在产品里 |
| 中断 | 约30% | 低速通信,数据量小 |
| DMA | 约5% | 高速通信,数据量大 |
我做过一个测试:同样是115200波特率,接收1KB数据。中断方式CPU占用率约28%,DMA方式只有4%。差距很明显。
DMA的坑:
我曾经在一个项目里,DMA配置好了,但数据就是不对。查了半天,发现是DMA和外设的时钟域不同步。有些芯片的DMA和UART挂在不同的时钟总线上,需要做同步处理。另外,DMA传输完成中断里不要做太多事,否则会影响下一次传输。
4.5 实战建议
好了,总结一下我的建议:
- 协议参数:先确认对方的参数,再配置自己的。别想当然。
- 波特率:算好分频系数,误差控制在2%以内。批量产品注意晶振精度。
- 环形缓冲区:大小设为2的幂,方便取模运算。head和tail加volatile。
- DMA:能用DMA就别用中断。但要注意DMA和外设的时钟同步问题。
最后说一句:UART看似简单,但坑不少。我见过太多人因为波特率配置不对、缓冲区溢出、DMA配置错误而浪费大量时间。希望这一章能帮你少走弯路。
下一章,咱们聊聊I2C通信层。那玩意儿比UART复杂多了,但掌握了也很实用。