3. UART串口通信协议与驱动

UART,说白了就是嵌入式世界里最常用的“聊天工具”。我做了这么多年驱动,可以说90%的调试信息、传感器数据、甚至固件升级,都是通过这根串口线完成的。今天咱们就把UART从协议到驱动,彻底聊透。

3.1 UART帧格式与波特率计算

UART通信其实很简单,就是两根线:TX发、RX收。但为什么能传数据?靠的是双方约定好的“说话节奏”——波特率。

3.1.1 帧格式解剖

一个完整的UART数据帧长这样:

起始位(1bit) + 数据位(5~9bit) + 校验位(0/1bit) + 停止位(1/1.5/2bit)

我习惯用8N1配置:8位数据、无校验、1位停止位。这是最通用的组合,兼容性最好。

参数 常见配置 说明
起始位 1 bit 固定为低电平,标志传输开始
数据位 8 bit 最常用,兼容ASCII字符
校验位 None/Even/Odd 无校验最省事,校验位用于检错
停止位 1 bit 高电平,标志传输结束
我的习惯:调试阶段用8N1,量产时如果线路长、干扰大,我会加偶校验。曾经在一个电机控制项目里,不加校验时偶尔收到乱码,加了偶校验后问题就解决了。

3.1.2 波特率计算

波特率就是每秒传输的比特数。STM32的USART模块通过时钟分频得到目标波特率:

波特率 = fCK / (16 × USARTDIV)

其中fCK是外设时钟,USARTDIV是分频系数

举个例子:fCK=72MHz,想要115200bps:

USARTDIV = 72,000,000 / (16 × 115200) ≈ 39.0625

嗯,这里要注意。USARTDIV的整数部分和小数部分要分别写入寄存器。我见过不少新手直接把39.0625取整,结果波特率偏差太大,通信不稳定。

避坑指南:波特率误差必须控制在±2%以内。我曾经在一个项目里用了8MHz晶振配250000波特率,误差达到了3.5%,结果丢包率高达10%。后来换成7.3728MHz晶振,问题才解决。

3.2 中断与DMA接收机制

轮询接收太浪费CPU了。实际项目中,我们几乎只用中断或DMA。

3.2.1 中断接收

中断接收的流程很简单:

  1. 使能USART接收中断
  2. 每收到一个字节,硬件自动触发中断
  3. 在中断服务函数里读取DR寄存器
  4. 把数据存入缓冲区

代码骨架长这样:

void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        uint8_t data = USART_ReceiveData(USART1);
        // 把data存入环形缓冲区
        ringbuf_write(&uart_rx_buf, data);
    }
}

中断接收的好处是实时性好,但缺点也很明显——每个字节都进中断,高频数据时CPU开销大。

3.2.2 DMA接收

DMA接收就优雅多了。硬件自动把数据从USART的DR寄存器搬到内存,完全不用CPU干预。

我一般这样配置:

  • 使能USART的DMA接收请求
  • 配置DMA为循环模式或正常模式
  • 设置传输完成中断(可选)
关键点:DMA接收需要配合空闲中断(IDLE)使用。因为DMA不知道一帧数据什么时候结束,只有总线空闲了,我们才知道数据收完了。

我曾经在一个4G模块项目里,用DMA+IDLE中断接收数据,CPU占用率从30%降到了2%。效果立竿见影。

3.3 环形缓冲区设计

环形缓冲区,说白了就是一个固定大小的数组,配合读写指针循环使用。这是嵌入式里最经典的数据结构之一。

3.3.1 为什么需要环形缓冲区?

中断接收的数据如果不及时处理,就会被新数据覆盖。环形缓冲区能暂存数据,让主循环慢慢处理。

3.3.2 核心实现

typedef struct {
    uint8_t buffer[RINGBUF_SIZE];
    volatile uint16_t head;  // 写指针
    volatile uint16_t tail;  // 读指针
} ringbuf_t;

// 写数据
int ringbuf_write(ringbuf_t *rb, uint8_t data)
{
    uint16_t next = (rb->head + 1) % RINGBUF_SIZE;
    if(next == rb->tail) return -1;  // 缓冲区满
    rb->buffer[rb->head] = data;
    rb->head = next;
    return 0;
}

// 读数据
int ringbuf_read(ringbuf_t *rb, uint8_t *data)
{
    if(rb->head == rb->tail) return -1;  // 缓冲区空
    *data = rb->buffer[rb->tail];
    rb->tail = (rb->tail + 1) % RINGBUF_SIZE;
    return 0;
}
我的经验:head和tail一定要加volatile,因为中断和主循环会同时访问它们。另外,缓冲区大小建议选2的幂次方,这样取模可以用位运算优化。

3.4 实战:串口控制台与AT指令解析

理论说完了,咱们来点实际的。串口控制台和AT指令解析,是嵌入式开发里最常见的两个场景。

3.4.1 串口控制台

串口控制台说白了就是一个命令行界面。用户输入命令,设备执行并返回结果。

我一般这样设计:

  • 接收线程(中断/DMA)把数据存入环形缓冲区
  • 主循环从缓冲区读取数据,拼成完整的一行
  • 遇到换行符(\n)后,解析命令并执行

命令解析可以用简单的字符串比较,也可以用查表法:

typedef struct {
    const char *cmd;
    void (*handler)(int argc, char **argv);
} cmd_entry_t;

cmd_entry_t cmd_table[] = {
    {"help",    cmd_help},
    {"reset",   cmd_reset},
    {"version", cmd_version},
    {"set",     cmd_set},
};

3.4.2 AT指令解析

AT指令是模块通信的标准协议。格式一般是:AT+<命令>=<参数>\r\n

解析流程:

  1. 接收完整的一行数据(以\r\n结尾)
  2. 去掉\r\n,提取命令和参数
  3. 查表执行对应处理函数
  4. 返回响应(OK/ERROR/数据)
避坑指南:AT指令的响应可能分多行返回。比如查询信号强度,可能先返回+CSQ: 20,0,再返回OK。解析时一定要处理这种多行响应。我曾经在4G模块项目里没处理好这个,导致解析状态机卡死,排查了两天才找到原因。

3.5 知识体系总览

下面这张图总结了UART驱动的核心知识结构:

UART驱动知识体系 协议层 帧格式:起始位+数据+校验+停止 波特率:fCK/(16×USARTDIV) 驱动层 中断接收:每字节触发 DMA接收:批量搬运+IDLE中断 数据层 环形缓冲区:读写指针循环 AT指令解析:状态机+查表 核心思路:协议层定规矩 → 驱动层收数据 → 数据层做缓存和解析 实战应用:串口控制台 + AT指令解析 关键指标:波特率误差<2% | 缓冲区大小2的幂 | 中断优先级合理配置

UART驱动看似简单,但要做好其实不容易。协议层要算准波特率,驱动层要选对接收方式,数据层要设计好缓冲区。这三层配合好了,串口通信才能稳定可靠。

总结一下:UART驱动开发的核心就是“收得准、存得住、解得对”。收得准靠波特率,存得住靠环形缓冲区,解得对靠状态机。把这三点吃透了,串口通信这块就稳了。