第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驱动这么多年,我总结了几条铁律:

  1. 波特率必须实测验证——别信计算值,用示波器量一下实际波形
  2. 中断优先级要合理——UART中断别设太高,否则会饿死其他任务
  3. 环形缓冲区大小要够——至少能存下最大数据帧的两倍
  4. 错误处理不能省——溢出、帧错误、校验错误都要有处理逻辑

我记得有一次,客户反馈说ECU在低温下通信异常。排查后发现是环形缓冲区太小,低温下CPU处理速度变慢,缓冲区溢出导致丢包。把缓冲区从64字节改成256字节后,问题就解决了。

最后说一句:UART看似简单,但要做好、做稳定,需要你对硬件、中断、DMA、数据结构都有深入理解。别因为它“老”就轻视它——在车规级领域,稳定可靠比花里胡哨重要得多。