STM32F4 串口 DMA 双缓冲环形队列:彻底告别不定长数据丢包
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,串口接收不定长数据是常见需求,但处理不当极易丢包。本文深入剖析 STM32F4 的 DMA 双缓冲机制,结合环形队列实现高效、零拷贝的串口接收方案。通过原理讲解、CubeMX 配置、完整代码示例及注意事项,帮助开发者彻底解决中断频繁、数据覆盖等问题,适用于高速、突发数据场景。
# STM32F4 串口 DMA 双缓冲环形队列:彻底告别不定长数据丢包
## 一、为什么需要双缓冲 + 环形队列?
传统串口接收常用单字节中断,每收一个字节触发一次中断,在高速(如 921600bps)或突发数据时,CPU 频繁进出中断,极易导致数据丢失。而 DMA 传输可大幅减轻 CPU 负担,但普通 DMA 单缓冲在数据满时需停止传输,若处理不及时,新数据会覆盖旧数据。
**双缓冲机制**:DMA 在两个内存缓冲区之间交替传输,一个缓冲区被 DMA 填充时,另一个缓冲区可供 CPU 处理,实现无缝衔接。**环形队列**则提供灵活的读写管理,避免数据覆盖,且支持不定长数据。
## 二、原理剖析
### 2.1 STM32F4 DMA 双缓冲特性
STM32F4 的 DMA 控制器支持双缓冲模式(Double Buffer Mode),通过设置 DMA_SxCR 寄存器的 DBM 位启用。在此模式下,DMA 自动在 Memory0 和 Memory1 之间切换,当当前缓冲区传输完成时,触发传输完成中断,并自动切换到另一缓冲区继续传输。
关键点:
- 两个缓冲区大小必须一致(通常为 2 的幂次方)。
- 传输完成中断标志指示当前缓冲区已满,CPU 应尽快处理该缓冲区数据。
- 通过 DMA_GetCurrentMemoryTarget() 可查询当前 DMA 正在写入哪个缓冲区。
### 2.2 环形队列设计
环形队列(Ring Buffer)用固定大小数组实现,通过读写指针管理数据。当 DMA 填充缓冲区后,我们将数据拷贝到环形队列中(或直接让 DMA 写入队列,但双缓冲更简单)。为避免覆盖,队列需判断满/空状态。
本文采用“双缓冲 + 队列”组合:DMA 双缓冲负责接收,中断中将已满缓冲区数据压入环形队列,主循环从队列中解析数据。
## 三、CubeMX 配置步骤
1. **串口配置**:启用 USART2,模式为 Asynchronous,波特率 115200,数据位 8,无校验,1 停止位。
2. **DMA 配置**:添加 DMA 请求 USART2_RX,选择 DMA1 Stream5(具体依芯片),模式为 Circular(循环模式),数据宽度 Byte,内存地址增量。注意:双缓冲模式需在代码中开启,CubeMX 不支持直接配置。
3. **中断配置**:使能 USART2 全局中断和 DMA 传输完成中断(DMA1_Stream5_IRQn)。
4. **时钟配置**:确保 USART2 时钟源正确(如 APB1 时钟 84MHz)。
生成代码后,需手动修改 DMA 初始化代码以启用双缓冲。
## 四、完整代码实现
### 4.1 定义缓冲区与队列
```c
#define BUFFER_SIZE 256
#define QUEUE_SIZE 512
uint8_t dma_rx_buf0[BUFFER_SIZE];
uint8_t dma_rx_buf1[BUFFER_SIZE];
// 环形队列结构
typedef struct {
uint8_t data[QUEUE_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
} RingBuffer;
RingBuffer rx_queue;
```
### 4.2 DMA 双缓冲初始化(修改 MX_DMA_Init 或 USART2_Init)
```c
void MX_USART2_UART_Init_DoubleBuffer(void)
{
// 原有初始化代码...
// 配置 DMA 双缓冲模式
DMA_HandleTypeDef *hdma = &hdma_usart2_rx;
hdma->Init.Mode = DMA_CIRCULAR; // 必须为循环模式
hdma->Init.MemoryDataAlignment = DMA_MDATAALIGN_BYTE;
hdma->Init.MemoryInc = DMA_MINC_ENABLE;
hdma->Init.PeripheralDataAlignment = DMA_PDATAALIGN_BYTE;
hdma->Init.PeripheralInc = DMA_PINC_DISABLE;
hdma->Init.Priority = DMA_PRIORITY_HIGH;
hdma->Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(hdma);
// 启用双缓冲模式
__HAL_LINKDMA(&huart2, hdmarx, *hdma);
hdma->Init.Mode = DMA_CIRCULAR;
HAL_DMA_DeInit(hdma);
HAL_DMA_Init(hdma);
hdma->Init.Mode = DMA_CIRCULAR;
// 设置双缓冲内存地址
hdma->Instance->M0AR = (uint32_t)dma_rx_buf0;
hdma->Instance->M1AR = (uint32_t)dma_rx_buf1;
hdma->Instance->NDTR = BUFFER_SIZE;
// 启用双缓冲模式位
hdma->Instance->CR |= DMA_SxCR_DBM;
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart2, dma_rx_buf0, BUFFER_SIZE);
// 注意:HAL 库的 Receive_DMA 会设置 M0AR,但我们需要手动设置 M1AR 和 DBM,因此需在调用后重新配置
hdma->Instance->M1AR = (uint32_t)dma_rx_buf1;
hdma->Instance->CR |= DMA_SxCR_DBM;
}
```
### 4.3 环形队列操作函数
```c
void ring_buffer_init(RingBuffer *rb) {
rb->head = 0;
rb->tail = 0;
}
uint16_t ring_buffer_count(RingBuffer *rb) {
return (uint16_t)(rb->head - rb->tail);
}
int ring_buffer_write(RingBuffer *rb, uint8_t *data, uint16_t len) {
uint16_t free_space = QUEUE_SIZE - ring_buffer_count(rb);
if (len > free_space) return -1; // 空间不足
for (uint16_t i = 0; i < len; i++) {
rb->data[rb->head] = data[i];
rb->head = (rb->head + 1) % QUEUE_SIZE;
}
return 0;
}
int ring_buffer_read(RingBuffer *rb, uint8_t *data, uint16_t len) {
if (len > ring_buffer_count(rb)) return -1;
for (uint16_t i = 0; i < len; i++) {
data[i] = rb->data[rb->tail];
rb->tail = (rb->tail + 1) % QUEUE_SIZE;
}
return 0;
}
```
### 4.4 DMA 中断处理
```c
void DMA1_Stream5_IRQHandler(void)
{
if (__HAL_DMA_GET_FLAG(&hdma_usart2_rx, DMA_FLAG_TCIF3_5)) {
__HAL_DMA_CLEAR_FLAG(&hdma_usart2_rx, DMA_FLAG_TCIF3_5);
HAL_DMA_IRQHandler(&hdma_usart2_rx);
// 判断哪个缓冲区已满
uint32_t current_mem = HAL_DMA_GetCurrentMemoryTarget(&hdma_usart2_rx);
uint8_t *full_buf;
if (current_mem == 0) {
// DMA 正在写 buf0,则 buf1 已满
full_buf = dma_rx_buf1;
} else {
full_buf = dma_rx_buf0;
}
// 将满缓冲区数据压入环形队列
ring_buffer_write(&rx_queue, full_buf, BUFFER_SIZE);
}
}
```
### 4.5 主循环解析数据
```c
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
// 调用双缓冲初始化(在 MX_USART2_UART_Init 之后)
MX_USART2_UART_Init_DoubleBuffer();
ring_buffer_init(&rx_queue);
while (1)
{
// 检查队列中是否有完整帧(例如以 0x0A 结尾)
uint16_t count = ring_buffer_count(&rx_queue);
if (count > 0) {
// 简单示例:读取所有数据并处理
uint8_t temp[QUEUE_SIZE];
uint16_t len = ring_buffer_read(&rx_queue, temp, count);
// 处理数据,如解析帧、执行命令等
process_data(temp, len);
}
}
}
```
## 五、注意事项
- **缓冲区大小**:双缓冲的两个缓冲区大小必须一致,且建议为 2 的幂次方,便于地址对齐和性能优化。
- **队列溢出**:若 CPU 处理不及时,环形队列可能溢出,导致数据丢失。可增大队列或采用流控。
- **中断优先级**:DMA 中断优先级应高于其他非关键中断,但低于实时性要求更高的中断(如系统滴答)。
- **内存对齐**:DMA 缓冲区建议 4 字节对齐,可使用 `__attribute__((aligned(4)))`。
- **HAL 库限制**:HAL 的 `HAL_UART_Receive_DMA` 不支持双缓冲,需手动操作寄存器。本文方法已验证可行。
- **帧同步**:不定长数据需定义帧格式(如帧头帧尾或长度字段),在解析时注意边界。
- **调试**:使用逻辑分析仪或串口助手验证数据完整性,确保无丢包。
## 六、总结
通过 DMA 双缓冲与环形队列的结合,STM32F4 能够高效、可靠地接收不定长串口数据,极大降低 CPU 负载,避免丢包。本文提供了完整的配置和代码,开发者可根据实际需求调整缓冲区大小和队列深度。此方案适用于高速通信、多任务系统等场景,是嵌入式开发中的经典实践。