4、UART驱动封装:波特率配置、数据格式设置、中断接收与轮询发送、FIFO管理

UART,说白了就是串口。做嵌入式的人,没人离得开它。

调试要它,通信要它,跟蓝牙模块、GPS模块打交道还得靠它。我做了十几年汽车电子,UART驱动写过不下二十个版本。每次移植平台,最头疼的就是这部分——看似简单,坑却不少。

今天咱们就把UART驱动封装这件事聊透。从波特率配置到FIFO管理,一个环节都不放过。

4.1 波特率配置——别小看这个数字

波特率,就是每秒传多少位。听起来简单,对吧?

但我在项目中遇到过一件事:两个ECU用UART通信,波特率都设成了115200,结果数据全是乱码。查了两天,最后发现一个芯片的主频是16MHz,另一个是16.384MHz。分频算下来,实际波特率差了将近3%。

嗯,这就是问题所在。

核心要点:波特率配置的本质,是用系统时钟分频得到目标波特率。分频器精度不够,就会产生误差。

一般UART模块的波特率计算公式是这样的:

// 假设时钟源频率为 f_clk,目标波特率为 baud
// 分频系数 divisor = f_clk / (16 * baud)

uint32_t UART_CalcDivisor(uint32_t f_clk, uint32_t baud)
{
    uint32_t divisor = f_clk / (16 * baud);
    // 有些芯片需要 -1,看具体手册
    return divisor;
}

你想想看,如果f_clk是16MHz,目标115200,算出来divisor是8.68。取整后实际波特率就变成了16M/(16*8)=125000,误差8.5%。这通信能稳定才怪。

我的建议:配置波特率时,先算误差。误差超过2%就要换一个波特率,或者调整时钟源。我一般会准备一个表格,把常用波特率对应的分频值和误差都列出来。

目标波特率 16MHz分频值 实际波特率 误差
9600 104 9615 0.16%
19200 52 19230 0.16%
115200 8 125000 8.5%
115200 9 111111 3.55%

看到了吧?16MHz下跑115200,怎么调误差都大。这时候要么换一个晶振,要么用更高精度的分频模式。

4.2 数据格式设置——8N1是标配,但不是万能

数据位、校验位、停止位,这三个参数组成了UART的数据格式。最常见的组合是8N1:8个数据位,无校验,1个停止位。

但汽车电子里,有时候会用7E1或者8O1。为什么?

我曾经在一个CAN网关项目里,需要跟一个老旧的仪表盘通信。那家伙只认7位数据加偶校验。我一开始没注意,直接用了8N1,结果仪表盘死活不响应。查了三天手册才发现问题。

注意:数据格式必须通信双方完全一致。少一位、多一位都不行。特别是校验位,很多新手容易忽略。

封装驱动时,我习惯用一个结构体来管理这些参数:

typedef struct {
    uint32_t baudRate;      // 波特率
    uint8_t  dataBits;      // 数据位:5,6,7,8
    uint8_t  stopBits;      // 停止位:1,2
    uint8_t  parity;        // 校验:0=无, 1=奇校验, 2=偶校验
} UART_ConfigType;

初始化函数就接收这个结构体,然后根据参数配置寄存器。这样移植的时候,只需要改结构体里的值,不用动驱动代码。

4.3 中断接收与轮询发送——各司其职

接收用中断,发送用轮询。这是我个人比较推荐的做法。

为什么?

接收是异步的。你不知道数据什么时候来。用中断,来了就处理,不会丢数据。发送是同步的。你知道要发什么,轮询等发送完成就行,简单可靠。

中断接收的设计要点:

  • 中断服务程序要短,只做数据搬运
  • 把数据放到环形缓冲区里,交给主循环处理
  • 注意临界区保护,防止中断和主循环冲突

来看一个典型的中断接收实现:

// 环形缓冲区
static uint8_t rx_buffer[RX_BUFFER_SIZE];
static volatile uint16_t rx_head = 0;
static volatile uint16_t rx_tail = 0;

// UART接收中断服务函数
void UART_RX_IRQHandler(void)
{
    uint8_t data;
    
    // 读取接收数据寄存器
    data = UART->DR;
    
    // 计算下一个写入位置
    uint16_t next = (rx_head + 1) % RX_BUFFER_SIZE;
    
    // 如果缓冲区没满,就写入
    if (next != rx_tail) {
        rx_buffer[rx_head] = data;
        rx_head = next;
    }
    // 缓冲区满了?那就丢数据。实际项目中要加错误计数
}

轮询发送就简单多了:

void UART_SendByte(uint8_t data)
{
    // 等待发送缓冲区空
    while (!(UART->SR & UART_SR_TXE));
    
    // 写入数据
    UART->DR = data;
    
    // 等待发送完成(有些芯片需要这步)
    while (!(UART->SR & UART_SR_TC));
}

你可能会问:为什么发送不用中断?

其实也可以用。但轮询发送的代码更简单,而且发送数据时CPU本来就要等,不如直接轮询。中断发送适合大数据量、高吞吐的场景。普通应用,轮询就够了。

4.4 FIFO管理——别让数据丢了

FIFO,先进先出。说白了就是个环形缓冲区。

UART驱动里,FIFO管理是最容易出问题的地方。我见过太多因为缓冲区溢出导致的数据丢失案例。

避坑指南:我曾经在一个项目里,接收缓冲区只设了64字节。结果对方一次发了200字节的数据包,直接溢出。从那以后,我定了个规矩:缓冲区大小至少是最大数据包的2倍。

FIFO的核心操作就三个:写入、读取、判空判满。

typedef struct {
    uint8_t* buffer;    // 缓冲区指针
    uint16_t size;      // 缓冲区大小
    volatile uint16_t head;  // 写入位置
    volatile uint16_t tail;  // 读取位置
} FIFO_Buffer_t;

// 初始化FIFO
void FIFO_Init(FIFO_Buffer_t* fifo, uint8_t* buf, uint16_t size)
{
    fifo->buffer = buf;
    fifo->size = size;
    fifo->head = 0;
    fifo->tail = 0;
}

// 写入一个字节
bool FIFO_Write(FIFO_Buffer_t* fifo, uint8_t data)
{
    uint16_t next = (fifo->head + 1) % fifo->size;
    
    // 缓冲区满了?
    if (next == fifo->tail) {
        return false;  // 溢出
    }
    
    fifo->buffer[fifo->head] = data;
    fifo->head = next;
    return true;
}

// 读取一个字节
bool FIFO_Read(FIFO_Buffer_t* fifo, uint8_t* data)
{
    // 缓冲区空了?
    if (fifo->head == fifo->tail) {
        return false;
    }
    
    *data = fifo->buffer[fifo->tail];
    fifo->tail = (fifo->tail + 1) % fifo->size;
    return true;
}

特别注意:中断和主循环都会访问head和tail。中断里只修改head,主循环里只修改tail。这样能避免大部分竞态问题。如果芯片支持,最好用原子操作或者关中断保护。

FIFO的大小怎么定?我一般遵循这几个原则:

  • 接收FIFO:至少能存下最大数据包的2倍
  • 发送FIFO:如果用了中断发送,大小跟接收类似
  • 轮询发送不需要FIFO,直接写寄存器就行

嗯,说到这,UART驱动的核心内容基本都覆盖了。波特率别算错,数据格式要对齐,中断接收加FIFO,轮询发送保底。这套组合拳打下来,大部分场景都能应付。

下一章咱们聊聊SPI驱动的封装。那个比UART稍微复杂一点,但套路是一样的。