4. UART驱动开发:从协议到三种收发模式

UART,说白了就是串口通信。做嵌入式开发的人,没有不跟它打交道的。我刚开始做驱动时,觉得UART太简单了——不就是发数据收数据吗?后来踩的坑多了才明白,轮询、中断、DMA这三种模式,用不好就是给自己挖坑。

这一章,咱们把UART驱动从协议到HAL层实现,掰开揉碎了讲清楚。

4.1 UART通信协议基础

UART是异步串行通信,没有时钟线。收发双方靠约定的波特率来同步。我见过不少新手,上来就调不通,最后发现是波特率算错了。

参数 说明 常见值
波特率 每秒传输的符号数 9600, 115200, 921600
数据位 一帧中的数据位数 7, 8
停止位 帧结束标志位 1, 1.5, 2
校验位 奇偶校验或无校验 None, Even, Odd

一帧UART数据长这样:起始位(0) + 数据位(LSB先发) + 校验位(可选) + 停止位(1)。

我的习惯:调试阶段一律用115200-8-N-1。这个组合最通用,大部分串口工具默认就是这个。等产品定型了再根据需求调整。

4.2 HAL UART接口设计

HAL层要做什么?把底层寄存器操作封装成统一的API。我设计HAL接口时,遵循一个原则:上层调用者不需要知道底层是哪个MCU。

先看结构体定义:

typedef struct {
    uint32_t baudrate;
    uint8_t  data_bits;    // 7 or 8
    uint8_t  stop_bits;    // 1 or 2
    uint8_t  parity;       // 0:None, 1:Odd, 2:Even
    uint8_t  mode;         // 0:Polling, 1:Interrupt, 2:DMA
} UART_Config_t;

typedef struct {
    UART_Config_t config;
    uint8_t*      rx_buffer;
    uint32_t      rx_buffer_size;
    volatile uint32_t rx_index;
    void (*tx_callback)(void* arg);
    void (*rx_callback)(uint8_t data, void* arg);
    void*         callback_arg;
} UART_Handle_t;

接口函数就五个:

  • HAL_UART_Init(UART_Handle_t* huart) — 初始化串口
  • HAL_UART_DeInit(UART_Handle_t* huart) — 反初始化
  • HAL_UART_Transmit(UART_Handle_t* huart, uint8_t* data, uint32_t len) — 发送
  • HAL_UART_Receive(UART_Handle_t* huart, uint8_t* data, uint32_t len) — 接收
  • HAL_UART_IRQHandler(UART_Handle_t* huart) — 中断处理入口

设计要点:回调函数用void*参数传递上下文,这样同一个回调可以服务多个串口实例。我在项目里用这个模式,一个中断处理函数处理了6路UART,代码复用率很高。

4.3 轮询模式收发实现

轮询模式最简单,也最占CPU。适合调试阶段或者低速率通信。

发送实现:

HAL_Status_t HAL_UART_Transmit_Polling(UART_Handle_t* huart, 
                                        uint8_t* data, uint32_t len) {
    for(uint32_t i = 0; i < len; i++) {
        // 等待发送缓冲区空
        while(!(huart->regs->SR & USART_SR_TXE));
        huart->regs->DR = data[i];
    }
    // 等待发送完成
    while(!(huart->regs->SR & USART_SR_TC));
    return HAL_OK;
}

接收实现:

HAL_Status_t HAL_UART_Receive_Polling(UART_Handle_t* huart, 
                                       uint8_t* data, uint32_t len) {
    for(uint32_t i = 0; i < len; i++) {
        // 等待接收数据就绪
        while(!(huart->regs->SR & USART_SR_RXNE));
        data[i] = (uint8_t)(huart->regs->DR & 0xFF);
    }
    return HAL_OK;
}
我曾经踩过的坑:轮询接收时,如果对方发过来的数据长度不确定,while循环会卡死。后来我加了超时机制——用SysTick计时,超过一定时间就返回超时错误。这个改动救了我好几次。

4.4 中断模式收发实现

中断模式是实际项目中最常用的。CPU不用死等,数据来了再处理。

中断发送的思路:先触发TXE中断,在中断服务程序里一个字节一个字节地发,发完关中断。

void HAL_UART_Transmit_IT(UART_Handle_t* huart, 
                           uint8_t* data, uint32_t len) {
    huart->tx_buffer = data;
    huart->tx_size = len;
    huart->tx_index = 0;
    // 使能发送缓冲区空中断
    huart->regs->CR1 |= USART_CR1_TXEIE;
}

// 中断服务程序中调用
void HAL_UART_IRQHandler(UART_Handle_t* huart) {
    // 发送中断
    if(huart->regs->SR & USART_SR_TXE) {
        if(huart->tx_index < huart->tx_size) {
            huart->regs->DR = huart->tx_buffer[huart->tx_index++];
        } else {
            // 发送完成,关中断
            huart->regs->CR1 &= ~USART_CR1_TXEIE;
            if(huart->tx_callback) 
                huart->tx_callback(huart->callback_arg);
        }
    }
    // 接收中断
    if(huart->regs->SR & USART_SR_RXNE) {
        uint8_t data = (uint8_t)(huart->regs->DR & 0xFF);
        if(huart->rx_callback)
            huart->rx_callback(data, huart->callback_arg);
    }
}

接收中断更简单——每收到一个字节,就调用回调函数。上层应用在回调里处理数据。

我建议:中断服务程序里不要做复杂处理。收到数据后,用环形缓冲区暂存,主循环里再解析。否则中断嵌套一多,系统就崩了。

4.5 DMA模式收发实现

DMA模式是性能最好的。CPU只需要配置好DMA,剩下的搬运工作DMA自己完成。适合大数据量、高速率通信。

DMA发送流程:

  1. 配置DMA通道:源地址=内存,目的地址=UART数据寄存器
  2. 设置传输长度
  3. 使能DMA,使能UART的DMA发送请求
  4. DMA传输完成触发中断,通知上层
HAL_Status_t HAL_UART_Transmit_DMA(UART_Handle_t* huart, 
                                    uint8_t* data, uint32_t len) {
    // 配置DMA
    DMA_Config_t dma_cfg = {
        .src_addr  = (uint32_t)data,
        .dst_addr  = (uint32_t)&huart->regs->DR,
        .size      = len,
        .src_inc   = 1,    // 源地址递增
        .dst_inc   = 0,    // 目的地址固定
        .transfer_size = 1 // 字节传输
    };
    HAL_DMA_Config(huart->dma_tx_channel, &dma_cfg);
    // 使能UART DMA发送
    huart->regs->CR3 |= USART_CR3_DMAT;
    // 启动DMA
    HAL_DMA_Start(huart->dma_tx_channel);
    return HAL_OK;
}

DMA接收类似,但要注意环形缓冲区的处理。我常用的做法是DMA双缓冲:

  • DMA在接收数据时,CPU处理另一块缓冲区
  • DMA传输完成中断里,交换两个缓冲区指针
  • 这样数据流不会断
我曾经遇到的坑:DMA接收时,如果UART长时间没有数据,DMA传输完成中断永远不会触发。解决方案是配合空闲中断——检测到总线空闲时,强制结束当前DMA传输,把数据交出去。

4.6 三种模式对比与选型

模式 CPU占用 实时性 数据量 适用场景
轮询 高(100%占用) 差(阻塞) 小(<100字节) 调试、初始化
中断 中(每字节中断) 好 中(<1KB) 通用通信
DMA 低(仅配置+完成中断) 最好 大(>1KB) 高速、大数据量

选型建议:

  • 调试阶段用轮询,简单直接
  • 产品中用中断,平衡性能和资源
  • WiFi模块、蓝牙模块这类高速通信,必须用DMA

4.7 UART驱动知识体系

下面这张图,把UART驱动的核心脉络画出来了:

UART驱动开发知识体系 UART协议基础 HAL接口设计 三种收发模式 波特率/数据位/停止位 帧格式:起始+数据+校验+停止 结构体:Config/Handle API:Init/Transmit/Receive 轮询模式 中断模式 DMA模式 实现细节:超时机制 / 环形缓冲区 / 空闲中断 / 双缓冲DMA 选型建议:调试用轮询 → 产品用中断 → 高速用DMA

嗯,这张图把UART驱动的三个层次说清楚了。协议是基础,HAL接口是桥梁,三种模式是具体实现。实际项目中,你可能会在一个驱动里同时用中断和DMA——比如命令用中断收发,大数据用DMA收发。灵活搭配才是王道。