一、为什么需要DMA+空闲中断?

在嵌入式系统中,串口接收不定长数据帧是高频需求。传统逐字节中断方式每接收一个字节就触发一次中断,高波特率下CPU频繁被打断,容易丢数据。DMA+空闲中断方案利用DMA自动搬运数据,仅在总线空闲时触发一次中断,极大降低CPU负载,同时保证数据完整性。

  • DMA:无需CPU干预,自动将串口数据寄存器内容搬运到内存缓冲区。
  • 空闲中断:当串口总线检测到空闲(即一个字节时间无新数据)时触发,标志一帧数据接收完成。

二、硬件与软件环境

  • MCU:STM32F103C8T6(其他STM32系列类似)
  • 开发环境:STM32CubeMX + HAL库 + Keil MDK
  • 串口:USART1(PA9 TX,PA10 RX)
  • DMA通道:DMA1 Channel5(USART1_RX)

三、CubeMX配置步骤

  1. 时钟配置:根据实际晶振配置系统时钟(如72MHz)。
  2. USART1配置:
    • 模式:Asynchronous
    • 波特率:115200
    • 字长:8位
    • 校验:无
    • 停止位:1
    • 开启USART1全局中断(NVIC中使能USART1中断)
  3. DMA配置:
    • 添加USART1_RX的DMA请求
    • 模式:Circular(循环模式)或Normal(普通模式),推荐Circular避免频繁重启
    • 数据宽度:Byte(字节)
    • 优先级:中或高
  4. 生成代码:生成初始化代码,HAL库会自动配置DMA和串口。

四、核心代码实现

4.1 定义缓冲区与变量

#define RX_BUFFER_SIZE 256
uint8_t rx_buffer[RX_BUFFER_SIZE];  // DMA接收缓冲区
volatile uint16_t rx_len = 0;       // 本次接收数据长度
volatile uint8_t rx_flag = 0;       // 接收完成标志

4.2 启动DMA接收与空闲中断

在main.c的初始化部分调用:

// 启动DMA接收,缓冲区大小设为RX_BUFFER_SIZE
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
// 使能空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

4.3 空闲中断处理函数

在stm32f1xx_it.c中修改USART1_IRQHandler:

void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&huart1);
    // 检查空闲中断标志
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
    {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);  // 清除空闲标志
        // 停止DMA以获取当前接收长度
        HAL_UART_DMAStop(&huart1);
        // 计算接收长度:总大小 - DMA剩余计数
        rx_len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        rx_flag = 1;  // 置位接收完成标志
        // 重新启动DMA接收(循环模式可省略此步)
        HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
    }
}

4.4 主循环处理数据

while (1)
{
    if (rx_flag)
    {
        // 处理接收到的数据,rx_buffer中前rx_len字节有效
        process_data(rx_buffer, rx_len);
        rx_flag = 0;
    }
}

五、踩坑记录与注意事项

5.1 DMA模式选择

  • Circular模式:DMA自动循环填充缓冲区,无需每次重启,但需注意缓冲区覆盖问题。若数据处理不及时,新数据可能覆盖旧数据。
  • Normal模式:每次接收完成后需手动重启DMA,否则无法接收下一帧。推荐在空闲中断中重启。

5.2 空闲中断标志清除

必须使用__HAL_UART_CLEAR_IDLEFLAG清除标志,否则会反复进入中断。注意:HAL库没有提供直接的空闲中断回调,需在IRQHandler中手动处理。

5.3 数据长度计算

使用__HAL_DMA_GET_COUNTER获取DMA剩余传输数量,接收长度 = 缓冲区总大小 - 剩余数量。注意:在Circular模式下,若数据超过缓冲区大小,长度计算会出错,需结合环形缓冲区管理。

5.4 中断优先级与实时性

  • 空闲中断优先级不宜过低,避免数据被后续帧覆盖。
  • 若使用RTOS,建议在中断中发送信号量,由任务处理数据,避免长时间占用中断。

5.5 缓冲区对齐与大小

  • DMA缓冲区建议使用__attribute__((aligned(4)))对齐,提高访问效率。
  • 缓冲区大小应为2的幂次,便于环形缓冲区索引计算。

5.6 错误处理

  • 使能UART错误中断(如溢出、噪声),在错误回调中重启DMA接收。
  • 若DMA传输完成中断也开启,需与空闲中断协调,避免重复处理。

六、总结

DMA+空闲中断方案能高效可靠地接收不定长串口数据,关键在于正确配置DMA模式、及时清除空闲标志、准确计算数据长度。实际项目中还需考虑数据覆盖、错误恢复等细节。掌握此方案后,可轻松应对大多数串口通信场景,显著提升系统实时性。