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);
}
轮询的缺点很明显:CPU被占死了。你想想看,如果串口收一个字节要等1ms,这1ms里CPU啥也干不了。对于简单的“打印Hello World”没问题,但要是系统里还有传感器要读、电机要控,那就彻底完蛋。
3.2 中断模式:解放CPU
中断模式就好多了。数据来了,硬件自动触发中断,CPU暂停手头的事,去把数据收进来。收完继续干原来的活。
中断接收的典型流程:
- 使能USART的RXNE中断
- 在中断服务函数里读取DR寄存器
- 把数据放到一个环形缓冲区里
- 主循环从缓冲区取数据
代码骨架:
// 环形缓冲区
#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;
}
中断模式解决了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负载
3.4 实战:串口控制台与FinSH组件移植
好了,理论说完了,咱们动手。把串口控制台和FinSH跑起来,这是RT-Thread最常用的调试手段。
3.4.1 移植步骤
- 使能FinSH组件:在menuconfig里勾选FinSH,选择串口作为控制台
- 实现底层驱动:在drv_usart.c里实现serial框架需要的接口
- 注册设备:调用
rt_hw_serial_register()注册串口设备 - 配置控制台:设置
RT_CONSOLE_DEVICE_NAME为你的串口设备名 - 测试:编译下载,用串口工具连接,敲回车,看到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驱动开发的三个层次和关键节点串起来了:
3.6 避坑指南
最后,分享几个我实际项目中遇到的坑:
- 波特率不准:STM32的USART时钟源可能是APB1或APB2,频率不同。算波特率分频时一定要确认时钟源。我见过有人用72MHz算分频,结果时钟是36MHz,波特率差了一倍。
- 中断优先级:FinSH的串口中断优先级不能太高,否则会打断其他关键中断。也不能太低,否则丢数据。我一般设成中等优先级,比如5(0最高,15最低)。
- 缓冲区溢出:如果FinSH命令执行时间较长,串口接收缓冲区可能会满。适当加大缓冲区,或者用DMA模式缓解。
- 流控问题:如果硬件上有RTS/CTS流控,记得在配置里关掉,否则只接TX/RX三根线会收不到数据。
嗯,这一章的内容就到这。UART驱动是RT-Thread驱动开发的“敲门砖”,搞懂了它,后面的I2C、SPI、CAN等框架,你会发现套路都差不多。