4. UART串口驱动移植实战:从硬件到控制台
串口,说白了就是嵌入式世界的"标准输出"。你调试、打印、交互,都离不开它。这一章我们就把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 框架分层结构
从下往上,一共三层:
- 硬件操作层:直接操作USART寄存器,实现发送/接收字节
- 驱动适配层:实现
rt_serial_ops结构体中的回调函数 - 设备管理层:提供
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模式需要配置两个通道:一个用于发送,一个用于接收。配置步骤:
- 使能DMA时钟,配置DMA通道
- 设置源地址(内存)和目标地址(USART_DR)
- 配置传输方向、数据宽度、循环模式
- 使能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移植的脉络串起来了。从硬件寄存器,到驱动框架,再到控制台应用,一条线走到底。
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移植思路都是类似的。记住:先硬件后软件,先轮询后中断,先验证后集成。按这个节奏来,不会出大问题。