3. UART串口驱动开发:从轮询到中断,再到RT-Thread框架

串口,说白了就是嵌入式世界的“嗓子”和“耳朵”。你调试代码、打印日志、跟传感器聊天,都离不开它。这一章,咱们就把它彻底搞透。

我个人习惯把UART驱动分成三个层次:裸机轮询、裸机中断、RT-Thread框架。一层层往上走,你就能理解为什么需要框架,以及框架到底帮你省了什么事。

3.1 先看裸机:轮询模式

轮询模式最简单,也最“傻”。CPU一直盯着状态寄存器看,问“数据来了吗?发完了吗?”

核心代码就这几行:

// 发送一个字节
void uart_send_byte(USART_TypeDef *USARTx, uint8_t data)
{
    // 等待发送数据寄存器空
    while (!(USARTx->SR & USART_SR_TXE));
    // 写入数据
    USARTx->DR = data;
}

// 接收一个字节
uint8_t uart_recv_byte(USART_TypeDef *USARTx)
{
    // 等待接收数据寄存器非空
    while (!(USARTx->SR & USART_SR_RXNE));
    // 读取数据
    return (uint8_t)(USARTx->DR & 0xFF);
}
我曾经踩过的坑: 轮询发送时,如果对方没开接收或者波特率不匹配,while循环会一直卡死。整个系统就“冻住”了。所以轮询模式只适合调试阶段,或者对实时性没要求的场景。

轮询的缺点很明显:CPU被占死了。你想想看,如果串口收一个字节要等1ms,这1ms里CPU啥也干不了。对于简单的“打印Hello World”没问题,但要是系统里还有传感器要读、电机要控,那就彻底完蛋。

3.2 中断模式:解放CPU

中断模式就好多了。数据来了,硬件自动触发中断,CPU暂停手头的事,去把数据收进来。收完继续干原来的活。

中断接收的典型流程:

  1. 使能USART的RXNE中断
  2. 在中断服务函数里读取DR寄存器
  3. 把数据放到一个环形缓冲区里
  4. 主循环从缓冲区取数据

代码骨架:

// 环形缓冲区
#define UART_BUF_SIZE 256
static uint8_t uart_rx_buf[UART_BUF_SIZE];
static uint16_t uart_rx_head = 0, uart_rx_tail = 0;

// 中断服务函数
void USART1_IRQHandler(void)
{
    if (USART1->SR & USART_SR_RXNE) {
        uint8_t data = (uint8_t)(USART1->DR & 0xFF);
        // 放入环形缓冲区
        uart_rx_buf[uart_rx_head] = data;
        uart_rx_head = (uart_rx_head + 1) % UART_BUF_SIZE;
        // 如果满了,覆盖旧数据(简单处理)
        if (uart_rx_head == uart_rx_tail) {
            uart_rx_tail = (uart_rx_tail + 1) % UART_BUF_SIZE;
        }
    }
}

// 主循环读取
uint8_t uart_read_byte(uint8_t *data)
{
    if (uart_rx_head == uart_rx_tail) {
        return 0; // 无数据
    }
    *data = uart_rx_buf[uart_rx_tail];
    uart_rx_tail = (uart_rx_tail + 1) % UART_BUF_SIZE;
    return 1;
}
我的经验: 环形缓冲区的大小要根据你的波特率和数据量来定。115200波特率下,1ms能收约11.5个字节。如果你的主循环处理周期是10ms,缓冲区至少得能装115个字节。我一般给256字节起步,省心。

中断模式解决了CPU空等的问题,但你自己得管理缓冲区、处理临界区(中断里和主循环同时访问head/tail)、还要考虑溢出。每个串口都得写一套类似的代码,很烦。

3.3 RT-Thread Serial设备驱动框架

RT-Thread把上面这些脏活累活都封装好了。它提供了一个统一的serial设备驱动框架,你只需要实现几个底层接口,上层就能用一套API操作所有串口。

框架的核心结构:

层次 职责 对应文件
应用层 用户调用 rt_device_read/write 用户代码
设备管理层 统一设备接口,调度 rt_device.c
serial框架层 缓冲区管理、中断处理、DMA调度 serial.c
BSP驱动层 操作硬件寄存器,实现底层收发 drv_usart.c

你作为驱动开发者,主要工作在BSP驱动层。需要实现这几个函数:

  • rt_serial_configure():配置波特率、数据位、停止位、校验位
  • rt_serial_send():发送数据(轮询或DMA)
  • rt_serial_recv():接收数据(轮询或DMA)
  • rt_serial_control():控制命令,比如设置流控

框架层帮你做了这些事:

  • 自动管理发送和接收的环形缓冲区
  • 中断处理时自动把数据塞进缓冲区
  • 提供信号量/事件机制,让读线程可以阻塞等待数据
  • 支持DMA模式,进一步降低CPU负载
说白了: 你写30行底层代码,框架给你提供300行的通用功能。这笔买卖,划算。

3.4 实战:串口控制台与FinSH组件移植

好了,理论说完了,咱们动手。把串口控制台和FinSH跑起来,这是RT-Thread最常用的调试手段。

3.4.1 移植步骤

  1. 使能FinSH组件:在menuconfig里勾选FinSH,选择串口作为控制台
  2. 实现底层驱动:在drv_usart.c里实现serial框架需要的接口
  3. 注册设备:调用rt_hw_serial_register()注册串口设备
  4. 配置控制台:设置RT_CONSOLE_DEVICE_NAME为你的串口设备名
  5. 测试:编译下载,用串口工具连接,敲回车,看到FinSH的msh >提示符

3.4.2 关键代码片段

注册设备:

static struct rt_serial_device serial1;
static struct stm32_usart_config uart1_config;

int rt_hw_usart_init(void)
{
    struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
    
    // 配置硬件
    uart1_config.Instance = USART1;
    uart1_config.BaudRate = 115200;
    
    // 注册serial设备
    rt_hw_serial_register(&serial1,
                          "uart1",
                          RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
                          &uart1_config,
                          &stm32_usart_ops);
    return 0;
}

底层发送函数(中断模式):

static rt_size_t stm32_usart_send(struct rt_serial_device *serial,
                                   const rt_uint8_t *buf, rt_size_t size)
{
    rt_size_t i;
    for (i = 0; i < size; i++) {
        // 等待发送寄存器空
        while (!(USART1->SR & USART_SR_TXE));
        USART1->DR = buf[i];
    }
    return i;
}
我曾经犯过的错: 注册设备时忘了设置RT_DEVICE_FLAG_INT_RX标志。结果FinSH能发不能收,敲键盘没反应。查了半天才发现是标志位没配。嗯,这种低级错误,犯过一次就记住了。

3.4.3 验证结果

移植成功后,串口工具里应该能看到:

 \ | /
- RT -     Thread Operating System
 / | \     5.0.0 build Jan 10 2025 14:30:00
 2006 - 2025 Copyright by RT-Thread team
msh >

输入help,能看到所有支持的命令。输入list_device,能看到你注册的uart1设备。

3.5 知识结构图

下面这张图,把UART驱动开发的三个层次和关键节点串起来了:

UART驱动开发三层架构 第1层:裸机轮询模式 CPU死等状态寄存器,while循环阻塞 优点:代码简单,零依赖 缺点:CPU占用100%,无法处理多任务 第2层:裸机中断模式 中断触发接收,环形缓冲区管理数据 优点:CPU利用率高,实时性好 缺点:每个串口都要重复造轮子,临界区处理麻烦 第3层:RT-Thread Serial框架 统一设备接口,自动缓冲区管理,信号量同步 优点:写30行底层代码,获得300行框架功能 支持轮询/中断/DMA三种模式,一键切换 实战目标:从第1层走到第3层,最终移植FinSH控制台

3.6 避坑指南

最后,分享几个我实际项目中遇到的坑:

  • 波特率不准:STM32的USART时钟源可能是APB1或APB2,频率不同。算波特率分频时一定要确认时钟源。我见过有人用72MHz算分频,结果时钟是36MHz,波特率差了一倍。
  • 中断优先级:FinSH的串口中断优先级不能太高,否则会打断其他关键中断。也不能太低,否则丢数据。我一般设成中等优先级,比如5(0最高,15最低)。
  • 缓冲区溢出:如果FinSH命令执行时间较长,串口接收缓冲区可能会满。适当加大缓冲区,或者用DMA模式缓解。
  • 流控问题:如果硬件上有RTS/CTS流控,记得在配置里关掉,否则只接TX/RX三根线会收不到数据。
我的习惯: 调试阶段先用轮询模式把FinSH跑通,确认硬件没问题后,再切到中断模式。这样能快速定位问题是出在硬件还是软件。

嗯,这一章的内容就到这。UART驱动是RT-Thread驱动开发的“敲门砖”,搞懂了它,后面的I2C、SPI、CAN等框架,你会发现套路都差不多。