在嵌入式开发中,串口是最常用的通信接口之一,而非定长数据接收是串口开发的高频需求。传统的 “查询式接收” 或 “字节中断接收” 存在效率低、CPU 占用高的问题,而 “串口空闲中断 + DMA” 的组合能以极低的 CPU 开销实现非定长数据的高效接收。本文结合 STM32F4+FreeRTOS 的实战场景,详解该方案的实现逻辑,并分享开发中踩过的坑与解决方案,帮后人少走弯路。

一、核心原理:为什么选空闲中断 + DMA?

1. 核心痛点

非定长数据的关键问题是 “不知道数据何时结束”:

  • 字节中断:每接收 1 个字节触发 1 次中断,频繁中断会拉高 CPU 占用;
  • 查询方式:需轮询串口状态,浪费 CPU 资源,且实时性差。

2. 空闲中断 + DMA 的优势

  • DMA(直接存储器访问):无需 CPU 介入,自动将串口接收寄存器(USART->DR)的数据搬运到内存缓冲区,解放 CPU;
  • 串口空闲中断:当串口接收线在指定时间内无数据输入时触发,标志着一帧非定长数据接收完成;
  • 两者结合:DMA 负责 “无脑搬运” 数据,空闲中断负责 “通知数据接收完成”,兼顾效率与实时性。

3. 核心流程

  1. 初始化 DMA 为循环模式,持续将串口 DR 的数据搬运到缓冲区;
  2. 开启串口空闲中断,当检测到空闲状态时触发中断;
  3. 中断中仅做 “标记数据接收完成 + 计算数据长度”,不处理数据;
  4. 在 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 时需重点关注中断轻量化和任务间资源竞争问题。核心避坑要点:

  1. 空闲中断仅做标记,数据处理放任务;
  2. DMA 必须配置循环模式,处理数据前关闭 DMA;
  3. FreeRTOS 下 printf 重定向需用互斥量保护;
  4. 空闲中断标志必须通过读 SR+DR 清除。

这套方案兼顾了效率、实时性和稳定性,已在 STM32F4+FreeRTOS 的实际项目中验证,希望能帮到各位开发者少走弯路。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐