第4章 DMA中断与事件管理:传输完成中断、半传输中断、传输错误中断、中断优先级配置实战
DMA 中断,说白了就是告诉 CPU:「活干完了,你来收个尾吧」。很多初学者把 DMA 配置好就不管了,结果数据传完都不知道。我刚开始做项目时也犯过这毛病——数据丢了半天才发现,后来老老实实把中断全开了一遍。
这一章我们重点聊三个中断:传输完成中断、半传输中断、传输错误中断。再加上中断优先级的配置,基本覆盖了 DMA 事件管理的核心场景。
4.1 三种中断的触发时机与典型用途
先看一张表,把三种中断的触发条件和典型场景说清楚。
| 中断类型 | 触发时机 | 典型用途 |
|---|---|---|
| 传输完成中断(TC) | DMA 传输完设定的数据量(NDTR 减到 0) | 通知 CPU 数据已就绪,可以处理或进入休眠 |
| 半传输中断(HT) | DMA 传输完一半数据量(NDTR 减到初始值的一半) | 双缓冲切换、流水线处理、实时数据分帧 |
| 传输错误中断(TE) | 总线错误、FIFO 溢出/下溢、配置冲突 | 异常捕获、系统保护、调试定位 |
嗯,这里要注意:半传输中断只对固定长度的块传输有意义。如果你用的是循环模式(Circular Mode),半传输中断配合完成中断,可以实现完美的双缓冲乒乓操作。我在一个音频采集项目里就用过这个技巧——ADC 采完前半段数据,CPU 处理前半段,DMA 继续采后半段,效率直接翻倍。
4.2 中断使能与标志位操作
STM32 的 DMA 中断使能,是通过 DMA_CCR 寄存器里的三个位控制的:
- TCIE(Transfer Complete Interrupt Enable)
- HTIE(Half Transfer Interrupt Enable)
- TEIE(Transfer Error Interrupt Enable)
用 HAL 库的话,配置起来很简单。我习惯在初始化结构体里直接设置:
DMA_HandleTypeDef hdma_usart1_tx;
hdma_usart1_tx.Instance = DMA1_Channel4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_HIGH;
// 使能传输完成中断和传输错误中断
hdma_usart1_tx.Init.TCIE = ENABLE; // 传输完成中断
hdma_usart1_tx.Init.HTIE = DISABLE; // 半传输中断,这里先关掉
hdma_usart1_tx.Init.TEIE = ENABLE; // 传输错误中断
HAL_DMA_Init(&hdma_usart1_tx);
中断使能后,DMA 传输完成或出错时,硬件会自动置位对应的标志位。标志位在 DMA_ISR 寄存器里:
- TCIF(传输完成标志)
- HTIF(半传输标志)
- TEIF(传输错误标志)
中断服务函数里,记得手动清除标志位。HAL 库的弱函数已经帮我们做了这件事,但如果你自己写中断处理,千万别漏了这一步。我曾经见过一个同事,中断标志位没清,结果 CPU 一直在中断里打转,系统直接卡死。
4.3 中断服务函数编写实战
以 USART1 发送为例,我们写一个完整的中断处理流程。先注册中断回调:
// 传输完成回调
void HAL_DMA_TC_Callback(DMA_HandleTypeDef *hdma)
{
if (hdma->Instance == DMA1_Channel4)
{
// 数据发送完成,关闭 USART 发送使能,节省功耗
__HAL_UART_DISABLE(&huart1);
// 通知应用层:数据已发送完毕
uart_tx_complete_flag = 1;
}
}
// 传输错误回调
void HAL_DMA_Error_Callback(DMA_HandleTypeDef *hdma)
{
if (hdma->Instance == DMA1_Channel4)
{
// 记录错误信息
dma_error_code = HAL_DMA_GetError(hdma);
// 复位 DMA 通道
HAL_DMA_Abort(hdma);
// 重新初始化
HAL_DMA_Init(hdma);
// 打印或记录日志(调试阶段用)
printf("DMA Error: 0x%X\r\n", dma_error_code);
}
}
4.4 半传输中断与双缓冲应用
半传输中断最经典的应用就是双缓冲乒乓操作。原理很简单:
- DMA 开始往 Buffer A 写数据
- 写到一半时触发半传输中断,CPU 开始处理 Buffer A 的前半段
- DMA 继续写 Buffer A 的后半段
- 写完后触发传输完成中断,CPU 处理后半段
但更高效的做法是:用两个独立的 Buffer,半传输中断时切换目标地址。不过 STM32 的标准 DMA 不支持传输中动态改地址(除非你用双缓冲模式的 DMA,比如 STM32F4 的 DMA2 流)。
我个人的经验是:半传输中断更适合做数据分帧。比如一个 1024 字节的音频帧,半传输中断时正好是 512 字节,你可以在这时启动下一个 DMA 传输,或者更新状态机。这样流水线就建立起来了。
void HAL_DMA_HT_Callback(DMA_HandleTypeDef *hdma)
{
if (hdma->Instance == DMA2_Stream0)
{
// 半传输完成,处理前半段数据
process_audio_frame(audio_buffer, 0, 512);
// 更新状态
half_transfer_done = 1;
}
}
void HAL_DMA_TC_Callback(DMA_HandleTypeDef *hdma)
{
if (hdma->Instance == DMA2_Stream0)
{
// 传输完成,处理后半段数据
process_audio_frame(audio_buffer, 512, 512);
// 重新启动 DMA(循环模式不需要)
HAL_DMA_Start_IT(hdma, (uint32_t)&adc_value, (uint32_t)audio_buffer, 1024);
}
}
4.5 中断优先级配置——别让 DMA 中断堵车
中断优先级配置,是很多工程师容易忽略的地方。DMA 中断的优先级,直接影响系统的实时性。
STM32 的 NVIC 支持抢占优先级和子优先级。DMA 中断一般建议配置为中等偏上的优先级。为什么?
- 优先级太高:会打断关键任务(比如系统滴答定时器、高优先级外设中断)
- 优先级太低:DMA 传输完成通知被延迟,数据可能被覆盖
我一般这样配:
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 2, 0); // 抢占优先级2,子优先级0
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
这里有个坑:多个 DMA 通道共用同一个中断向量。比如 STM32F103 的 DMA1 通道 1~7 共用 DMA1_Channel1_IRQn 到 DMA1_Channel7_IRQn,但每个通道有自己的中断向量。而有些系列(如 STM32F4)的 DMA 流是独立中断向量。一定要查数据手册,别配错了。
4.6 中断事件管理的完整流程图
下面这张图,把 DMA 中断从触发到处理的完整流程画清楚了。你可以对照着理解。
4.7 中断使能与关闭的功耗考量
低功耗设计里,中断不是越多越好。每多一个中断,CPU 被唤醒的概率就大一分。我总结了几条原则:
- 非必要不使能:如果不需要半传输通知,就把 HTIE 关掉。少一个中断源,就少一次唤醒。
- 传输完成后及时关闭中断:在 TC 回调里,如果不再需要 DMA,可以调用
HAL_DMA_Abort()或直接关闭中断使能位。 - 错误中断必须开:TE 中断建议始终使能。DMA 出错时如果不处理,数据可能静默丢失,排查起来非常痛苦。
// 传输完成后关闭中断,降低功耗
void HAL_DMA_TC_Callback(DMA_HandleTypeDef *hdma)
{
// 关闭 DMA 中断
__HAL_DMA_DISABLE_IT(hdma, DMA_IT_TC | DMA_IT_TE);
// 关闭 DMA 通道
__HAL_DMA_DISABLE(hdma);
// 通知应用层
dma_done = 1;
}
4.8 常见问题与调试方法
调试 DMA 中断时,我遇到过几个典型问题,列出来供你参考:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 中断不触发 | 中断使能位未设置;NVIC 未使能;DMA 未启动 | 检查 CCR 寄存器;检查 NVIC_ISER;确认 DMA 已使能 |
| 中断频繁触发 | 标志位未清除;循环模式未正确处理 | 在 ISR 中清除标志位;循环模式下用回调处理 |
| 中断响应延迟大 | 优先级配置过低;其他高优先级中断阻塞 | 提高 DMA 中断优先级;检查中断嵌套情况 |
| 传输错误中断触发 | 总线冲突;FIFO 溢出;地址对齐问题 | 检查 DMA_ISR 寄存器;降低传输速率;检查地址对齐 |
调试时,我习惯用逻辑分析仪抓 DMA 的请求和应答信号。如果没有逻辑分析仪,可以在中断服务函数里翻转一个 GPIO,用示波器看波形。这个方法虽然土,但很管用。
好了,这一章的内容就到这里。DMA 中断管理是嵌入式开发里的基本功,但也是容易出细节问题的地方。多动手、多调试,慢慢就能找到感觉。