基于 STM32 的 DMA 半传输中断在环形缓冲区中的高效应用与踩坑记录

一、为什么需要半传输中断?

在嵌入式系统中,外设(如 ADC、UART)持续产生数据时,传统做法是每次触发中断读取单个数据,但高频中断会消耗大量 CPU 资源。DMA 可以批量搬运数据,但若等到传输完成才处理,缓冲区后半段的数据可能被覆盖。半传输中断(Half Transfer Interrupt)在 DMA 传输完成一半时触发,允许 CPU 在处理前半段数据的同时,DMA 继续填充后半段,形成流水线作业。配合环形缓冲区,可实现无阻塞、零拷贝的数据流处理,特别适合音频采样、传感器数据采集等场景。

二、原理剖析:DMA 半传输中断与环形缓冲区

DMA 传输分为正常模式(Normal)和循环模式(Circular)。在循环模式下,DMA 持续运行,当传输计数减到一半时,硬件置位半传输标志(HT),传输完成时置位完成标志(TC)。利用这两个标志,可将缓冲区视为两个半区:

  • 前半区([0, N/2)):CPU 处理数据时,DMA 正在写后半区。
  • 后半区([N/2, N)):CPU 处理后半区时,DMA 已回到前半区写入新数据。

环形缓冲区(Ring Buffer)则通过读写指针管理数据,避免频繁搬移。结合半传输中断,CPU 只需在 HT 和 TC 中断中更新读指针,并处理对应半区的数据,无需关心 DMA 内部状态。

三、STM32 配置步骤(以 HAL 库为例)

1. 硬件环境

  • MCU:STM32F407(或其他系列)
  • 外设:ADC1 连续采样,DMA2 Stream0 通道0
  • 缓冲区:uint16_t buffer[128](256字节)

2. CubeMX 配置

  • 启用 ADC1,开启连续转换模式,DMA 请求使能。
  • 配置 DMA2 Stream0:模式为 Circular,外设地址增量关闭,内存地址增量开启,数据宽度半字(16位)。
  • 使能 DMA 中断(HT 和 TC)。

3. 代码实现

/* 缓冲区定义 */
#define BUFFER_SIZE 128
uint16_t dma_buffer[BUFFER_SIZE];
volatile uint8_t half_flag = 0;
volatile uint8_t complete_flag = 0;

/* 启动 ADC DMA */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)dma_buffer, BUFFER_SIZE);

/* DMA 中断回调 */
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {
    half_flag = 1;
    // 处理前半区数据(索引 0 ~ BUFFER_SIZE/2-1)
    process_data(&dma_buffer[0], BUFFER_SIZE/2);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    complete_flag = 1;
    // 处理后半区数据(索引 BUFFER_SIZE/2 ~ BUFFER_SIZE-1)
    process_data(&dma_buffer[BUFFER_SIZE/2], BUFFER_SIZE/2);
}

void process_data(uint16_t* data, uint16_t len) {
    // 例如计算平均值、存储等
}

注意:HAL 库中,ADC 的 DMA 中断回调函数名是 HAL_ADC_ConvHalfCpltCallbackHAL_ADC_ConvCpltCallback,需在 stm32f4xx_hal_adc.c 中使能相关宏。

四、踩坑记录与解决方案

坑1:中断标志误清除导致数据丢失

HAL 库在中断处理中会自动清除标志位,但若在回调中执行耗时操作,可能导致 DMA 继续运行并覆盖未处理的数据。解决:回调中仅设置标志或快速拷贝数据,耗时处理放到主循环。

坑2:缓冲区大小不是 2 的幂次方

环形缓冲区通常用位与运算求余,若大小不是 2 的幂,需用取模运算,效率降低。建议将缓冲区大小定义为 2 的幂,例如 128、256。

坑3:半传输中断与完成中断的竞争

当 DMA 循环模式时,HT 和 TC 交替触发。若 CPU 处理速度慢,可能错过中断。解决:在回调中禁用中断(__disable_irq()),处理完再使能,但需注意嵌套优先级。

坑4:DMA 与 CPU 的数据一致性

Cortex-M 内核有缓存(如 F4 无缓存,但 F7/H7 有),需使用 __DMB()SCB_InvalidateDCache() 确保数据可见。对于无缓存 MCU,直接访问即可。

坑5:初始化时缓冲区未清零

DMA 启动后立即开始写入,若缓冲区有随机值,首次处理可能出错。初始化时用 memset 清零。

五、性能优化与扩展

  • 使用双缓冲(Double Buffer)模式:DMA 支持内存到内存的双缓冲,可避免半传输中断,但占用更多内存。
  • 结合空闲中断(IDLE)处理不定长数据:UART 场景下,半传输中断处理固定半区,空闲中断处理剩余数据。
  • 使用 __attribute__((aligned(4))) 对齐缓冲区,提高 DMA 访问效率。

六、总结

DMA 半传输中断是嵌入式数据流处理的核心技巧,正确使用可大幅降低 CPU 负载。但需注意中断回调的耗时、缓冲区边界、数据一致性等细节。本文提供的配置和代码可直接用于 ADC 采样,也可扩展至 UART、SPI 等外设。希望这些踩坑经验能帮助你在项目中游刃有余。