基于 STM32 的 DMA 半传输中断在环形缓冲区中的高效应用技巧

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

在 ADC 连续采样、UART 接收长帧或 SPI 从机接收数据时,DMA 能大幅减轻 CPU 负担。但若仅使用 DMA 传输完成中断,则必须等待整个缓冲区填满才能处理数据,导致延迟高;若使用双缓冲(如 STM32F4 的 DMA 双缓冲模式),则需额外配置内存地址切换,且部分低端型号不支持。

半传输中断(Half Transfer Interrupt) 在 DMA 传输达到缓冲区一半时触发,配合环形缓冲区,可将缓冲区视为两个独立的“半区”,实现类似双缓冲的效果:CPU 处理前半区数据时,DMA 正在填充后半区,反之亦然。这种“乒乓”操作极大降低了数据丢失风险,且无需硬件双缓冲支持。

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

1. 环形缓冲区设计

环形缓冲区(Ring Buffer)通常包含:

  • 数据存储数组 buf[]
  • 读指针 read_index
  • 写指针 write_index(由 DMA 硬件更新,软件只读)
  • 容量 size(必须为 2 的幂,便于取模)

2. 半传输中断的触发时机

以 STM32F103 为例,DMA 通道的 CNDTR 寄存器记录剩余传输次数。当 CNDTRsize/2 变为 size/2 - 1 时,硬件置位 HTIF 标志,若使能中断则进入 ISR。此时 DMA 已填充了缓冲区的前半部分,而后半部分仍在填充中。

3. 工作流程

  • 初始化:DMA 循环模式(Circular Mode),缓冲区大小 N(偶数)。
  • 第一次半传输中断:处理 buf[0]buf[N/2-1] 的数据。
  • 第一次全传输中断:处理 buf[N/2]buf[N-1] 的数据。
  • 之后重复:半传输中断处理前半区,全传输中断处理后半区。

注意:在循环模式下,全传输中断后 DMA 自动重载 CNDTRN,继续下一轮传输,因此中断会持续交替触发。

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

1. 硬件环境

  • MCU: STM32F103C8T6(或任意系列)
  • 外设:ADC1 连续采样,或 USART1 接收
  • 开发工具:STM32CubeIDE + HAL 库

2. 配置 DMA 与环形缓冲区

// 环形缓冲区定义
#define BUFFER_SIZE 256  // 必须为偶数
uint8_t rx_buffer[BUFFER_SIZE];
volatile uint16_t read_index = 0;

// 使能 DMA 半传输中断和全传输中断
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 处理后半区数据(索引 N/2 到 N-1)
    process_data(&rx_buffer[BUFFER_SIZE/2], BUFFER_SIZE/2);
}

void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    // 处理前半区数据(索引 0 到 N/2-1)
    process_data(&rx_buffer[0], BUFFER_SIZE/2);
}

// 启动 DMA 接收(循环模式)
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);

3. 处理函数示例

void process_data(uint8_t *data, uint16_t len)
{
    // 将数据拷贝到应用缓冲区或直接解析
    // 注意:此时 DMA 正在写另一块区域,不会冲突
    for (uint16_t i = 0; i < len; i++) {
        // 处理 data[i]
    }
}

4. 若使用中断方式读取数据(非回调)

// 在中断服务函数中手动判断标志
void DMA1_Channel5_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA1_IT_HT5)) {
        DMA_ClearITPendingBit(DMA1_IT_HT5);
        // 处理前半区
    }
    if (DMA_GetITStatus(DMA1_IT_TC5)) {
        DMA_ClearITPendingBit(DMA1_IT_TC5);
        // 处理后半区
    }
}

四、完整代码示例(基于 HAL 库,UART 接收)

// main.c 片段
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;

#define RX_BUF_SIZE 256
uint8_t rx_buf[RX_BUF_SIZE];

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 处理后半区
        process_data(&rx_buf[RX_BUF_SIZE/2], RX_BUF_SIZE/2);
    }
}

void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 处理前半区
        process_data(&rx_buf[0], RX_BUF_SIZE/2);
    }
}

void process_data(uint8_t *data, uint16_t len)
{
    // 示例:将数据通过另一个 UART 发送出去
    HAL_UART_Transmit(&huart2, data, len, 100);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();

    // 启动 DMA 接收,循环模式
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);

    while (1)
    {
        // 主循环可处理其他任务
    }
}

五、注意事项与优化技巧

  • 缓冲区大小必须为偶数,否则半传输中断的位置会偏移。
  • 确保 DMA 配置为循环模式(Circular),否则传输完成后停止,无法连续工作。
  • 中断优先级设置:半传输和全传输中断应设为较高优先级,避免数据覆盖。
  • 处理数据时避免访问正在被 DMA 写入的区域:在回调中,只处理对应半区,不要修改缓冲区内容,除非你确定 DMA 已离开该区域。
  • 对于 ADC 采样:若使用扫描模式,半传输中断可用来处理前一半采样值,适合实时性要求高的控制。
  • 内存对齐:建议将缓冲区按 4 字节对齐,提高访问效率。
  • 若使用 STM32F4/H7 等带双缓冲 DMA 的型号,可考虑硬件双缓冲,但半传输中断依然适用于简单场景。

六、总结

DMA 半传输中断是嵌入式开发中一个实用但常被忽视的技巧。通过合理利用它,开发者可以在不增加硬件成本的情况下,实现高效的数据流处理,降低 CPU 占用并减少数据丢失风险。本文提供的原理与代码可直接应用于 UART、SPI、ADC 等场景,希望对你有所帮助。