4. RT-Thread设备驱动框架

串口,说白了就是嵌入式世界的"标准输出"。你调试、打印、交互,都离不开它。这一章我们就把STM32的USART外设和RT-Thread的Serial驱动框架彻底打通,最终让控制台跑起来。

4.1 STM32 USART外设深度分析

STM32的USART模块,我习惯把它拆成三块来看:时钟与引脚、数据通路、中断与DMA。这样分层,移植的时候思路会清晰很多。

4.1.1 时钟与引脚配置

USART挂在哪条总线上?APB1还是APB2?不同型号的STM32差异很大。以STM32F407为例,USART1挂在APB2上(最高84MHz),USART2挂在APB1上(最高42MHz)。

关键点:波特率计算公式为:BaudRate = fCK / (16 * USARTDIV)。其中fCK是USART时钟频率,USARTDIV是分频系数。如果时钟算错了,波特率怎么调都对不上。

引脚复用这块,我踩过坑。曾经有个项目,USART3的TX/RX默认映射到PB10/PB11,但板子上实际连的是PD8/PD9。结果忘了配置AFIO重映射,串口死活不通。嗯,这里要注意:一定要核对原理图上的引脚映射,再配置GPIO_AF。

4.1.2 数据寄存器与移位机制

USART的数据寄存器DR是双缓冲的。你写DR时,数据先进发送移位寄存器,然后逐位发出。接收时,移位寄存器收完一个字节,自动存入DR。这个过程硬件自动完成,我们只需要关心DR的读写时机。

寄存器 作用 注意事项
DR 数据收发寄存器 读=接收,写=发送
SR 状态寄存器 TXE、RXNE、TC标志位
CR1 控制寄存器1 使能位、中断使能、字长、奇偶校验
CR2 控制寄存器2 停止位配置
CR3 控制寄存器3 DMA使能、流控配置

4.2 RT-Thread Serial设备驱动框架

RT-Thread的Serial框架,我把它理解成一个"适配层"。它定义了一套标准接口,你只要实现这些接口,上层应用就能统一调用。

4.2.1 框架分层结构

从下往上,一共三层:

  1. 硬件操作层:直接操作USART寄存器,实现发送/接收字节
  2. 驱动适配层:实现rt_serial_ops结构体中的回调函数
  3. 设备管理层:提供rt_device_open/read/write/control等API

说白了,你只需要填好struct rt_uart_ops这个结构体,剩下的框架帮你搞定。

我的习惯:先实现轮询模式,再上中断,最后加DMA。一步到位容易出问题,分层验证更稳妥。

4.2.2 核心数据结构

每个串口设备对应一个struct rt_serial_device实例。里面包含了:

  • ops:操作函数指针
  • config:波特率、数据位、停止位、校验位
  • serial_rx / serial_tx:接收/发送缓冲区
  • int_rx / int_tx:中断相关标志

移植时,你需要实例化这个结构体,并调用rt_hw_serial_register()注册到设备管理器。

4.3 中断模式与DMA模式配置

两种模式各有适用场景。我一般这样选:

  • 中断模式:适合不定长、小数据量的通信,比如调试打印、AT指令交互
  • DMA模式:适合大数据量、固定长度的传输,比如固件升级、日志记录

4.3.1 中断模式实现

中断模式的核心是:每收到一个字节,触发一次中断。在中断服务函数里,把数据放入接收缓冲区,然后通知上层。

// 中断接收回调示例
static rt_err_t uart_rx_cb(struct rt_serial_device *serial, rt_uint8_t data)
{
    // 将数据放入环形缓冲区
    rt_ringbuffer_putchar(&serial->serial_rx->rb, data);
    // 触发接收事件
    rt_sem_release(&serial->serial_rx->rx_sem);
    return RT_EOK;
}

这里有个坑:中断服务函数里不要做耗时操作。我曾经在中断里直接调用rt_kprintf,结果递归调用把自己搞死了。中断里只做数据搬运,处理逻辑交给线程。

4.3.2 DMA模式实现

DMA模式需要配置两个通道:一个用于发送,一个用于接收。配置步骤:

  1. 使能DMA时钟,配置DMA通道
  2. 设置源地址(内存)和目标地址(USART_DR)
  3. 配置传输方向、数据宽度、循环模式
  4. 使能DMA传输完成中断

注意:DMA接收时,必须开启USART的IDLE中断(空闲中断)。否则你不知道数据什么时候结束。我见过有人用DMA接收不定长数据,结果一直等不到完成中断,最后超时处理。

4.4 实现控制台交互

控制台是RT-Thread的"门面"。你敲命令,它执行,然后打印结果。实现控制台交互,本质上就是把串口设备注册为rt_console_set_device。

4.4.1 控制台初始化流程

在rt_hw_board_init()中,完成以下步骤:

  • 初始化USART硬件(时钟、GPIO、NVIC)
  • 调用rt_hw_serial_register()注册设备
  • 调用rt_console_set_device("uart1")设置控制台设备

嗯,这里要注意:控制台设备必须在调度器启动之前注册。否则你连rt_kprintf都用不了。

4.4.2 FinSH组件的配合

FinSH是RT-Thread的命令行外壳。它依赖控制台设备。配置FinSH时,需要:

  • 在rtconfig.h中使能RT_USING_FINSH
  • 设置FINSH_USING_MSH为1(使用MSH模式)
  • 配置FINSH_DEVICE_NAME为你的串口设备名

配置完成后,你就能在串口终端上敲list_device、ps、free等命令了。

4.5 知识结构总览

下面这张图,把整个UART移植的脉络串起来了。从硬件寄存器,到驱动框架,再到控制台应用,一条线走到底。

UART串口驱动移植知识结构 STM32 USART外设 时钟配置 / GPIO复用 中断 / DMA配置 RT-Thread Serial框架 struct rt_uart_ops 环形缓冲区管理 控制台 / FinSH rt_kprintf / MSH命令 设备注册与绑定 移植核心:实现struct rt_uart_ops → 注册设备 → 绑定控制台 调试顺序:轮询模式 → 中断模式 → DMA模式

4.6 常见问题与避坑

移植过程中,我遇到过不少问题。挑几个典型的说说:

  • 波特率对不上:检查时钟树配置,确认USART的时钟源是否正确。特别是使用HSE或HSI时,分频系数要算清楚。
  • 中断不触发:检查NVIC优先级分组,确保中断使能位已置位。我犯过低级错误——忘了在CR1里置位RXNEIE。
  • DMA传输卡死:检查DMA通道的优先级,避免和USB、SDIO等高优先级外设冲突。另外,DMA的循环模式要慎用,容易造成缓冲区覆盖。
  • 控制台无输出:确认rt_console_set_device传入的设备名和注册时的设备名一致。大小写敏感,别写错了。

调试小技巧:在rt_hw_serial_register之前,先写一个简单的轮询发送函数,直接操作USART寄存器发一个"OK"字符串。如果能看到,说明硬件没问题,问题出在框架适配层。

好了,这一章的内容就到这里。UART移植是RT-Thread驱动开发的"第一关",打通了它,后面的SPI、I2C、CAN移植思路都是类似的。记住:先硬件后软件,先轮询后中断,先验证后集成。按这个节奏来,不会出大问题。