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传输完成时处理一次中断。
配置思路是这样的:
- 使能UART的DMA接收请求
- 配置DMA通道,源地址为UART数据寄存器,目的地址为内存缓冲区
- 设置DMA传输完成中断
- 在DMA中断中,切换缓冲区或处理数据
实际项目中,我常用双缓冲方案:DMA正在往缓冲区A写数据,CPU处理缓冲区B的数据。DMA传输完成时自动切换,CPU也切换处理。这样收发完全无中断,CPU利用率极高。
核心思路:DMA负责搬运,CPU负责处理。两者并行工作,互不干扰。这才是高速通信的正确打开方式。
知识体系总览
下面这张图,把UART的核心知识点串起来了:
UART看似简单,但真正用好它,需要理解协议、算准波特率、选对模式、设计好缓冲区、用好DMA。每一步都有坑,但每一步也都有解法。希望今天的内容,能帮你少走一些弯路。
公众号:蓝海资料掘金营,微信deep3321