一、为什么需要DMA+空闲中断+缓冲区翻转?

在嵌入式开发中,串口接收不定长数据是常见需求。传统轮询或单字节中断方式占用CPU资源且效率低。DMA+空闲中断方案能实现零拷贝、低负载接收,而缓冲区翻转(双缓冲)进一步解决数据处理与接收的并发冲突,避免数据覆盖。

  • DMA:自动搬运数据,释放CPU。
  • 空闲中断:检测总线空闲,标志一帧数据结束。
  • 缓冲区翻转:双缓冲交替使用,实现“接收-处理”流水线。

二、原理详解

2.1 空闲中断触发机制

串口空闲中断(IDLE)在检测到总线空闲(一个字节时间无新数据)时触发。结合DMA传输完成中断,可精准判断帧结束。

2.2 缓冲区翻转(双缓冲)设计

定义两个缓冲区buf0和buf1,DMA当前写入buf0,当空闲中断发生时,切换DMA目标到buf1,同时通知应用处理buf0。处理完成后,再切回buf0,形成乒乓操作。

  • 优点:接收与处理并行,避免数据覆盖。
  • 关键:切换DMA目标需重新配置DMA通道(HAL库中需先停止DMA再启动)。

三、CubeMX配置步骤

  1. 使能USART1,模式为Asynchronous。
  2. 开启USART1全局中断(NVIC中使能USART1中断)。
  3. 添加DMA通道:USART1_RX,模式Circular(循环模式),数据宽度Byte。
  4. 生成代码。

四、代码实现

4.1 定义缓冲区与状态

#define BUF_SIZE 128
uint8_t buf0[BUF_SIZE], buf1[BUF_SIZE];
uint8_t *active_buf = buf0;   // DMA当前写入的缓冲区
uint8_t *ready_buf = NULL;    // 待处理的缓冲区
volatile uint8_t data_ready = 0;

4.2 启动DMA接收

void Start_Receive(void) {
    HAL_UART_Receive_DMA(&huart1, active_buf, BUF_SIZE);
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

4.3 空闲中断处理(在stm32f1xx_it.c中)

void USART1_IRQHandler(void) {
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        // 停止DMA以获取已接收长度
        HAL_UART_DMAStop(&huart1);
        uint16_t len = BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        if (len > 0) {
            ready_buf = active_buf;
            // 切换缓冲区
            active_buf = (active_buf == buf0) ? buf1 : buf0;
            data_ready = 1;
        }
        // 重新启动DMA接收
        HAL_UART_Receive_DMA(&huart1, active_buf, BUF_SIZE);
        __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
    }
    HAL_UART_IRQHandler(&huart1);
}

4.4 主循环处理数据

while (1) {
    if (data_ready) {
        // 处理ready_buf中的数据,长度需另行记录(见注意事项)
        ProcessData(ready_buf, data_len);
        data_ready = 0;
    }
}

五、关键踩坑与注意事项

  • 长度获取问题:上述代码中len在切换缓冲区后丢失,需用全局变量保存每次接收的长度。建议定义volatile uint16_t recv_len;在中断中赋值。
  • DMA停止与重启的时序:HAL_UART_DMAStop会关闭DMA和串口接收,必须重新调用HAL_UART_Receive_DMA启动,否则无法接收下一帧。
  • 中断优先级:确保USART中断优先级高于DMA中断(若使用DMA中断),避免竞争。
  • 缓冲区大小与溢出:若一帧数据超过BUF_SIZE,DMA循环模式会覆盖,需根据实际帧长调整或使用流控。
  • 内存对齐:DMA缓冲区建议使用__attribute__((aligned(4)))确保对齐,提升效率。
  • 空闲中断标志清除:必须使用__HAL_UART_CLEAR_IDLEFLAG清除,否则会反复进入中断。
  • HAL库版本差异:不同系列(F1/F4/H7)的DMA计数器获取方式略有不同,注意__HAL_DMA_GET_COUNTER的宏定义。

六、总结

DMA+空闲中断+缓冲区翻转是STM32串口接收不定长数据的高效方案。掌握其原理与实现细节,能显著提升系统实时性与可靠性。重点在于正确管理缓冲区切换、长度记录及DMA重启时序,避开上述陷阱即可稳定运行。