4. UART驱动封装(上):波特率配置、数据帧格式、环形缓冲区设计与实现

UART,说白了就是串口。做单片机开发的,没人离得开它。调试打印、跟传感器通信、和上位机握手……哪样都少不了它。

但很多新手写UART驱动,就是调个库函数完事。一旦波特率对不上、数据丢帧、缓冲区溢出,立马抓瞎。我个人习惯是,把UART驱动做成一个独立的硬件抽象层,上层应用只管读写,底层细节全部封装好。

今天咱们就先把UART驱动的三个核心模块拆开讲:波特率怎么配才准、数据帧格式怎么设、环形缓冲区怎么设计才不掉数据。

4.1 波特率配置:不是随便填个数就行

波特率就是每秒传输的比特数。比如9600波特率,就是每秒传9600个bit。但问题在于——单片机的主频不是为波特率量身定做的,你得通过分频算出一个接近的值。

我遇到过最坑的一次,是某款国产芯片的主频是16MHz,我想配115200波特率。按公式算出来分频系数是个小数,取整后实际波特率变成了115384,误差0.16%。看起来不大对吧?但连续传1000个字节,最后一位的累积误差直接导致帧同步失败。

避坑指南:波特率误差必须控制在±2%以内。我曾经因为偷懒没算误差,导致产品在高温下批量丢数据,排查了整整两天。

波特率计算公式(以常见的USART为例):

// 波特率计算公式
// 过采样模式:OVER8 = 0 时(16倍过采样)
// BaudRate = Fck / (8 * (2 - OVER8) * USARTDIV)
// 
// 简化版(OVER8=0):
// USARTDIV = Fck / (16 * BaudRate)

// 示例:Fck = 72MHz,目标波特率 115200
uint32_t usartdiv = 72000000 / (16 * 115200);  // = 39.0625
// 取整后 USARTDIV = 39,实际波特率 = 72000000 / (16 * 39) = 115384
// 误差 = (115384 - 115200) / 115200 = 0.16%  ✅ 可用
目标波特率 主频(MHz) 理论分频值 实际分频值 实际波特率 误差(%) 结论
9600 8 52.083 52 9615 0.16 ✅ 可用
115200 72 39.062 39 115384 0.16 ✅ 可用
115200 16 8.680 9 111111 -3.55 ❌ 不可用
250000 72 18.000 18 250000 0.00 ✅ 完美
小技巧:如果主频算不出精确的常用波特率,可以换个主频,或者用芯片内部的PLL倍频出一个"好算"的频率。比如72MHz对115200就很友好,16MHz反而尴尬。

4.2 数据帧格式:别让双方"对不上暗号"

UART的数据帧,就像两个人约定好怎么说话。你得告诉对方:多少位数据、什么校验、几个停止位。

标准UART帧结构长这样:

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

最常见配置:1 + 8 + 0 + 1 = 10bit 一帧

我见过最离谱的bug,是两边的工程师一个配了7位数据+偶校验,另一个配了8位数据+无校验。两边都能发能收,但数据全是乱码。排查了三天才发现是帧格式没对齐。

配置帧格式时,记住三个要点:

  • 数据位:绝大多数场景用8位。7位用于老式ASCII传输,9位用于多机通信。
  • 校验位:无校验最常用。偶校验用于对数据完整性有要求的场景。奇校验用得少。
  • 停止位:1位停止位是标配。2位停止位用于低速或噪声环境,给接收端多点时间处理。
我的经验:调试阶段一律用"8N1"(8数据位、无校验、1停止位)。这是最通用的配置,几乎所有设备都支持。等通信稳定了,再根据需求加校验。

4.3 环形缓冲区:UART驱动的"心脏"

没有环形缓冲区的UART驱动,就像没有蓄水池的自来水厂——来一股水你就得接一股,慢了就洒一地。

环形缓冲区(Ring Buffer)说白了就是一个固定大小的数组,加上两个指针:写指针(生产者)和读指针(消费者)。写指针追着读指针跑,永远不会溢出——除非你写得太快,把读指针给"套圈"了。

我早期做的一个项目,用环形缓冲区处理GPS数据。每秒10条NMEA语句,每条最长82字节。如果不用环形缓冲区,中断里处理不过来,直接丢帧。用了环形缓冲区后,中断只管往缓冲区塞数据,主循环慢慢解析,再也没丢过数据。

4.3.1 环形缓冲区数据结构

/* 环形缓冲区结构体 */
typedef struct {
    uint8_t *buffer;    // 缓冲区起始地址
    uint32_t size;      // 缓冲区大小(必须是2的幂)
    uint32_t head;      // 写指针(生产者)
    uint32_t tail;      // 读指针(消费者)
} ring_buffer_t;

为什么size必须是2的幂?因为可以用位运算代替取模,速度更快。你想想看,中断服务程序里每来一个字节都要做一次取模运算,累积起来开销不小。

/* 用位运算实现指针递增(size必须是2的幂) */
#define RING_BUFFER_MASK(size)  (size - 1)

static inline void ring_buffer_advance_head(ring_buffer_t *rb) {
    rb->head = (rb->head + 1) & RING_BUFFER_MASK(rb->size);
}

static inline void ring_buffer_advance_tail(ring_buffer_t *rb) {
    rb->tail = (rb->tail + 1) & RING_BUFFER_MASK(rb->size);
}

4.3.2 核心操作:写入与读取

/* 向环形缓冲区写入一个字节 */
int ring_buffer_put(ring_buffer_t *rb, uint8_t data) {
    uint32_t next_head = (rb->head + 1) & RING_BUFFER_MASK(rb->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) & RING_BUFFER_MASK(rb->size);
    return 0;
}
注意:上面的实现"浪费"了一个存储单元来区分"满"和"空"。当head == tail时表示空,当(head + 1) % size == tail时表示满。这样就不用额外维护一个count变量,在中断中更安全。

4.3.3 在UART中断中的典型用法

/* 全局环形缓冲区实例 */
static ring_buffer_t uart_rx_buffer;
static uint8_t uart_rx_data[256];  // 256字节缓冲区

/* UART接收中断服务程序 */
void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        uint8_t data = USART_ReceiveData(USART1);
        
        // 中断中只做一件事:把数据塞进环形缓冲区
        if (ring_buffer_put(&uart_rx_buffer, data) != 0) {
            // 缓冲区满了,可以置一个溢出标志
            // 或者丢弃旧数据(取决于你的策略)
        }
    }
}

/* 主循环中处理接收数据 */
void main_loop(void) {
    uint8_t byte;
    while (1) {
        // 从环形缓冲区取数据,非阻塞
        if (ring_buffer_get(&uart_rx_buffer, &byte) == 0) {
            // 处理接收到的字节
            process_received_byte(byte);
        }
        
        // 做其他事情...
    }
}

4.4 本章知识结构

下面这张图把UART驱动封装的三个核心模块串起来了。你可以看到,波特率配置和数据帧格式是"初始化阶段"的事,而环形缓冲区是"运行时"的核心。

UART驱动封装核心模块 UART驱动封装 波特率配置 数据帧格式 环形缓冲区 分频系数计算 误差校验(±2%) 过采样模式选择 数据位(5~9bit) 校验位(无/奇/偶) 停止位(1/1.5/2) 读写指针管理 满/空状态判断 中断安全写入 设计要点总结 • 波特率配置:先算分频值,再验误差,最后写寄存器 • 数据帧格式:收发双方必须完全一致,调试阶段用8N1 • 环形缓冲区:中断只写不读,主循环只读不写,避免竞态

4.5 写在最后

UART驱动封装的第一步,就是把这三个基础模块做扎实。波特率配准了,帧格式对上了,环形缓冲区设计好了,剩下的就是把这些模块组合成一个完整的驱动层。

嗯,这里要注意一点:环形缓冲区的大小不是越大越好。我见过有人给串口缓冲区分配4KB,结果RAM不够用。一般256字节对于115200波特率已经绰绰有余,除非你的主循环处理速度极慢。