STM32F407串口空闲中断 + DMA 在FreeRTOS 下避坑指南
在嵌入式开发中,串口是最常用的通信接口之一,而非定长数据接收是串口开发的高频需求。传统的 “查询式接收” 或 “字节中断接收” 存在效率低、CPU 占用高的问题,而 “串口空闲中断 + DMA” 的组合能以极低的 CPU 开销实现非定长数据的高效接收。本文结合 STM32F4+FreeRTOS 的实战场景,详解该方案的实现逻辑,并分享开发中踩过的坑与解决方案,帮后人少走弯路。
一、核心原理:为什么选空闲中断 + DMA?
1. 核心痛点
非定长数据的关键问题是 “不知道数据何时结束”:
- 字节中断:每接收 1 个字节触发 1 次中断,频繁中断会拉高 CPU 占用;
- 查询方式:需轮询串口状态,浪费 CPU 资源,且实时性差。
2. 空闲中断 + DMA 的优势
- DMA(直接存储器访问):无需 CPU 介入,自动将串口接收寄存器(USART->DR)的数据搬运到内存缓冲区,解放 CPU;
- 串口空闲中断:当串口接收线在指定时间内无数据输入时触发,标志着一帧非定长数据接收完成;
- 两者结合:DMA 负责 “无脑搬运” 数据,空闲中断负责 “通知数据接收完成”,兼顾效率与实时性。
3. 核心流程
- 初始化 DMA 为循环模式,持续将串口 DR 的数据搬运到缓冲区;
- 开启串口空闲中断,当检测到空闲状态时触发中断;
- 中断中仅做 “标记数据接收完成 + 计算数据长度”,不处理数据;
- 在 FreeRTOS 任务中轮询标记,检测到数据就绪后关闭 DMA、处理数据,处理完成后重新配置 DMA 并开启。
二、代码实战:基于 STM32F4+FreeRTOS 的实现
结合本文提供的工程代码,分模块解析关键实现。
1. 核心宏定义与全局变量
// DMA.h 中定义串口DMA缓冲区大小
#define UART_BUFFER_SIZE 128
// 全局缓冲区:DMA搬运的目标地址
uint8_t UART_RX_BUFFER[UART_BUFFER_SIZE];
// bsp_uart.c 中定义状态变量
volatile uint8_t uart1_data_ready = 0; // 数据接收完成标记
volatile uint16_t uart1_rx_len = 0; // 接收到的数据长度
2. DMA 初始化:配置循环模式(关键!)
// DMA.c - UART_DMA_Init 函数
void UART_DMA_Init(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);
DMA_DeInit(DMA2_Stream2); // 复位DMA流,避免残留配置
DMA_InitStructure.DMA_Channel=DMA_Channel_4; // 绑定USART1的DMA通道
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&USART1->DR; // 外设地址:串口数据寄存器
DMA_InitStructure.DMA_Memory0BaseAddr=(uint32_t)UART_RX_BUFFER; // 内存缓冲区地址
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralToMemory; // 方向:外设→内存
DMA_InitStructure.DMA_BufferSize=UART_BUFFER_SIZE; // 缓冲区大小
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable; // 外设地址不递增(固定DR)
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_Medium;
DMA_InitStructure.DMA_FIFOMode=DMA_FIFOMode_Disable;
DMA_Init(DMA2_Stream2,&DMA_InitStructure);
DMA_Cmd(DMA2_Stream2, ENABLE); // 开启DMA
}
关键说明:DMA_Mode_Circular(循环模式)是核心 ——DMA 会持续循环搬运数据,直到手动关闭,解决了 “DMA 不知道串口数据何时结束” 的问题;若配置为DMA_Mode_Normal(正常模式),DMA 传输完预设长度后会停止,导致非定长数据仅接收一部分。
3. 串口初始化:开启空闲中断 + 绑定 DMA
// bsp_uart.c - uart1_init 函数
void uart1_init(uint32_t __Baud)
{
// (GPIO、USART基础配置略)
// 配置串口中断(空闲中断)
NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=5;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_ITConfig(USART1,USART_IT_IDLE,ENABLE); // 开启空闲中断
UART_DMA_Init(); // 初始化DMA
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE); // 开启串口DMA接收
USART_Cmd(USART1,ENABLE);
}
4. 空闲中断处理:仅标记、不处理(核心避坑点)
// bsp_uart.c - USART1_IRQHandler 中断服务函数
void USART1_IRQHandler(void)
{
if((USART1->SR&USART_SR_IDLE)!=RESET) // 检测空闲中断
{
// 必须读SR+DR清空闲中断标志(否则中断反复触发!)
uint8_t temp=USART1->SR;
temp=USART1->DR;
(void)temp; // 避免编译器警告
// 计算接收长度:总缓冲区大小 - DMA剩余未传输长度
uart1_rx_len=UART_BUFFER_SIZE-DMA_GetCurrDataCounter(DMA2_Stream2);
uart1_data_ready=1; // 标记数据接收完成
}
}
核心避坑:中断中仅做 “清标志 + 标记数据就绪”,绝对不能处理数据(比如解析、发送响应)—— 中断执行时间过长会导致系统卡死,甚至 FreeRTOS 任务调度异常。
5. 数据处理:在 FreeRTOS 任务中异步处理
// bsp_uart.c - uart1_process_data 函数
void uart1_process_data(void)
{
if(uart1_data_ready==1)
{
// 1. 关闭DMA,防止处理过程中数据被覆盖(核心!)
DMA_Cmd(DMA2_Stream2,DISABLE);
while(DMA_GetCmdStatus(DMA2_Stream2)!=DISABLE); // 等待DMA真正关闭
// 2. 处理数据(示例:回显接收到的数据)
uart1_send_str("RX data:");
uart1_send_num(uart1_rx_len);
uart1_send_str(":");
for(uint16_t i=0;i<uart1_rx_len;i++)
{
uart1_send_hex(UART_RX_BUFFER[i]);
}
uart1_send_str("\r\n");
// 3. 重置缓冲区与状态
memset(UART_RX_BUFFER,0,uart1_rx_len);
uart1_rx_len=0;
uart1_data_ready=0;
// 4. 重新配置DMA并开启(恢复循环接收)
DMA_SetCurrDataCounter(DMA2_Stream2,UART_BUFFER_SIZE);
DMA_Cmd(DMA2_Stream2,ENABLE);
}
}
// freertos_task.c - ui_task 任务中调用处理函数
void ui_task(void *pvParameters)
{
while(1)
{
uart1_process_data(); // 轮询处理串口数据
vTaskDelay(pdMS_TO_TICKS(500));
}
}
关键说明:
- 处理数据前必须关闭 DMA:若不关闭,后续新接收的数据会覆盖缓冲区中未处理的旧数据;
- 处理完成后需重新配置 DMA 的传输长度:循环模式下,DMA 的剩余长度不会自动重置,需手动恢复为
UART_BUFFER_SIZE。
6. FreeRTOS 下 printf 重定向:互斥量防止乱码
在 FreeRTOS 中,多个任务可能同时调用printf,若直接重定向串口发送,会因任务抢占导致数据乱码。解决方案是用互斥量保护fputc函数:
// bsp_uart.c - printf重定向与互斥量保护
SemaphoreHandle_t printf_mutex;
// 初始化互斥量(在FreeRTOS启动任务中调用)
void printf_Init(void)
{
printf_mutex=xSemaphoreCreateMutex();
}
// 重定向fputc:串口1发送
int fputc(int ch, FILE *f)
{
// 获取互斥量,确保同一时间只有一个任务操作串口
xSemaphoreTake(printf_mutex,portMAX_DELAY);
while((USART1->SR&USART_SR_TXE)==0); // 等待发送寄存器空
USART1->DR=(uint8_t)ch; // 发送字节
xSemaphoreGive(printf_mutex); // 释放互斥量
return ch;
}
调用时机:在 FreeRTOS 启动任务中初始化互斥量:
// freertos_task.c - start_task 函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL();
printf_Init(); // 初始化printf互斥量
// (创建其他任务略)
taskEXIT_CRITICAL();
}
三、踩坑实录:
坑 1:FreeRTOS 下 printf 输出乱码
- 现象:多个任务同时调用
printf,输出内容错乱、丢字节; - 原因:任务抢占导致串口发送寄存器被交替写入;
- 解决方案:用 FreeRTOS 互斥量(
xSemaphoreCreateMutex)保护fputc函数,确保同一时间只有一个任务操作串口发送。
坑 2:空闲中断反复触发,系统卡死
- 现象:触发一次空闲中断后,中断不断被触发,CPU 占用 100%;
- 原因:未正确清除空闲中断标志;
- 解决方案:中断中必须依次读取
USART->SR和USART->DR,这是 STM32 清除空闲中断的唯一方式(代码中temp=USART1->SR; temp=USART1->DR;)。
坑 3:中断中处理数据导致系统卡死
- 现象:在
USART1_IRQHandler中解析数据、调用printf,系统偶尔卡死; - 原因:中断服务函数执行时间过长,阻塞了 FreeRTOS 任务调度或其他关键中断;
- 解决方案:中断中仅做 “最小化操作”(清标志、标记数据就绪),所有数据处理逻辑放到 FreeRTOS 任务中异步执行。
坑 4:DMA 配置为正常模式,数据接收不完整
- 现象:非定长数据仅接收前 N 个字节(N 为 DMA 预设长度),后续数据丢失;
- 原因:
DMA_Mode_Normal模式下,DMA 传输完预设长度后会停止,无法继续搬运后续数据; - 解决方案:必须配置为
DMA_Mode_Circular(循环模式),让 DMA 持续循环搬运数据。
坑 5:处理数据时未关闭 DMA,数据被覆盖
- 现象:解析的数据与实际发送的数据不一致,出现乱码;
- 原因:处理数据过程中,新的串口数据通过 DMA 持续写入缓冲区,覆盖了未处理的旧数据;
- 解决方案:处理数据前关闭 DMA,处理完成后重新配置并开启 DMA。
四、总结
“串口空闲中断 + DMA” 是 STM32 实现非定长数据接收的最优方案之一,结合 FreeRTOS 时需重点关注中断轻量化和任务间资源竞争问题。核心避坑要点:
- 空闲中断仅做标记,数据处理放任务;
- DMA 必须配置循环模式,处理数据前关闭 DMA;
- FreeRTOS 下 printf 重定向需用互斥量保护;
- 空闲中断标志必须通过读 SR+DR 清除。
这套方案兼顾了效率、实时性和稳定性,已在 STM32F4+FreeRTOS 的实际项目中验证,希望能帮到各位开发者少走弯路。
更多推荐


所有评论(0)