3. UART驱动与HAL封装:轮询模式、中断模式、环形缓冲区设计、波特率配置实战
UART,说白了就是串口。做嵌入式开发,这东西几乎天天见。调试打印、跟传感器通信、对接蓝牙模块,哪哪都离不开它。但很多新手写UART驱动,就是简单调个库函数,一旦遇到数据错乱、丢字节、波特率对不上,立马抓瞎。
我个人习惯,不管项目多小,UART驱动一定要做HAL封装。为什么?因为换芯片的时候你就知道了。我早期一个项目,从STM32F1换到GD32,底层寄存器全变,但上层协议栈一点没动——全靠HAL层挡了一刀。
3.1 轮询模式:简单但别滥用
轮询模式,就是CPU死等。发一个字节,等发送完成;读一个字节,等接收完成。代码写起来最直接,但效率最低。
// 轮询发送一个字节
void uart_poll_send(UART_HandleTypeDef *huart, uint8_t data)
{
// 等待发送缓冲区空
while (!(huart->SR & USART_SR_TXE));
// 写入数据
huart->DR = data;
// 等待发送完成
while (!(huart->SR & USART_SR_TC));
}
// 轮询接收一个字节
uint8_t uart_poll_recv(UART_HandleTypeDef *huart)
{
// 等待接收到数据
while (!(huart->SR & USART_SR_RXNE));
return (uint8_t)(huart->DR);
}
这段代码,每个字节都要死等。你想想看,如果波特率是115200,一个字节大概87微秒,CPU就干等着。要是系统里还有别的任务要跑,这种写法直接拖垮实时性。
3.2 中断模式:让CPU喘口气
中断模式就好多了。CPU该干嘛干嘛,数据来了,硬件自动触发中断,你在中断服务函数里处理。我习惯把中断服务函数写得尽量短,只做数据搬运,不做协议解析。
// 中断服务函数(以STM32为例)
void USART1_IRQHandler(void)
{
uint8_t data;
// 检查接收中断标志
if (USART1->SR & USART_SR_RXNE)
{
data = (uint8_t)(USART1->DR);
// 把数据放入环形缓冲区
ringbuf_put(&rx_ringbuf, data);
}
// 检查发送中断标志(如果需要)
if (USART1->SR & USART_SR_TXE)
{
// 从发送缓冲区取数据发送
if (ringbuf_get(&tx_ringbuf, &data) == 0)
{
USART1->DR = data;
}
else
{
// 没有数据要发送,关闭发送中断
USART1->CR1 &= ~USART_CR1_TXEIE;
}
}
}
这里有个坑,我踩过好几次。中断里操作缓冲区,一定要考虑原子性。如果主循环也在读写同一个缓冲区,不加保护的话,数据竞争是必然的。最简单的做法:中断里只写,主循环里只读,或者反过来。
3.3 环形缓冲区设计:核心中的核心
环形缓冲区,说白了就是一个固定大小的数组,加上两个指针:写指针和读指针。数据写满了就从头覆盖,读指针追着写指针走。
我项目中用过很多种实现,最稳定的是这种:
// 环形缓冲区结构体
typedef struct {
uint8_t *buffer; // 缓冲区内存
uint32_t size; // 缓冲区大小(必须是2的幂)
volatile uint32_t head; // 写指针
volatile uint32_t tail; // 读指针
} RingBuf_t;
// 初始化
void ringbuf_init(RingBuf_t *rb, uint8_t *buf, uint32_t size)
{
rb->buffer = buf;
rb->size = size;
rb->head = 0;
rb->tail = 0;
}
// 写入一个字节
int ringbuf_put(RingBuf_t *rb, uint8_t data)
{
uint32_t next_head = (rb->head + 1) & (rb->size - 1);
// 缓冲区满
if (next_head == rb->tail)
return -1;
rb->buffer[rb->head] = data;
rb->head = next_head;
return 0;
}
// 读取一个字节
int ringbuf_get(RingBuf_t *rb, uint8_t *data)
{
// 缓冲区空
if (rb->head == rb->tail)
return -1;
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) & (rb->size - 1);
return 0;
}
- 缓冲区大小必须是2的幂,这样可以用位运算代替取模,效率高很多
- head和tail用volatile修饰,防止编译器优化掉中断和主循环之间的共享变量
- 判断满和空的条件:head == tail表示空,(head+1) & (size-1) == tail表示满
- 实际可用空间是size-1,留一个空位区分满和空
我曾经在一个项目里,缓冲区大小设成了100,不是2的幂。结果取模运算在中断里多花了几个时钟周期,波特率高了之后就开始丢数据。改成128之后,问题立刻消失。嗯,这里要注意,嵌入式里,细节真的决定成败。
3.4 波特率配置实战
波特率配置,看起来就是套公式,但实际项目中翻车最多的地方就在这里。不同芯片的时钟树不一样,APB总线频率不同,分频系数不同,算出来的实际波特率和目标波特率有误差。
标准公式:
波特率 = 时钟频率 / (16 * USARTDIV)
// USARTDIV是分频系数,可以是小数
// 例如:时钟频率72MHz,目标波特率115200
// USARTDIV = 72000000 / (16 * 115200) = 39.0625
// 整数部分:39,小数部分:0.0625 * 16 = 1
// 所以:USART_BRR = (39 << 4) | 1 = 0x271
| 目标波特率 | 时钟频率 | 实际波特率 | 误差 | 是否可用 |
|---|---|---|---|---|
| 115200 | 72 MHz | 115200 | 0.00% | ✅ 完美 |
| 115200 | 84 MHz | 115068 | 0.11% | ✅ 可用 |
| 115200 | 100 MHz | 114547 | 0.57% | ⚠️ 临界 |
| 115200 | 120 MHz | 113636 | 1.36% | ❌ 不可用 |
3.5 HAL封装:统一接口,屏蔽差异
HAL封装的核心思想:上层代码不关心底层是哪个芯片、哪个UART外设。你只需要定义一套统一的接口,然后针对不同平台实现底层。
// HAL层统一接口
typedef struct {
int (*init)(uint32_t baudrate);
int (*send)(uint8_t *data, uint32_t len);
int (*recv)(uint8_t *data, uint32_t timeout_ms);
void (*irq_handler)(void);
} UART_HAL_t;
// 具体平台实现(以STM32为例)
static int stm32_uart_init(uint32_t baudrate)
{
// 配置GPIO、USART寄存器、NVIC中断
// 设置波特率
// 使能USART
return 0;
}
static int stm32_uart_send(uint8_t *data, uint32_t len)
{
// 把数据写入发送环形缓冲区
// 使能发送中断
return 0;
}
static int stm32_uart_recv(uint8_t *data, uint32_t timeout_ms)
{
// 从接收环形缓冲区读取数据
// 支持超时返回
return 0;
}
// HAL层实例
const UART_HAL_t uart_hal_stm32 = {
.init = stm32_uart_init,
.send = stm32_uart_send,
.recv = stm32_uart_recv,
.irq_handler = stm32_uart_irq_handler,
};
这样做的好处很明显。上层协议栈(比如Modbus、AT指令解析器)只跟UART_HAL_t打交道。换芯片的时候,只需要重新实现这4个函数,上层代码一行都不用改。
- 接口要足够抽象,不暴露任何寄存器细节
- 支持同步和异步两种模式
- 错误码统一,上层不用关心底层错误类型
- 中断处理函数由HAL层管理,上层不直接操作中断
3.6 知识结构总览
下面这张图,把UART驱动从底层到上层的完整结构画出来了。你对照着看,思路会更清楚。
从这张图可以看得很清楚:应用层只跟HAL封装层打交道,HAL层调用驱动核心层的三种模式,驱动核心层再通过硬件抽象层操作寄存器。每一层各司其职,换芯片的时候,只需要改最下面两层。
嗯,UART这部分内容,看起来基础,但真正做扎实了,后面所有串行通信的中间件集成都会轻松很多。你想想看,Modbus、AT指令、GPS NMEA协议,底层都是UART。HAL层封装好了,上层就是纯粹的协议解析逻辑,跟硬件彻底解耦。