3、UART驱动与中断管理:轮询模式与中断模式、环形缓冲区设计、UART驱动封装与测试
UART,串口通信,嵌入式开发中最基础也最常用的外设。很多工程师觉得它简单,不就是发数据收数据嘛。但实际项目中,UART驱动写不好,系统卡死、丢数据、中断风暴,各种问题都会找上门。
我个人习惯把UART驱动分成三个层次:底层寄存器操作、中间层数据管理、上层应用接口。今天我们就从轮询和中断这两种模式讲起,再聊聊环形缓冲区这个经典设计,最后把整个驱动封装起来。
3.1 轮询模式 vs 中断模式
先看轮询模式。说白了就是CPU死等。发送时循环检查发送寄存器是否为空,接收时循环检查是否有数据到达。代码简单,逻辑清晰,但CPU被占死了。
// 轮询发送一个字节
void uart_poll_send(uint8_t data) {
while (!(UART->SR & UART_SR_TXE)); // 等待发送寄存器空
UART->DR = data;
}
// 轮询接收一个字节
uint8_t uart_poll_recv(void) {
while (!(UART->SR & UART_SR_RXNE)); // 等待接收数据
return (uint8_t)(UART->DR & 0xFF);
}
这种模式适合什么场景?调试打印、低速率通信、系统空闲时间多。但如果你在跑RTOS,或者要处理多个外设,轮询就是灾难。
中断模式就灵活多了。数据来了,硬件自动触发中断,CPU保存现场,执行中断服务函数,然后恢复现场继续干活。收发都不用CPU死等。
// UART中断服务函数(接收)
void UART_IRQHandler(void) {
if (UART->SR & UART_SR_RXNE) {
uint8_t data = (uint8_t)(UART->DR & 0xFF);
// 把数据存入缓冲区
ring_buffer_write(&rx_buf, data);
}
// 发送中断类似处理
}
这里有个坑。我曾经在一个项目中,中断服务函数里做了太多事情——又是解析协议,又是调用回调函数。结果中断嵌套太多,系统直接崩了。记住:中断服务函数要快进快出,只做最必要的事。
轮询 vs 中断 核心对比
| 对比项 | 轮询模式 | 中断模式 |
|---|---|---|
| CPU占用 | 高,一直等待 | 低,只在收发时响应 |
| 实时性 | 差,取决于轮询频率 | 好,硬件触发 |
| 代码复杂度 | 简单 | 中等,需处理中断上下文 |
| 适用场景 | 调试、低负载 | 多任务、高实时性 |
3.2 环形缓冲区设计
中断模式解决了CPU等待问题,但带来了新问题:数据怎么存?中断随时可能来,主循环随时可能取数据。如果直接用全局数组,读写冲突怎么办?
环形缓冲区就是干这个的。它是个固定大小的数组,用两个指针(读指针和写指针)来管理。写数据时移动写指针,读数据时移动读指针。只要缓冲区没满,读写就不会冲突。
typedef struct {
uint8_t *buffer; // 数据缓冲区
uint32_t size; // 缓冲区大小
volatile uint32_t head; // 写指针(生产者)
volatile uint32_t tail; // 读指针(消费者)
} ring_buffer_t;
// 初始化
void ring_buffer_init(ring_buffer_t *rb, uint8_t *buf, uint32_t size) {
rb->buffer = buf;
rb->size = size;
rb->head = 0;
rb->tail = 0;
}
// 写入一个字节
bool ring_buffer_write(ring_buffer_t *rb, uint8_t data) {
uint32_t next = (rb->head + 1) % rb->size;
if (next == rb->tail) {
return false; // 缓冲区满
}
rb->buffer[rb->head] = data;
rb->head = next;
return true;
}
// 读取一个字节
bool ring_buffer_read(ring_buffer_t *rb, uint8_t *data) {
if (rb->head == rb->tail) {
return false; // 缓冲区空
}
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
return true;
}
注意看,head和tail我都加了volatile关键字。为什么?因为中断和主循环都可能修改它们,编译器优化时可能会把变量值缓存到寄存器里,导致读写不一致。这个坑我踩过,调试了两天才发现是优化问题。
设计要点
- 缓冲区大小建议取2的幂次方,这样取模可以用位运算加速
- 判满和判空要区分清楚:head == tail 为空,next == tail 为满
- 中断中只写不读,主循环中只读不写,避免竞争
3.3 UART驱动封装
有了环形缓冲区,我们就可以把UART驱动封装成一套完整的接口。我个人习惯把驱动分成三层:硬件抽象层、驱动核心层、应用接口层。
// 硬件抽象层:与具体MCU相关
typedef struct {
void (*init)(uint32_t baudrate);
void (*send_byte)(uint8_t data);
uint8_t (*recv_byte)(void);
void (*enable_rx_interrupt)(bool enable);
void (*enable_tx_interrupt)(bool enable);
} uart_hw_ops_t;
// 驱动核心层:与硬件无关
typedef struct {
uart_hw_ops_t *hw; // 硬件操作函数指针
ring_buffer_t rx_buf; // 接收环形缓冲区
ring_buffer_t tx_buf; // 发送环形缓冲区
uint8_t rx_buf_data[256];
uint8_t tx_buf_data[256];
bool tx_busy; // 发送忙标志
} uart_device_t;
// 应用接口层:给用户调用
void uart_init(uart_device_t *dev, uint32_t baudrate);
void uart_send(uart_device_t *dev, uint8_t *data, uint32_t len);
uint32_t uart_recv(uart_device_t *dev, uint8_t *buf, uint32_t max_len);
void uart_irq_handler(uart_device_t *dev); // 中断中调用
这种分层设计的好处是:换MCU时只需要重写硬件抽象层,驱动核心层和应用接口层基本不用动。我在一个项目中从STM32F1换到GD32,只改了不到100行代码。
注意:中断处理中的发送逻辑
发送中断要小心处理。我见过有人每次发送都开中断,发完关中断。但如果在中断中又调用了发送函数,就会形成递归中断。我的做法是:用tx_busy标志位,发送时先关中断,把数据写入环形缓冲区,再开中断。中断服务函数里检查缓冲区是否为空,不为空就继续发。
3.4 测试与验证
驱动写完了,怎么测?不能光靠printf看现象。我一般会写一套测试用例,覆盖正常收发、缓冲区满、中断嵌套等场景。
// 测试用例1:正常收发
void test_normal_comm(void) {
uint8_t send_data[] = "Hello UART!";
uint8_t recv_buf[32];
uart_send(&dev, send_data, sizeof(send_data));
delay_ms(100); // 等待接收完成
uint32_t len = uart_recv(&dev, recv_buf, sizeof(recv_buf));
// 验证数据一致性
assert(len == sizeof(send_data));
assert(memcmp(send_data, recv_buf, len) == 0);
}
// 测试用例2:缓冲区满
void test_buffer_full(void) {
uint8_t data[300]; // 大于缓冲区大小
memset(data, 'A', sizeof(data));
uart_send(&dev, data, sizeof(data));
delay_ms(500);
uint32_t len = uart_recv(&dev, data, sizeof(data));
// 应该只能收到256字节(缓冲区大小)
assert(len == 256);
}
测试时最好用逻辑分析仪或串口助手抓波形。我习惯在测试用例里加超时机制,防止死等。比如接收超时5秒就报错退出。
测试清单
- 单字节收发测试
- 多字节连续收发测试(超过缓冲区大小)
- 中断频繁触发测试(模拟高负载)
- 长时间稳定性测试(跑24小时以上)
- 异常情况测试(拔线、重新插拔)
嗯,到这里UART驱动的基本框架就搭起来了。轮询模式简单直接,中断模式灵活高效,环形缓冲区解决了数据管理问题,分层封装让代码可移植。这些思路不仅适用于UART,SPI、I2C等外设驱动也可以用同样的方法。