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

传统串口接收每字节触发中断,在高波特率下CPU频繁进出中断,效率低下。DMA可自动搬运数据,但DMA传输完成中断仅适用于固定长度。空闲中断(IDLE)在总线空闲时触发,恰好能判断一帧数据结束,结合DMA可实现高效的不定长接收。

  • DMA:减轻CPU负担,自动将串口数据搬运到内存。
  • 空闲中断:检测总线空闲,标志一帧数据接收完成。
  • 组合优势:无需预先知道数据长度,实时性好,CPU占用率低。

二、CubeMX配置步骤

  1. 使能串口:选择USART1,模式为Asynchronous。
  2. 配置DMA:在DMA Settings中添加USART1_RX,模式为Circular(循环模式),优先级中。
  3. 使能串口中断:在NVIC Settings中勾选USART1 global interrupt。
  4. 生成代码:配置时钟后生成工程。

注意:DMA模式选择Circular而非Normal,确保DMA始终就绪,避免频繁重启。

三、核心代码实现

3.1 变量定义

#define RX_BUFFER_SIZE 256
uint8_t rx_buffer[RX_BUFFER_SIZE];
volatile uint16_t rx_len = 0;
volatile uint8_t rx_flag = 0;

3.2 启动接收

在main()中初始化后调用:

HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

3.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);
    HAL_UART_DMAStop(&huart1);
    uint16_t temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
    rx_len = RX_BUFFER_SIZE - temp;
    rx_flag = 1;
    HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
  }
}

3.4 主循环处理

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

四、原理剖析

  • DMA循环模式:DMA持续搬运数据,当计数器减到0时自动重装,缓冲区循环使用。
  • 空闲中断触发:当串口总线空闲(无数据)时,硬件置位IDLE标志,触发中断。
  • 计算长度:__HAL_DMA_GET_COUNTER返回DMA剩余传输数量,用缓冲区大小减去剩余量即为已接收字节数。
  • 重启DMA:处理完数据后重新启动DMA接收,准备下一帧。

五、踩坑记录与注意事项

  • 坑1:DMA模式误选Normal
    Normal模式下DMA传输完成即停止,无法持续接收。必须选择Circular模式。

  • 坑2:未清除IDLE标志
    空闲中断标志必须手动清除,否则会反复进入中断。使用__HAL_UART_CLEAR_IDLEFLAG。

  • 坑3:DMA停止与重启的时机
    在中断中先停止DMA再读取计数器,防止数据竞争。处理完数据后立即重启DMA,避免数据丢失。

  • 坑4:缓冲区溢出
    若一帧数据超过缓冲区大小,DMA会覆盖旧数据。建议缓冲区足够大或增加超时机制。

  • 坑5:中断优先级冲突
    确保USART1中断优先级高于DMA中断,避免DMA中断打断空闲中断处理。

  • 坑6:HAL库版本差异
    不同HAL库版本中__HAL_UART_CLEAR_IDLEFLAG宏定义可能不同,请查阅对应头文件。

六、总结

DMA+空闲中断是STM32串口不定长接收的经典方案,兼顾效率与实时性。掌握CubeMX配置、中断处理与长度计算逻辑后,可轻松移植到其他系列。注意避开上述坑点,确保稳定运行。