STM32F4 DMA+双缓冲串口接收:帧边界与超时判定的精准实现
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,使用DMA+双缓冲接收不定长串口数据是提升系统实时性的常用手段,但帧边界识别和超时判定常成为难点。本文深入剖析STM32F4的DMA双缓冲机制,结合空闲中断与定时器超时检测,给出一种高效、可靠的帧处理方案,并附完整代码示例,帮助开发者规避常见坑点,实现精准的数据帧解析。
# STM32F4 DMA+双缓冲串口接收:帧边界与超时判定的精准实现
## 1. 为什么需要DMA+双缓冲?
在串口通信中,不定长数据的接收是常见需求。传统中断逐字节接收会频繁打断CPU,尤其在高速率(如921600bps)下,CPU占用率极高。DMA(直接内存访问)可自动将数据从外设搬运到内存,配合双缓冲机制,能实现数据接收与处理的流水线化,大幅降低CPU负载。
双缓冲的核心思想是:两个缓冲区交替使用,一个用于DMA写入,另一个供应用程序读取,从而避免数据覆盖和等待。
## 2. 帧边界识别与超时判定的挑战
不定长数据意味着接收方必须自行判断一帧数据的结束。常见方法有:
- **空闲中断(IDLE)**:当总线空闲一个字节时间后触发,适合帧间间隔固定的场景。
- **超时判定**:通过定时器监测接收间隔,超过阈值则认为帧结束,适用于帧间隔不固定的场景。
但DMA+双缓冲下,数据可能跨缓冲区,且空闲中断与DMA传输完成中断的时序关系复杂,需精心设计才能避免丢帧或错帧。
## 3. 硬件与软件架构
### 3.1 硬件资源
- STM32F407(或其他F4系列)
- USART1(PA9/PA10)
- DMA2 Stream2(USART1_RX)
- TIM2(用于超时检测)
### 3.2 软件设计思路
1. 配置USART1的DMA接收,使用双缓冲模式(DMA_CTRL_MEMORY_INC | DMA_CTRL_CIRCULAR)。
2. 使能USART1的空闲中断(IDLE)。
3. 在空闲中断中,计算当前DMA接收位置,提取一帧数据,并切换缓冲区。
4. 同时启动一个定时器(如TIM2)作为超时保护,若在设定时间内未收到新数据,则强制结束当前帧。
## 4. 关键配置步骤
### 4.1 初始化DMA双缓冲
```c
// 定义双缓冲区,大小256字节
uint8_t dma_buf1[256];
uint8_t dma_buf2[256];
void DMA_Config(void) {
DMA_InitTypeDef DMA_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
DMA_DeInit(DMA2_Stream2);
DMA_InitStructure.DMA_Channel = DMA_Channel_4; // USART1_RX
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)dma_buf1;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStructure.DMA_BufferSize = 256;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式,实现双缓冲
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream2, &DMA_InitStructure);
// 使能DMA传输完成中断(用于切换缓冲区)
DMA_ITConfig(DMA2_Stream2, DMA_IT_TC, ENABLE);
DMA_Cmd(DMA2_Stream2, ENABLE);
}
```
### 4.2 配置USART1空闲中断
```c
void USART_Config(void) {
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_RX | USART_Mode_TX;
USART_Init(USART1, &USART_InitStructure);
// 使能空闲中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE);
USART_Cmd(USART1, ENABLE);
}
```
### 4.3 定时器超时检测(TIM2)
```c
void TIM2_Config(void) {
TIM_TimeBaseInitTypeDef TIM_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_InitStructure.TIM_Period = 1000; // 1ms @ 1MHz计数
TIM_InitStructure.TIM_Prescaler = 84 - 1; // 84MHz/84 = 1MHz
TIM_InitStructure.TIM_ClockDivision = 0;
TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_InitStructure);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2, DISABLE); // 初始关闭
}
```
## 5. 帧处理逻辑与代码实现
核心思想:在空闲中断中,根据DMA当前计数位置,确定数据长度,并将数据从当前缓冲区拷贝到应用层。同时,启动定时器作为超时保护,若定时器溢出(即超时),则强制结束当前帧。
### 5.1 全局变量与状态机
```c
#define BUF_SIZE 256
volatile uint8_t rx_complete_flag = 0; // 帧完成标志
volatile uint16_t rx_len = 0; // 当前帧长度
volatile uint8_t *current_buf; // 当前DMA写入的缓冲区
// 缓冲区索引管理
volatile uint8_t buf_index = 0; // 0: buf1, 1: buf2
```
### 5.2 空闲中断处理
```c
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_IDLE)) {
USART_ClearITPendingBit(USART1, USART_IT_IDLE);
// 读取DMA剩余数据计数
uint16_t remaining = DMA_GetCurrDataCounter(DMA2_Stream2);
uint16_t received = BUF_SIZE - remaining; // 本次接收到的字节数
// 确定当前缓冲区
if (buf_index == 0) {
current_buf = dma_buf1;
} else {
current_buf = dma_buf2;
}
// 如果接收长度大于0,则处理帧
if (received > 0) {
rx_len = received;
// 此处可调用帧处理函数,例如:ProcessFrame(current_buf, rx_len);
rx_complete_flag = 1;
}
// 切换缓冲区(双缓冲)
buf_index ^= 1;
if (buf_index == 0) {
DMA_SetCurrDataCounter(DMA2_Stream2, BUF_SIZE);
DMA_MemoryTargetConfig(DMA2_Stream2, (uint32_t)dma_buf1, DMA_Memory_0);
} else {
DMA_SetCurrDataCounter(DMA2_Stream2, BUF_SIZE);
DMA_MemoryTargetConfig(DMA2_Stream2, (uint32_t)dma_buf2, DMA_Memory_0);
}
// 启动超时定时器(例如10ms)
TIM_SetCounter(TIM2, 0);
TIM_Cmd(TIM2, ENABLE);
}
}
```
### 5.3 定时器超时处理
```c
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update)) {
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
TIM_Cmd(TIM2, DISABLE);
// 超时未收到新数据,强制结束当前帧
if (rx_complete_flag == 0) {
// 处理残留在缓冲区中的数据(可能不完整)
// 例如:丢弃或作为错误处理
rx_len = 0;
}
}
}
```
### 5.4 主循环处理帧数据
```c
int main(void) {
// 初始化...
while (1) {
if (rx_complete_flag) {
rx_complete_flag = 0;
// 处理帧数据,current_buf指向有效数据
ProcessFrame(current_buf, rx_len);
}
}
}
```
## 6. 注意事项与常见坑点
- **缓冲区大小与DMA计数**:DMA的剩余计数在空闲中断时读取,注意在切换缓冲区后必须重新设置计数,否则会错乱。
- **双缓冲切换时机**:必须在空闲中断中完成切换,避免DMA正在写入时切换导致数据错位。
- **超时时间选择**:应根据波特率和帧间隔设置,一般取帧间隔的2-3倍,过短会误判,过长影响实时性。
- **中断优先级**:USART空闲中断优先级应高于定时器,避免超时中断抢占导致帧丢失。
- **DMA传输完成中断**:在循环模式下,传输完成中断会在缓冲区满时触发,但通常我们不依赖它,而是用空闲中断。若需处理半满情况,可启用DMA_IT_HT。
- **数据拷贝**:在空闲中断中处理数据时,应尽快拷贝到应用层缓冲区,避免占用中断过长时间。
## 7. 总结
通过DMA双缓冲结合空闲中断和定时器超时,我们实现了高效且可靠的不定长串口接收。该方法充分利用了硬件特性,减少了CPU干预,同时通过超时机制保证了帧边界的准确性。在实际项目中,可根据需求调整缓冲区大小和超时参数,以适应不同通信协议。希望本文能帮助你在嵌入式开发中少走弯路。