3. DMA中断与回调:传输完成中断、半传输中断、错误中断、中断优先级配置、HAL回调函数机制

DMA 这东西,说白了就是个硬件搬运工。你告诉它从哪里搬、搬到哪里、搬多少,它自己就干完了。但问题来了——你怎么知道它搬完了?搬了一半要不要处理?搬错了怎么办?

这就得靠中断了。DMA 的中断系统,是它和 CPU 沟通的唯一语言。我刚开始用 STM32 做 DMA 传输时,就吃过没配中断的亏——数据都传完了,CPU 还在傻等轮询标志位,白白浪费性能。

3.1 DMA中断类型

STM32 的 DMA 控制器,每个数据流(或通道)都支持三种中断:

中断类型 触发条件 典型应用场景
传输完成中断 (TC) DMA 完成设定的传输数量 通知 CPU 数据已就绪,可以处理
半传输中断 (HT) DMA 完成一半传输数量 双缓冲处理、流水线操作
传输错误中断 (TE) 总线错误、FIFO 溢出/下溢 异常处理、数据重传

嗯,这里要注意——不是所有 STM32 系列都支持半传输中断。比如 STM32F1 的标准 DMA 就没有 HT 中断,只有 F4/H7 系列的数据流模式才支持。选型时一定要看参考手册。

3.2 中断优先级配置

中断优先级这事,我踩过坑。有一次做音频采集,DMA 传输完成中断和定时器中断抢优先级,结果音频数据丢帧,声音断断续续。查了两天才发现是优先级没配好。

STM32 的 NVIC 支持抢占优先级和子优先级。DMA 中断一般建议配成中等偏上优先级:

// 以 STM32F4 为例,配置 DMA 中断优先级
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);

我个人习惯:

  • 传输完成中断:优先级 2~3(别太高,留给更紧急的定时器)
  • 半传输中断:优先级 3~4(可以稍低)
  • 错误中断:优先级 0~1(错误必须尽快处理)
注意: 中断服务函数里不要做耗时操作。DMA 中断 ISR 应该只做标志位设置或数据指针切换,真正的数据处理放到主循环或任务里。我曾经在 TC 中断里直接做 memcpy,结果把系统搞崩了——中断嵌套太深,栈溢出了。

3.3 HAL回调函数机制

HAL 库把 DMA 中断处理封装成了回调函数。你不用直接写 ISR,只需要实现对应的回调就行。这设计其实挺巧妙的——把硬件中断和业务逻辑解耦。

常用的 DMA 回调函数:

回调函数 对应中断 说明
HAL_DMA_XferCpltCallback 传输完成 最常用,数据搬完了
HAL_DMA_XferHalfCpltCallback 半传输完成 双缓冲切换的好时机
HAL_DMA_XferErrorCallback 传输错误 必须实现,哪怕只是打印错误
HAL_DMA_XferAbortCallback 传输中止 调用 HAL_DMA_Abort 后触发

回调函数的注册机制很简单——HAL 库在初始化 DMA 时,会把句柄里的回调函数指针指向你的实现。你只需要在代码里定义这些函数就行:

// 传输完成回调
void HAL_DMA_XferCpltCallback(DMA_HandleTypeDef *hdma)
{
    // 置标志位,通知主循环处理
    dma_tc_flag = 1;
}

// 半传输回调
void HAL_DMA_XferHalfCpltCallback(DMA_HandleTypeDef *hdma)
{
    // 切换缓冲区指针
    current_buf = (current_buf == buf1) ? buf2 : buf1;
}

// 错误回调
void HAL_DMA_XferErrorCallback(DMA_HandleTypeDef *hdma)
{
    // 记录错误,重启 DMA
    Error_Handler();
}
小技巧: 回调函数里尽量只做轻量操作。我一般只置一个 volatile 标志位,主循环里轮询这个标志位再做处理。这样既保证了中断响应速度,又避免了在 ISR 里做复杂逻辑。

3.4 半传输中断的妙用

半传输中断是个好东西。你想想看——DMA 传完一半数据就通知你,这时候你可以处理前半段数据,同时 DMA 还在继续传后半段。这不就是流水线吗?

我在做音频播放时用过这个特性:

  • DMA 从内存搬运音频数据到 DAC
  • 半传输中断触发时,CPU 把下一段音频数据写入前半段缓冲区
  • 传输完成中断触发时,CPU 把再下一段数据写入后半段缓冲区

这样音频流就不会断。说白了,就是用时间换空间——用 CPU 的零碎时间,换取连续的数据流。

3.5 错误中断处理

错误中断很多人不重视,觉得不会出问题。我告诉你,会出的。尤其是 FIFO 模式下,如果源地址或目的地址没对齐,FIFO 就会溢出或下溢。

我曾经在调试一个 SPI 读取传感器数据的项目时,DMA 老是报传输错误。查了半天,发现是 SPI 的时钟频率配得太高,DMA 来不及响应。解决办法:降低 SPI 时钟,或者开启 DMA 的 FIFO 模式并调整阈值。

// 错误中断的标准处理流程
void HAL_DMA_XferErrorCallback(DMA_HandleTypeDef *hdma)
{
    // 1. 停止 DMA
    HAL_DMA_Abort(hdma);
    
    // 2. 清除错误标志
    __HAL_DMA_CLEAR_FLAG(hdma, DMA_FLAG_TEIF0_4);
    
    // 3. 重新配置并启动
    HAL_DMA_Start_IT(hdma, src_addr, dst_addr, len);
}
重要: 错误中断发生后,DMA 会自动停止传输。你必须手动重新启动。而且重启前一定要清除错误标志位,否则中断会一直触发,形成死循环。

3.6 知识结构图

下面这张图,把 DMA 中断和回调的整体关系梳理清楚了:

DMA中断与回调机制 DMA 控制器 传输完成中断 (TC) 半传输中断 (HT) 传输错误中断 (TE) NVIC 中断控制器 中断优先级配置 HAL 回调函数机制 DMA中断 → NVIC优先级排序 → HAL回调处理,形成完整的中断响应链路

3.7 实际项目中的配置示例

最后给一个完整的配置示例。这是我做 UART DMA 接收时的标准配置:

// DMA 句柄定义
DMA_HandleTypeDef hdma_usart1_rx;

// DMA 初始化
void MX_DMA_Init(void)
{
    __HAL_RCC_DMA2_CLK_ENABLE();
    
    hdma_usart1_rx.Instance = DMA2_Stream0;
    hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;  // 循环模式
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
    hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
    hdma_usart1_rx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
    
    HAL_DMA_Init(&hdma_usart1_rx);
    
    // 使能中断
    HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}

// 中断服务函数
void DMA2_Stream0_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&hdma_usart1_rx);
}

// 回调函数
void HAL_DMA_XferCpltCallback(DMA_HandleTypeDef *hdma)
{
    if (hdma->Instance == DMA2_Stream0)
    {
        // UART 数据接收完成
        uart_rx_complete = 1;
    }
}

void HAL_DMA_XferHalfCpltCallback(DMA_HandleTypeDef *hdma)
{
    if (hdma->Instance == DMA2_Stream0)
    {
        // 半缓冲切换
        uart_rx_half = 1;
    }
}

void HAL_DMA_XferErrorCallback(DMA_HandleTypeDef *hdma)
{
    if (hdma->Instance == DMA2_Stream0)
    {
        // 错误处理
        uart_rx_error = 1;
    }
}

关键点总结:

  • 传输完成中断:最常用,数据就绪通知
  • 半传输中断:双缓冲流水线,提升吞吐量
  • 错误中断:必须处理,否则系统可能挂死
  • 优先级配置:错误 > 传输完成 > 半传输
  • 回调函数:轻量操作,只置标志位

嗯,DMA 中断这块其实不难,但细节很多。配好了,系统跑得飞起;配不好,各种奇怪问题。我建议你每次配完中断,都先跑一个简单的测试——比如只开传输完成中断,确认能触发,再逐步加上半传输和错误中断。这样出了问题也好定位。