第4章 UART通信驱动:从协议到实战
UART,说白了就是串口通信。这玩意儿在车规级开发里太常见了——ECU刷写、诊断通信、调试输出,哪哪都离不开它。我刚开始做驱动时,觉得UART简单得很,不就是发发数据嘛。直到有一次在量产项目上,因为波特率偏差导致通信丢包,排查了整整两天...嗯,从那以后我再也不敢小看这个“老朋友”了。
4.1 UART协议基础
UART全称是Universal Asynchronous Receiver/Transmitter。注意这个“异步”——收发双方没有独立的时钟线,全靠约定的波特率来同步。数据格式是这样的:
- 起始位:1位,逻辑低电平,告诉接收方“我要开始发了”
- 数据位:5~8位,LSB在前(这是惯例,别搞反了)
- 校验位:可选,奇校验或偶校验
- 停止位:1位或2位,逻辑高电平
我在项目中遇到过一个问题:某款MCU的UART模块默认数据位是7位,而我们的诊断协议要求8位。结果通信一直失败,最后发现是数据位配置错了。你想想看,这种低级错误排查起来多浪费时间。
关键点:车规级应用中,UART的电气特性必须符合ISO 26262要求。电平转换、隔离、ESD保护,一个都不能少。
4.2 波特率计算
波特率决定了通信速度。计算公式其实很简单:
波特率 = 时钟频率 / (16 × 波特率分频因子)
以常见的115200bps为例,假设系统时钟是16MHz:
分频因子 = 16,000,000 / (16 × 115200) ≈ 8.68
取整后 = 9
实际波特率 = 16,000,000 / (16 × 9) ≈ 111,111 bps
误差 = |111,111 - 115,200| / 115,200 ≈ 3.55%
这里有个坑——误差超过2%就可能出问题。我曾经在一个项目里,因为时钟源精度不够,波特率误差达到了4%,结果高温环境下通信频繁出错。后来换了个精度更高的晶振才解决。
| 目标波特率 | 16MHz时钟 | 20MHz时钟 | 推荐? |
|---|---|---|---|
| 9600 | 0.16% | 0.00% | ✅ |
| 115200 | 3.55% | 0.00% | ⚠️ 注意时钟 |
| 921600 | 3.55% | 1.73% | ❌ 不推荐 |
我的习惯:计算波特率时,我会先查芯片手册里的推荐分频值。很多MCU厂商会给出常用波特率的配置表,直接拿来用最稳妥。
4.3 FIFO与DMA模式
UART驱动有三种工作模式:轮询、中断、DMA。轮询模式在车规级里基本不用——太浪费CPU了。重点说说FIFO和DMA。
FIFO模式:硬件自带的小缓冲区,一般16字节或32字节。当FIFO存到一定阈值(比如8字节)时触发中断。这样做的好处是减少中断频率,提高效率。
我建议这样配置FIFO:
/* 以STM32为例 */
USART_CR1->UE = 1; /* 使能USART */
USART_CR3->DMAT = 1; /* 发送DMA使能 */
USART_CR3->DMAR = 1; /* 接收DMA使能 */
USART_CR1->TCIE = 1; /* 发送完成中断 */
DMA模式:数据直接在内存和UART之间搬运,CPU完全解放。这在高速通信(比如1Mbps以上)时特别有用。
我曾经在ADAS项目中,需要同时处理4路UART数据,每路921600bps。如果用中断模式,CPU几乎被占满。换成DMA后,CPU占用率从85%降到了15%。
注意:DMA模式要小心缓存一致性问题。特别是带Cache的MCU,DMA和CPU访问同一块内存时,必须做好Cache刷新或使用非Cache区域。
4.4 环形缓冲区实现
环形缓冲区是UART驱动的核心数据结构。说白了就是一个固定大小的数组,用两个指针(读指针和写指针)来管理数据。
我常用的实现方式:
typedef struct {
uint8_t *buffer;
uint32_t size;
volatile uint32_t head; /* 写指针 */
volatile uint32_t tail; /* 读指针 */
} ring_buffer_t;
/* 写入数据 */
int32_t ring_buffer_write(ring_buffer_t *rb, uint8_t data) {
uint32_t next = (rb->head + 1) % rb->size;
if (next == rb->tail) {
return -1; /* 缓冲区满 */
}
rb->buffer[rb->head] = data;
rb->head = next;
return 0;
}
/* 读取数据 */
int32_t ring_buffer_read(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 表示空, (head + 1) % size == tail 表示满。如果不浪费这个字节,你就没法区分空和满。
避坑指南:我曾经在环形缓冲区里用了32位变量做指针,结果在8位MCU上性能很差。后来改成16位指针,速度提升了3倍。嗯,选型时要考虑MCU的字长。
实际项目中,我会把环形缓冲区和DMA结合起来用:
/* DMA接收完成回调 */
void uart_dma_rx_callback(void) {
uint32_t received = DMA_GetCurrDataCounter();
/* 计算本次接收了多少数据 */
uint32_t count = last_counter - received;
/* 写入环形缓冲区 */
for (uint32_t i = 0; i < count; i++) {
ring_buffer_write(&rx_ring, dma_buffer[i]);
}
last_counter = received;
}
这种方案的好处是:DMA负责搬运数据,环形缓冲区负责缓存,CPU只在需要时处理数据。效率非常高。
4.5 实战经验总结
做UART驱动这么多年,我总结了几条铁律:
- 波特率必须实测验证——别信计算值,用示波器量一下实际波形
- 中断优先级要合理——UART中断别设太高,否则会饿死其他任务
- 环形缓冲区大小要够——至少能存下最大数据帧的两倍
- 错误处理不能省——溢出、帧错误、校验错误都要有处理逻辑
我记得有一次,客户反馈说ECU在低温下通信异常。排查后发现是环形缓冲区太小,低温下CPU处理速度变慢,缓冲区溢出导致丢包。把缓冲区从64字节改成256字节后,问题就解决了。
最后说一句:UART看似简单,但要做好、做稳定,需要你对硬件、中断、DMA、数据结构都有深入理解。别因为它“老”就轻视它——在车规级领域,稳定可靠比花里胡哨重要得多。