4. 串口通信:UART轮询、中断、DMA模式,以及环形缓冲区实现
串口,说白了就是嵌入式世界的“串行电话线”。你想想看,从调试打印到跟传感器、Wi-Fi模块、蓝牙模块打交道,哪个离得开UART?我做了这么多年项目,几乎每个板子至少留一个串口,就是为了万一出问题能扒开看看里面在干什么。
但很多人把串口想简单了——以为就是调个波特率、发个数据完事。其实,UART的三种工作模式(轮询、中断、DMA)选不对,轻则CPU空转浪费,重则丢数据、死机。今天我就把这三种模式掰开揉碎了讲清楚,再送你一个实战级的环形缓冲区实现。
4.1 UART硬件基础回顾
先快速过一下STM32的UART外设结构。每个USART(通用同步异步收发器)内部有独立的发送数据寄存器(TDR)和接收数据寄存器(RDR),它们共享一个移位寄存器。
发送时,你把数据写到TDR,硬件自动移位到TX引脚上。接收时,移位寄存器把RX引脚上的串行数据拼成字节,放到RDR里等你取走。
关键标志位就两个:
- TXE(发送数据寄存器空):TDR空了,可以写下一个字节
- RXNE(接收数据寄存器非空):RDR有数据,赶紧读走
嗯,这里要注意:如果你在RXNE置位后没及时读走数据,下一个字节进来就会覆盖——这就是经典的“溢出错误”。我在调试一个GPS模块时就吃过这个亏,后面会细说。
4.2 轮询模式:简单但低效
轮询模式就是CPU死等。发送时循环检查TXE,接收时循环检查RXNE。代码写起来最直接:
// 轮询发送一个字节
void UART_SendByte(USART_TypeDef* USARTx, uint8_t data)
{
while (!(USARTx->SR & USART_SR_TXE)); // 等TDR空
USARTx->DR = data;
}
// 轮询接收一个字节
uint8_t UART_ReceiveByte(USART_TypeDef* USARTx)
{
while (!(USARTx->SR & USART_SR_RXNE)); // 等RDR有数据
return (uint8_t)(USARTx->DR);
}
这段代码看着简单,但有个致命问题:CPU被完全占住了。如果你用轮询方式打印调试信息,发送一个字节要等几十微秒,发100个字节就是几毫秒——这期间CPU什么都不能干。
4.3 中断模式:解放CPU的关键一步
中断模式就好多了。你配置好UART中断,数据来了硬件自动调用中断服务函数(ISR),CPU不用空等。
配置步骤:
- 使能UART接收中断(RXNEIE位)
- 编写中断服务函数,在函数里读DR
- 使能NVIC中的UART中断通道
中断服务函数示例:
void USART1_IRQHandler(void)
{
if (USART1->SR & USART_SR_RXNE)
{
uint8_t data = USART1->DR;
// 把data存入缓冲区,或者直接处理
// 注意:ISR里不要做耗时操作!
}
}
中断模式解决了CPU空等的问题,但带来了新挑战:中断频率。如果波特率是115200,每秒约11520个字节,也就是每87微秒触发一次中断。如果ISR处理时间超过87微秒,就会丢数据。
4.4 DMA模式:零CPU开销的终极方案
DMA(直接存储器访问)模式,说白了就是给CPU请了个“数据搬运工”。你告诉DMA控制器:从UART的DR寄存器搬100个字节到内存数组里,搬完了通知我。然后CPU就可以去干别的事了。
STM32的UART DMA配置要点:
- 发送DMA:内存→外设,数据方向从内存到UART的DR
- 接收DMA:外设→内存,数据方向从UART的DR到内存
- 循环模式:接收DMA可以配置成循环模式,自动从头开始覆盖缓冲区
配置代码片段(以USART1接收DMA为例):
// 假设已经初始化了DMA1通道5(USART1_RX)
#define RX_BUF_SIZE 256
uint8_t rx_buffer[RX_BUF_SIZE];
void UART_DMA_Init(void)
{
// 配置DMA
DMA1_Channel5->CPAR = (uint32_t)&USART1->DR; // 外设地址
DMA1_Channel5->CMAR = (uint32_t)rx_buffer; // 内存地址
DMA1_Channel5->CNDTR = RX_BUF_SIZE; // 传输数量
DMA1_Channel5->CCR = DMA_CCR_MINC | // 内存地址递增
DMA_CCR_CIRC | // 循环模式
DMA_CCR_TCIE; // 传输完成中断
// 使能DMA
DMA1_Channel5->CCR |= DMA_CCR_EN;
// 使能USART1的DMA接收
USART1->CR3 |= USART_CR3_DMAR;
}
4.5 三种模式对比
| 特性 | 轮询模式 | 中断模式 | DMA模式 |
|---|---|---|---|
| CPU占用率 | 高(100%占用) | 中(每次中断占用) | 低(几乎为0) |
| 实时性 | 差(阻塞等待) | 好(立即响应) | 好(硬件自动处理) |
| 代码复杂度 | 低 | 中 | 高 |
| 适用场景 | 初始化打印、低频通信 | 中低速数据、命令交互 | 高速数据流、大块数据传输 |
| 数据丢失风险 | 低(但CPU被占) | 中(ISR处理不及时) | 低(硬件保证) |
4.6 环形缓冲区:解决数据粘包和丢失的利器
不管用中断还是DMA,你都会面临一个问题:数据是源源不断进来的,但CPU不可能每收到一个字节就处理一次。你需要一个缓冲区,把数据先存起来,等CPU有空了再处理。
环形缓冲区(Ring Buffer)就是为此而生。它用一块固定大小的内存,通过头指针(读指针)和尾指针(写指针)实现循环存取。
// 环形缓冲区结构体
typedef struct {
uint8_t buffer[RING_BUF_SIZE];
volatile uint16_t head; // 写指针,ISR中修改
volatile uint16_t tail; // 读指针,主循环中修改
} RingBuffer_t;
// 写入一个字节(通常在ISR中调用)
uint8_t RingBuf_Write(RingBuffer_t* rb, uint8_t data)
{
uint16_t next_head = (rb->head + 1) % RING_BUF_SIZE;
if (next_head == rb->tail) {
return 0; // 缓冲区满,丢数据
}
rb->buffer[rb->head] = data;
rb->head = next_head;
return 1;
}
// 读取一个字节(在主循环中调用)
uint8_t RingBuf_Read(RingBuffer_t* rb, uint8_t* data)
{
if (rb->head == rb->tail) {
return 0; // 缓冲区空
}
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % RING_BUF_SIZE;
return 1;
}
4.7 实战组合:DMA + 环形缓冲区
最推荐的方案是:DMA负责数据搬运,环形缓冲区负责数据缓存,主循环负责数据处理。这样CPU几乎零开销,数据也不会丢。
实现思路:
- DMA接收配置为循环模式,缓冲区大小设为512字节
- DMA传输完成中断里,记录当前DMA的CNDTR值,计算出新数据的位置
- 把新数据从DMA缓冲区拷贝到环形缓冲区
- 主循环从环形缓冲区读取数据并解析
// DMA传输完成中断
void DMA1_Channel5_IRQHandler(void)
{
if (DMA1->ISR & DMA_ISR_TCIF5)
{
uint16_t current_cnt = DMA1_Channel5->CNDTR;
// 计算本次接收到的数据量
uint16_t received = (last_cnt - current_cnt + RX_BUF_SIZE) % RX_BUF_SIZE;
// 从DMA缓冲区拷贝到环形缓冲区
for (uint16_t i = 0; i < received; i++)
{
uint16_t idx = (last_pos + i) % RX_BUF_SIZE;
RingBuf_Write(&uart_rb, rx_buffer[idx]);
}
last_cnt = current_cnt;
last_pos = (last_pos + received) % RX_BUF_SIZE;
DMA1->IFCR |= DMA_IFCR_CTCIF5; // 清除中断标志
}
}
4.8 知识体系总览
下面这张图把UART三种模式的选择逻辑和环形缓冲区的关系梳理清楚了:
4.9 避坑总结
- 波特率误差:STM32的UART时钟源要算准,误差超过2%就可能丢数据。我习惯用8倍过采样(OVER8=0),精度更高
- 中断优先级:UART中断优先级不要设太高,否则会阻塞其他关键中断。一般设到中等优先级就够了
- DMA缓冲区对齐:DMA缓冲区地址要4字节对齐,否则在某些STM32型号上会触发硬件错误
- 环形缓冲区大小:必须是2的幂次方,这样取模运算可以用位与(&)代替%,效率高很多
好了,UART的三种模式和环形缓冲区就讲到这里。这些内容是我多年实战中反复验证过的,你照着做基本不会出大问题。记住一句话:轮询保底、中断通用、DMA为王,环形缓冲区是最后的保险。