4. 串行通信(UART):从协议到实战

UART,串行通信里的老大哥。说实话,搞嵌入式这么多年,我还没见过哪个工业控制板子上没它的。它简单、可靠,而且几乎每个MCU都自带。今天咱们就把它彻底聊透。

4.1 UART协议时序:一根线怎么传数据?

UART全称是Universal Asynchronous Receiver/Transmitter。注意这个“异步”——它没有单独的时钟线。那收发双方怎么同步?靠的是约定的波特率,以及起始位和停止位。

咱们看一个典型的UART帧格式:

空闲状态(高电平) → 起始位(低电平) → 数据位(LSB先发) → 校验位(可选) → 停止位(高电平)

举个例子,发送0x55(二进制01010101),LSB先发,所以线上看到的是:

起始位(0) → 1 → 0 → 1 → 0 → 1 → 0 → 1 → 0 → 停止位(1)

嗯,这里要注意:UART的起始位一定是低电平,停止位一定是高电平。我见过有人把起始位和停止位搞反,结果通信死活不通。说白了,这就是协议最基础的约定,错不得。

关键点:UART是异步通信,没有时钟线。收发双方必须提前约定好相同的波特率、数据位长度、校验方式和停止位个数。任何一个参数不匹配,收到的数据就是乱码。

4.2 波特率计算:时钟分频的艺术

波特率,就是每秒传输的比特数。工业上常用9600、115200这些值。但MCU的主频往往不是这些值的整数倍,所以需要分频。

以STM32为例,UART的波特率计算公式:

波特率 = fCK / (16 × USARTDIV)

其中fCK是UART外设时钟,USARTDIV是一个16位寄存器,整数部分12位,小数部分4位。

举个例子,fCK=72MHz,想要115200波特率:

USARTDIV = 72,000,000 / (16 × 115200) = 39.0625
整数部分:39 (0x27)
小数部分:0.0625 × 16 = 1 (0x1)
所以USART_BRR = 0x271

我的经验:实际项目中,我建议用示波器量一下实际波特率。因为分频后的误差如果超过2%,长时间通信就可能丢字节。我曾经在一个项目中,波特率误差1.8%,短距离没问题,但线缆一长就频繁出错。后来换了晶振才解决。

4.3 单工/半双工/全双工:三种通信模式

这三种模式,说白了就是数据流向的区别:

模式 数据流向 典型应用
单工 单向,只能A→B 广播、传感器上报
半双工 双向,但不能同时 RS485总线
全双工 双向,可同时收发 标准UART(TX/RX独立)

标准UART的TX和RX是独立的物理引脚,所以天然支持全双工。但RS485只有一对差分线,只能半双工——发的时候不能收,收的时候不能发。

你想想看,半双工模式下,切换方向需要时间。我见过有人没加方向切换延时,结果发完立刻切接收,最后一个字节还没发完,数据就丢了。这个坑,踩过的人都知道。

4.4 环形缓冲区:解决数据堆积的利器

UART中断接收,如果来一个字节就处理一个,CPU会被频繁打断。尤其在高波特率下,根本忙不过来。怎么办?用环形缓冲区。

环形缓冲区,说白了就是一个固定大小的数组,加上读指针和写指针。写指针追着读指针跑,永远不会溢出——除非缓冲区满了。

#define BUFFER_SIZE 256

typedef struct {
    uint8_t buffer[BUFFER_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_head = (rb->head + 1) % BUFFER_SIZE;
    if (next_head == rb->tail) {
        return -1;  // 缓冲区满
    }
    rb->buffer[rb->head] = data;
    rb->head = next_head;
    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) % BUFFER_SIZE;
    return 0;
}

避坑指南:我曾经在环形缓冲区上栽过跟头——读写指针没有用volatile修饰。中断里改了写指针,主循环读的时候编译器优化了,读到的永远是旧值。嗯,volatile这个关键字,在中断和主循环共享变量时,绝对不能省。

4.5 DMA+UART:实现高速无中断收发

环形缓冲区虽然好,但每个字节还是会产生中断。如果波特率到了1Mbps以上,CPU大部分时间都在处理中断。这时候就该DMA上场了。

DMA,直接存储器访问。它可以在没有CPU干预的情况下,把UART接收到的数据自动搬运到内存里。CPU只需要在DMA传输完成时处理一次中断。

配置思路是这样的:

  1. 使能UART的DMA接收请求
  2. 配置DMA通道,源地址为UART数据寄存器,目的地址为内存缓冲区
  3. 设置DMA传输完成中断
  4. 在DMA中断中,切换缓冲区或处理数据

实际项目中,我常用双缓冲方案:DMA正在往缓冲区A写数据,CPU处理缓冲区B的数据。DMA传输完成时自动切换,CPU也切换处理。这样收发完全无中断,CPU利用率极高。

核心思路:DMA负责搬运,CPU负责处理。两者并行工作,互不干扰。这才是高速通信的正确打开方式。

知识体系总览

下面这张图,把UART的核心知识点串起来了:

UART串行通信知识体系 UART通信 协议时序 起始位/数据位/停止位 LSB先发,异步无时钟 波特率计算 fCK / (16 × USARTDIV) 误差需小于2% 通信模式 单工/半双工/全双工 RS485为半双工 环形缓冲区 读指针/写指针 volatile关键字不可省 DMA+UART 无中断高速收发 双缓冲并行处理 从协议到实现,从基础到进阶,UART是嵌入式通信的基石 掌握这些,工业通信不再难

UART看似简单,但真正用好它,需要理解协议、算准波特率、选对模式、设计好缓冲区、用好DMA。每一步都有坑,但每一步也都有解法。希望今天的内容,能帮你少走一些弯路。


公众号:蓝海资料掘金营,微信deep3321