4. UART驱动移植与实战(串口控制台)
好,咱们进入第四章。这一章可以说是整个课程里最实用的一章——串口。你想想看,做嵌入式开发,串口就是你的眼睛和嘴巴。没有串口,你连系统跑没跑起来都不知道。
我个人习惯,拿到一块新板子,第一件事就是把串口调通。为什么?因为后面所有的调试信息、交互命令,全得靠它。RT-Thread 的 FinSH 组件更是离不开串口。所以这一章,咱们把 STM32 的 UART 从底层到上层,彻底搞明白。
4.1 STM32 UART 基础与 DMA 配置
先说说 STM32 的 UART 外设。说白了,它就是一套串行通信接口,负责把并行数据转成串行比特流发出去,再把收到的串行比特流还原成数据。
STM32 的 USART(通用同步异步收发器)比普通 UART 多了同步功能,但咱们做串口控制台,用异步模式就够了。核心寄存器就那么几个:
| 寄存器 | 作用 | 我常用的配置 |
|---|---|---|
| USART_SR | 状态寄存器 | 查 TXE、TC、RXNE 标志位 |
| USART_DR | 数据寄存器 | 读/写 9 位数据 |
| USART_BRR | 波特率寄存器 | 根据时钟算分频系数 |
| USART_CR1~CR3 | 控制寄存器 | 使能 UE、TE、RE、DMA 等 |
波特率怎么算?公式很简单:波特率 = fCK / (16 × USARTDIV)。其中 fCK 是外设时钟,USARTDIV 就是 BRR 寄存器里的值。我一般用 115200,这个速率稳定,而且大部分串口工具都支持。
再说 DMA。为什么要用 DMA?因为传统的中断方式,每收发一个字节就要进一次中断。115200 波特率下,每秒约 11520 个字节,也就是 11520 次中断。CPU 光处理中断就忙不过来了。
DMA 的配置流程,我总结为四步:
- 使能 DMA 时钟 —— RCC 里打开对应 DMA 控制器时钟
- 配置 DMA 通道 —— 设置源地址、目标地址、传输方向、数据宽度
- 关联 USART 请求 —— USART_CR3 里使能 DMAT 和 DMAR
- 启动 DMA 传输 —— 使能 DMA 通道,开始干活
这里有个坑,我曾经踩过:DMA 的传输完成中断和 USART 的传输完成中断是两回事。DMA 把数据搬完了,不代表 USART 已经发完了。要等 USART 的 TC 标志位置位,才算真正发送完成。
4.2 RT-Thread Serial 设备驱动框架解析
RT-Thread 的 Serial 设备驱动框架,说白了就是一套标准接口。它把各种串口芯片的差异都封装起来了,你只需要实现几个回调函数,就能让你的 UART 被系统识别和使用。
框架的核心结构体是 struct rt_serial_device。它长这样:
struct rt_serial_device
{
struct rt_device parent; // 继承自 rt_device
const struct rt_uart_ops *ops; // 操作函数集
struct serial_configure config; // 配置参数
void *serial_rx; // 接收缓冲区
void *serial_tx; // 发送缓冲区
rt_uint8_t rx_int; // 接收中断标志
rt_uint8_t tx_int; // 发送中断标志
};
你注意看,它里面有个 ops 指针,指向 struct rt_uart_ops。这个结构体定义了所有你需要实现的函数:
| 函数 | 作用 | 说明 |
|---|---|---|
| configure | 配置串口参数 | 波特率、数据位、校验位、停止位 |
| control | 控制命令 | 如流控、DMA 使能/禁能 |
| putc | 发送一个字符 | 轮询模式用 |
| getc | 接收一个字符 | 轮询模式用 |
| dma_transmit | DMA 发送 | 非阻塞模式用 |
| dma_receive | DMA 接收 | 非阻塞模式用 |
框架的工作流程是这样的:上层应用调用 rt_device_write(),框架根据配置的模式(轮询/中断/DMA),调用对应的 ops 函数。你只需要把底层的硬件操作填进去就行。
为了让你更直观地理解,我画了一张框架图:
4.3 编写 UART 驱动
好,理论说完了,咱们动手写代码。目标是让 STM32 的 USART1 被 RT-Thread 识别为一个 Serial 设备。
第一步,定义设备结构体。我习惯把硬件相关的信息都放在一个结构体里:
struct stm32_uart
{
USART_TypeDef *instance; // USART 基地址
IRQn_Type irq; // 中断号
DMA_Stream_TypeDef *dma_tx; // DMA 发送流
DMA_Stream_TypeDef *dma_rx; // DMA 接收流
rt_uint32_t dma_tc_flag; // DMA 传输完成标志
};
static struct stm32_uart uart1_dev =
{
.instance = USART1,
.irq = USART1_IRQn,
.dma_tx = DMA2_Stream7,
.dma_rx = DMA2_Stream5,
};
第二步,实现 ops 函数。以 configure 为例:
static rt_err_t uart_configure(struct rt_serial_device *serial,
struct serial_configure *cfg)
{
struct stm32_uart *uart = (struct stm32_uart *)serial->parent.user_data;
USART_TypeDef *usart = uart->instance;
uint32_t baud_div;
// 计算波特率分频值
baud_div = HAL_RCC_GetPCLK2Freq() / (16 * cfg->baud_rate);
usart->BRR = baud_div;
// 配置数据位、停止位、校验位
usart->CR1 = (usart->CR1 & ~0x3000) | (cfg->data_bits << 12);
usart->CR2 = (usart->CR2 & ~0x3000) | (cfg->stop_bits << 12);
usart->CR1 = (usart->CR1 & ~0x100) | (cfg->parity << 8);
// 使能 USART
usart->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
return RT_EOK;
}
第三步,注册设备。在 rt_hw_usart_init() 函数里完成:
int rt_hw_usart_init(void)
{
rt_err_t ret;
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
// 设置波特率为 115200
config.baud_rate = BAUD_RATE_115200;
// 注册 Serial 设备
ret = rt_hw_serial_register(&serial1,
"uart1",
RT_DEVICE_FLAG_RDWR |
RT_DEVICE_FLAG_INT_RX |
RT_DEVICE_FLAG_DMA_TX,
&uart1_dev);
if (ret != RT_EOK)
{
return ret;
}
// 配置串口参数
rt_device_control(&serial1.parent, RT_DEVICE_CTRL_CONFIG, &config);
return RT_EOK;
}
RT_DEVICE_FLAG_DMA_TX。如果只写 RT_DEVICE_FLAG_RDWR,框架默认用轮询模式,性能差很多。
第四步,实现中断处理。接收中断里,要把数据放入框架的接收缓冲区:
void USART1_IRQHandler(void)
{
rt_interrupt_enter();
if (USART1->SR & USART_SR_RXNE)
{
// 读取数据
char ch = USART1->DR;
// 通知框架有数据到达
rt_hw_serial_isr(&serial1, RT_SERIAL_EVENT_RX_IND);
}
rt_interrupt_leave();
}
4.4 实战:串口控制台输出与 FinSH 组件集成
驱动写好了,怎么验证?最直接的办法——让串口输出 "Hello, RT-Thread!"。
首先,在 rtconfig.h 里打开 FinSH 组件:
#define RT_USING_FINSH
#define FINSH_USING_MSH
#define FINSH_THREAD_NAME "tshell"
#define FINSH_USING_HISTORY
#define FINSH_HISTORY_LINES 5
然后,在 board.h 里把控制台设备名改成 "uart1":
#define RT_CONSOLE_DEVICE_NAME "uart1"
编译、下载、上电。打开串口工具,你应该能看到 RT-Thread 的启动信息,然后出现 msh > 提示符。输入 help,能看到所有支持的命令。
list_device,如果能看到 "uart1" 设备,说明注册成功。输入 ps,能看到 "tshell" 线程在运行,说明 FinSH 正常工作。
这里有个小细节,我提一下。FinSH 的输入输出,本质上是调用了 rt_device_read() 和 rt_device_write()。你写的驱动如果正确实现了 ops,FinSH 就能正常工作。不需要额外写任何适配代码。
我曾经在一个项目里,因为 DMA 发送完成中断没处理好,导致 FinSH 输入命令后没反应。查了半天,发现是 DMA 传输完成后没有调用 rt_hw_serial_isr() 通知框架。加上之后就好了。
最后,给你一个完整的驱动文件结构参考:
| 文件 | 内容 |
|---|---|
| drv_usart.c | ops 函数实现、中断处理、DMA 回调 |
| drv_usart.h | 设备结构体定义、函数声明 |
| board.c | 调用 rt_hw_usart_init()、配置控制台设备 |
| rtconfig.h | 使能 FinSH 和 Serial 框架宏定义 |
嗯,这一章的内容就到这。串口驱动是 RT-Thread 驱动移植的入门关,也是最重要的一关。搞定了它,后面的 SPI、I2C 驱动移植,思路都是一样的——理解框架、实现 ops、注册设备。你把这个流程走一遍,后面就轻车熟路了。