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(错误必须尽快处理)
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();
}
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);
}
3.6 知识结构图
下面这张图,把 DMA 中断和回调的整体关系梳理清楚了:
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 中断这块其实不难,但细节很多。配好了,系统跑得飞起;配不好,各种奇怪问题。我建议你每次配完中断,都先跑一个简单的测试——比如只开传输完成中断,确认能触发,再逐步加上半传输和错误中断。这样出了问题也好定位。