4. UART驱动开发:从协议到三种收发模式
UART,说白了就是串口通信。做嵌入式开发的人,没有不跟它打交道的。我刚开始做驱动时,觉得UART太简单了——不就是发数据收数据吗?后来踩的坑多了才明白,轮询、中断、DMA这三种模式,用不好就是给自己挖坑。
这一章,咱们把UART驱动从协议到HAL层实现,掰开揉碎了讲清楚。
4.1 UART通信协议基础
UART是异步串行通信,没有时钟线。收发双方靠约定的波特率来同步。我见过不少新手,上来就调不通,最后发现是波特率算错了。
| 参数 | 说明 | 常见值 |
|---|---|---|
| 波特率 | 每秒传输的符号数 | 9600, 115200, 921600 |
| 数据位 | 一帧中的数据位数 | 7, 8 |
| 停止位 | 帧结束标志位 | 1, 1.5, 2 |
| 校验位 | 奇偶校验或无校验 | None, Even, Odd |
一帧UART数据长这样:起始位(0) + 数据位(LSB先发) + 校验位(可选) + 停止位(1)。
4.2 HAL UART接口设计
HAL层要做什么?把底层寄存器操作封装成统一的API。我设计HAL接口时,遵循一个原则:上层调用者不需要知道底层是哪个MCU。
先看结构体定义:
typedef struct {
uint32_t baudrate;
uint8_t data_bits; // 7 or 8
uint8_t stop_bits; // 1 or 2
uint8_t parity; // 0:None, 1:Odd, 2:Even
uint8_t mode; // 0:Polling, 1:Interrupt, 2:DMA
} UART_Config_t;
typedef struct {
UART_Config_t config;
uint8_t* rx_buffer;
uint32_t rx_buffer_size;
volatile uint32_t rx_index;
void (*tx_callback)(void* arg);
void (*rx_callback)(uint8_t data, void* arg);
void* callback_arg;
} UART_Handle_t;
接口函数就五个:
HAL_UART_Init(UART_Handle_t* huart)— 初始化串口HAL_UART_DeInit(UART_Handle_t* huart)— 反初始化HAL_UART_Transmit(UART_Handle_t* huart, uint8_t* data, uint32_t len)— 发送HAL_UART_Receive(UART_Handle_t* huart, uint8_t* data, uint32_t len)— 接收HAL_UART_IRQHandler(UART_Handle_t* huart)— 中断处理入口
设计要点:回调函数用void*参数传递上下文,这样同一个回调可以服务多个串口实例。我在项目里用这个模式,一个中断处理函数处理了6路UART,代码复用率很高。
4.3 轮询模式收发实现
轮询模式最简单,也最占CPU。适合调试阶段或者低速率通信。
发送实现:
HAL_Status_t HAL_UART_Transmit_Polling(UART_Handle_t* huart,
uint8_t* data, uint32_t len) {
for(uint32_t i = 0; i < len; i++) {
// 等待发送缓冲区空
while(!(huart->regs->SR & USART_SR_TXE));
huart->regs->DR = data[i];
}
// 等待发送完成
while(!(huart->regs->SR & USART_SR_TC));
return HAL_OK;
}
接收实现:
HAL_Status_t HAL_UART_Receive_Polling(UART_Handle_t* huart,
uint8_t* data, uint32_t len) {
for(uint32_t i = 0; i < len; i++) {
// 等待接收数据就绪
while(!(huart->regs->SR & USART_SR_RXNE));
data[i] = (uint8_t)(huart->regs->DR & 0xFF);
}
return HAL_OK;
}
4.4 中断模式收发实现
中断模式是实际项目中最常用的。CPU不用死等,数据来了再处理。
中断发送的思路:先触发TXE中断,在中断服务程序里一个字节一个字节地发,发完关中断。
void HAL_UART_Transmit_IT(UART_Handle_t* huart,
uint8_t* data, uint32_t len) {
huart->tx_buffer = data;
huart->tx_size = len;
huart->tx_index = 0;
// 使能发送缓冲区空中断
huart->regs->CR1 |= USART_CR1_TXEIE;
}
// 中断服务程序中调用
void HAL_UART_IRQHandler(UART_Handle_t* huart) {
// 发送中断
if(huart->regs->SR & USART_SR_TXE) {
if(huart->tx_index < huart->tx_size) {
huart->regs->DR = huart->tx_buffer[huart->tx_index++];
} else {
// 发送完成,关中断
huart->regs->CR1 &= ~USART_CR1_TXEIE;
if(huart->tx_callback)
huart->tx_callback(huart->callback_arg);
}
}
// 接收中断
if(huart->regs->SR & USART_SR_RXNE) {
uint8_t data = (uint8_t)(huart->regs->DR & 0xFF);
if(huart->rx_callback)
huart->rx_callback(data, huart->callback_arg);
}
}
接收中断更简单——每收到一个字节,就调用回调函数。上层应用在回调里处理数据。
4.5 DMA模式收发实现
DMA模式是性能最好的。CPU只需要配置好DMA,剩下的搬运工作DMA自己完成。适合大数据量、高速率通信。
DMA发送流程:
- 配置DMA通道:源地址=内存,目的地址=UART数据寄存器
- 设置传输长度
- 使能DMA,使能UART的DMA发送请求
- DMA传输完成触发中断,通知上层
HAL_Status_t HAL_UART_Transmit_DMA(UART_Handle_t* huart,
uint8_t* data, uint32_t len) {
// 配置DMA
DMA_Config_t dma_cfg = {
.src_addr = (uint32_t)data,
.dst_addr = (uint32_t)&huart->regs->DR,
.size = len,
.src_inc = 1, // 源地址递增
.dst_inc = 0, // 目的地址固定
.transfer_size = 1 // 字节传输
};
HAL_DMA_Config(huart->dma_tx_channel, &dma_cfg);
// 使能UART DMA发送
huart->regs->CR3 |= USART_CR3_DMAT;
// 启动DMA
HAL_DMA_Start(huart->dma_tx_channel);
return HAL_OK;
}
DMA接收类似,但要注意环形缓冲区的处理。我常用的做法是DMA双缓冲:
- DMA在接收数据时,CPU处理另一块缓冲区
- DMA传输完成中断里,交换两个缓冲区指针
- 这样数据流不会断
4.6 三种模式对比与选型
| 模式 | CPU占用 | 实时性 | 数据量 | 适用场景 |
|---|---|---|---|---|
| 轮询 | 高(100%占用) | 差(阻塞) | 小(<100字节) | 调试、初始化 |
| 中断 | 中(每字节中断) | 好 | 中(<1KB) | 通用通信 |
| DMA | 低(仅配置+完成中断) | 最好 | 大(>1KB) | 高速、大数据量 |
选型建议:
- 调试阶段用轮询,简单直接
- 产品中用中断,平衡性能和资源
- WiFi模块、蓝牙模块这类高速通信,必须用DMA
4.7 UART驱动知识体系
下面这张图,把UART驱动的核心脉络画出来了:
嗯,这张图把UART驱动的三个层次说清楚了。协议是基础,HAL接口是桥梁,三种模式是具体实现。实际项目中,你可能会在一个驱动里同时用中断和DMA——比如命令用中断收发,大数据用DMA收发。灵活搭配才是王道。