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

串口接收数据时,我们往往不知道一帧数据有多长。常见做法有:

  • 固定长度接收:简单但灵活性差,无法应对变长帧。
  • 逐字节中断:每收一个字节就进一次中断,高频数据下CPU占用高,且容易丢失数据。

空闲中断(IDLE) 是STM32 USART的一个硬件事件:当接收完一个字节后,总线保持空闲(电平持续为高)超过一个字节时间,就会触发中断。利用它,可以精准判断一帧数据结束。

DMA(直接内存访问) 负责把串口接收数据自动搬运到内存缓冲区,不占用CPU。两者结合,可实现:DMA持续接收,空闲中断标记帧结束,CPU只在帧完整时处理一次数据,效率极高。

二、工作原理

  1. 开启USART的DMA接收请求,设置DMA为循环模式(Circular Mode)。
  2. 数据到达时,DMA自动将字节存入缓冲区,并更新DMA计数器。
  3. 当一帧数据结束后,串口检测到线空闲,触发IDLE中断。
  4. 在IDLE中断中,读取DMA当前剩余计数(NDTR),即可算出本次接收到的数据长度:len = buffer_size - NDTR。
  5. 主程序或回调函数中处理这帧数据,然后DMA继续循环接收下一帧。

关键点:DMA是循环模式,意味着它可以不间断地写入缓冲区,不会在接收完一帧后停止;IDLE中断只是告知“有完整帧可处理”。

三、CubeMX 配置步骤

假设使用STM32F103系列,USART1,DMA通道。

1. 配置串口参数

  • 打开USART1,模式选择Asynchronous,波特率115200,8位数据,无校验,1停止位。

2. 配置DMA

  • 在DMA Settings中添加USART1_RX,方向PeripheralToMemory,模式Circular,数据宽度均为Byte,优先级High。
  • 注意:不需要配置TX DMA(若需要发送也可加)。

3. 开启中断

  • 在NVIC Settings中使能USART1 global interrupt(这同时会覆盖IDLE中断和错误中断)。
  • 不使能DMA中断(循环模式无需DMA传输完成中断)。

4. 生成代码

  • 生成初始化代码,后续只需在main.c中补充逻辑。

四、完整代码示例

1. 定义缓冲区与接收标志

#define RX_BUF_SIZE  256
uint8_t rx_buffer[RX_BUF_SIZE];
volatile uint8_t rx_frame_complete = 0;  // 帧完成标志
volatile uint16_t rx_frame_len = 0;      // 帧长度

2. 启动DMA接收(放在main初始化后)

// 启动DMA循环接收,数据会持续填充rx_buffer
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUF_SIZE);

// 开启USART空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

3. 空闲中断处理(在中断回调里)

void USART1_IRQHandler(void)
{
    uint32_t isr = READ_REG(huart1.Instance->SR);
    // 检查空闲中断标志
    if ((isr & UART_FLAG_IDLE) != 0)
    {
        // 清除IDLE标志(先读SR再读DR)
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);

        // 停止DMA传输,读取当前剩余计数
        HAL_UART_DMAStop(&huart1);
        uint16_t remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        // 计算已接收字节数
        rx_frame_len = RX_BUF_SIZE - remain;

        // 标记帧完成
        rx_frame_complete = 1;

        // 重新启动DMA接收,准备下一帧
        HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUF_SIZE);
    }
    HAL_UART_IRQHandler(&huart1);
}

注意:上面的代码中,HAL_UART_DMAStop会停止DMA,但也会关闭接收。重新调用HAL_UART_Receive_DMA可以再次开启。简单场景下也可不调用DMAStop,直接读取计数器再重启,但为了严谨,建议按上述方式。

4. 主循环处理帧数据

while (1)
{
    if (rx_frame_complete)
    {
        rx_frame_complete = 0;
        // 现在rx_buffer中有效数据为前rx_frame_len个字节
        // 例如打印十六进制数据
        for (uint16_t i = 0; i < rx_frame_len; i++)
        {
            printf("%02X ", rx_buffer[i]);
        }
        printf("\r\n");
        // 业务处理...
    }
}

这段代码的精髓在于:DMA循环写入缓冲区,帧边界由IDLE事件标定,CPU仅在帧结束时参与一次计算和业务处理。

五、注意事项与常见坑

  • 缓冲区大小:必须大于最大可能帧长度,否则数据会覆盖。若帧可能超过缓冲区,可加大缓冲区或分段处理。
  • 读取计数的时序:在IDLE中断中,务必先清标志再读DMA计数器,避免再次触发中断导致计数错误。
  • DMAStop的副作用:调用HAL_UART_DMAStop会同时清空DMA配置,因此必须重新调用HAL_UART_Receive_DMA。注意检查返回值,确保启动成功。
  • 多字节接收竞态:如果主循环读取缓冲区时,新数据又开始写入,可能导致数据交错。建议在读取前先关闭DMA(或使用双缓冲),本项目为演示简单场景,主循环处理及时则问题不大。
  • IDLE中断标志清除:在STM32F1系列上,清除IDLE标志是通过“先读SR,再读DR”实现的。HAL库提供了__HAL_UART_CLEAR_IDLEFLAG宏,使用它更安全。
  • 其他串口事件:防止溢出错误(ORE)中断干扰,可在开启IDLE前使能UART_IT_ERR并用HAL_UART_ErrorCallback处理,或直接置位huart1.RxState恢复接收。

六、扩展:双缓冲与多帧处理

若需要连续处理大量数据,可使用两个缓冲区交替使用(乒乓缓冲)。当IDLE中断时,切换DMA目标到另一缓冲区,同时将当前缓冲区数据交付处理,这样接收和处理完全并行,避免丢帧。实现方式为:在IDLE中断里,用HAL_UART_DMAStop + 修改huart1.hdmarx->Instance->CMAR指向另一缓冲区,再启动。有兴趣的读者可自行实现。

结语

空闲中断+DMA方案是STM32串口接收不定长数据的经典高效解法。理解其原理后,你可以轻松应用于各种串口协议解析。记住核心要点:DMA负责搬数据,IDLE负责划帧界。配合CubeMX快速配置,代码清晰可维护,非常适合工程落地。