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

在嵌入式应用中,串口接收不定长数据帧非常普遍。常见方案有:

  • 逐字节接收中断:每收到一个字节进一次中断,CPU开销大,高波特率下容易丢数据。
  • DMA+固定长度:适合定长帧,但无法处理不定长帧。
  • DMA+空闲中断:DMA自动搬运数据,总线空闲时触发一次中断,CPU仅在帧结束时处理,效率最高。

STM32的USART外设支持IDLE(空闲)中断:当接收线上连续出现一个字节时间的高电平(即总线空闲)时,硬件置位IDLE标志。结合DMA,可以在DMA搬运数据的同时,由IDLE中断通知“一帧结束”。

二、实现原理

  1. 初始化时启动DMA接收,将USART数据寄存器(DR)搬运到用户缓冲区,长度设为最大帧长。
  2. 每接收一个字节,DMA自动搬运,无需CPU干预。
  3. 总线空闲时,USART产生IDLE中断。
  4. 在IDLE中断中:
    • 读取已接收字节数(通过DMA剩余计数计算)。
    • 处理数据。
    • 重新启动DMA接收,准备下一帧。

关键点:HAL库的HAL_UART_Receive_DMA()在正常模式下,DMA传输完成会关闭接收,而IDLE中断发生时DMA可能还未完成。因此需要手动停止DMA、计算长度、重启接收。

三、CubeMX配置步骤

  1. 使能USART1,模式设为Asynchronous,波特率115200。
  2. 在NVIC Settings中使能USART1全局中断。
  3. 在DMA Settings中添加USART1_RX的DMA通道,模式为Normal(非循环),数据宽度Byte。
  4. 生成代码。

四、完整代码示例

4.1 定义缓冲区与标志

#define RX_BUF_SIZE 256

uint8_t rx_buf[RX_BUF_SIZE];
uint8_t rx_frame[RX_BUF_SIZE];
volatile uint16_t rx_len = 0;
volatile uint8_t rx_flag = 0;

4.2 启动接收与IDLE中断使能

void UART_StartReceive(void)
{
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

4.3 在USART1_IRQHandler中处理IDLE

void USART1_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
    {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);

        HAL_UART_DMAStop(&huart1);

        uint16_t remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        rx_len = RX_BUF_SIZE - remain;

        if (rx_len > 0 && rx_len <= RX_BUF_SIZE)
        {
            memcpy(rx_frame, rx_buf, rx_len);
            rx_flag = 1;
        }

        HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
    }

    HAL_UART_IRQHandler(&huart1);
}

4.4 主循环处理

while (1)
{
    if (rx_flag)
    {
        rx_flag = 0;
        // 处理rx_frame,长度为rx_len
        ProcessFrame(rx_frame, rx_len);
    }
}

五、踩坑记录与注意事项

  • 坑1:IDLE标志清除方式错误。必须使用__HAL_UART_CLEAR_IDLEFLAG(),该宏通过读SR再读DR来清除标志。直接写0无效。
  • 坑2:DMA未停止就读取计数器。在IDLE中断中应先HAL_UART_DMAStop(),再读取__HAL_DMA_GET_COUNTER(),否则计数器可能仍在变化。
  • 坑3:忘记重启DMA接收。处理完一帧后必须重新调用HAL_UART_Receive_DMA(),否则后续数据无法接收。
  • 坑4:HAL_UART_IRQHandler的调用顺序。如果先调用HAL_UART_IRQHandler(),它可能清除IDLE标志或产生错误处理,建议先处理IDLE再调用HAL库中断处理函数。
  • 坑5:DMA传输完成中断与IDLE冲突。当一帧长度恰好等于缓冲区大小时,DMA完成中断会触发,而IDLE可能不再触发。建议在DMA完成回调中也做处理,或使用循环模式+IDLE。
  • 坑6:缓冲区覆盖。处理数据时若耗时较长,新数据可能覆盖缓冲区。建议使用双缓冲或及时拷贝。
  • 注意:高波特率下建议增大缓冲区,并确保中断优先级合理,避免DMA被其他中断打断导致数据丢失。

六、总结

DMA+空闲中断是STM32串口不定长接收的高效方案,核心在于正确配置DMA、使能IDLE中断,并在中断中准确计算帧长度、及时重启接收。掌握上述配置与避坑要点,可显著提升串口通信的稳定性和CPU利用率。