STM32 DMA 循环传输踩坑:断点调试卡死在DMA中断函数
最近调试 STM32 ADC+DMA 循环采集的时候遇到了一个很玄学的问题:明明开启了 DMA 的半传输和全传输中断,理论上DMA中断函数里的Adc_IntCnt应该严格按照1→2→1→2交替出现,结果打断点调试的时候,发现两个中断会同时进入并不断循环。问题场景STM32F103,ADC 连续转换模式,DMA 循环传输DMA 缓冲区 128 点,开启半传输(前 64 点)和全传输(后 64 点)中
一、前言
最近调试 STM32 ADC+DMA 循环采集的时候遇到了一个很玄学的问题:明明开启了 DMA 的半传输和全传输中断,理论上DMA中断函数里的Adc_IntCnt应该严格按照1→2→1→2交替出现,结果打断点调试的时候,发现两个中断会同时进入并不断循环。
问题场景
先简单说下我的配置:
- STM32F103,ADC 连续转换模式,DMA 循环传输
- DMA 缓冲区 128 点,开启半传输(前 64 点)和全传输(后 64 点)中断
- 半传输中断里
Adc_IntCnt=1,全传输中断里Adc_IntCnt=2 - 主循环里判断
Adc_IntCnt非零就处理,处理完在滤波函数里清零
异常现象:主循环打断点调试时,Adc_IntCnt经常连续好几次等于 1,或者连续好几次等于 2,完全不按交替的规律来。
坑点 1:中断函数里的隐藏 bug
第一个最隐蔽的问题:
// 错误写法!两个独立的if,不是互斥的
void DMA1_Channel1_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_HT1))
{
DMA_ClearITPendingBit(DMA1_IT_HT1);
Adc_IntCnt=1;
}
if(DMA_GetITStatus(DMA1_IT_TC1)) // 这里是if,不是else if!
{
DMA_ClearITPendingBit(DMA1_IT_TC1);
Adc_IntCnt=2;
}
}
问题在哪?如果进入中断的时候,半传输和全传输的标志同时置 1 了,两个 if 分支都会执行:先把Adc_IntCnt设为 1,马上又被覆盖为 2,半传输的事件直接丢失了!
什么时候两个标志会同时置 1?
- 有更高优先级的中断阻塞了 DMA 中断,等阻塞结束,两个标志都已经置位了
- 调试断点暂停 CPU 期间,DMA 还在传输,两个标志都被硬件置位了
坑点 2:调试误区
之前认为打断点暂停程序,整个单片机都停了。大错特错!调试断点只会暂停CPU 内核,DMA、定时器、ADC 这些独立外设根本不受影响,还在后台正常运行!
更坑的是:CPU 暂停期间,DMA 每完成一次传输,就会置位一次中断标志,NVIC 中断控制器会把这些中断请求都 “记下来”,等你点击恢复运行,CPU 会连续处理所有堆积的中断。
举个实际例子:主循环断点处暂停了 1 秒钟,DMA 中断频率是 1kHz,这 1 秒内 DMA 产生了 1000 次中断请求,恢复运行后 CPU 会连续进 1000 次 DMA 中断,Adc_IntCnt会被反复赋值 1000 次。
如果连续3次最后 一次进的都是半传输中断,主循环读到的就是连续 3 次 1,这就是看到 “连续好几次等于 1” 的根本原因。
二、解决方法
1. 修复中断逻辑(最关键)
把第二个if改成else if,确保每次中断只处理一个事件:
// 正确写法:互斥分支,不会覆盖
void DMA1_Channel1_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_HT1))
{
DMA_ClearITPendingBit(DMA1_IT_HT1);
Adc_IntCnt=1;
}
else if(DMA_GetITStatus(DMA1_IT_TC1)) // 改成else if!
{
DMA_ClearITPendingBit(DMA1_IT_TC1);
Adc_IntCnt=2;
}
}
2. 提高 DMA 中断优先级
避免 DMA 中断被其他中断阻塞,把抢占优先级设为最高:
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
3. 调试时开启 DMA 冻结(未验证)
如果需要正常断点调试,加一行代码让 CPU 暂停时 DMA 也停止:
// 初始化的时候加,调试暂停时DMA同步停止
DBGMCU->CR |= DBGMCU_CR_DBG_DMA1_STOP;
4. 加计数器验证(可选)
想直观看到中断堆积了多少次,加三个全局变量就行:
uint32_t int_total = 0, int_ht = 0, int_tc = 0;
void DMA1_Channel1_IRQHandler(void)
{
int_total++;
// ... 原有逻辑
if(ht) int_ht++;
else int_tc++;
}
三、DMA中断标志知识补充:
一、HTIF 置位的同时,GIF 一定会置位
DMA 的GIFx(通道 x 全局中断标志)是该通道所有事件标志的逻辑或,也就是:
GIFx = TEIFx(传输错误) || HTIFx(半传输) || TCIFx(全传输)
只要这三个事件标志里有任何一个被硬件置 1,对应的 GIFx 就会自动置 1。所以当半传输完成,HTIF1 置 1 的瞬间,GIF1(通道 1 全局标志)也会同时置 1,不需要手动操作。
补充:GIF 只是一个 "汇总标志",平时写代码根本不用管它,处理中断的时候只需要判断具体的 HT/TC/TE 标志就行。
二、DMA_ClearITPendingBit(DMA1_IT_HT1)只清除 HTIF 标志
这个库函数的行为是非常明确的:
-
它只会清除你指定的那个事件标志:
DMA1_IT_HT1对应的是 DMA_IFCR 寄存器的CHTIF1位,写 1 只会清除 HTIF1 半传输标志,不会影响 TCIF1、TEIF1,也不会直接清除 GIF1。 -
GIF 标志会自动清零,不需要你手动清除:当该通道的所有事件标志(HT/TC/TE)都被清除后,GIFx 会被硬件自动清零。比如你清除了 HTIF1,如果此时 TCIF1 也已经被清除了,GIF1 就会自动变成 0;如果还有 TCIF1 置 1,GIF1 还是 1,直到 TCIF1 也被清除。
补充:库函数宏的对应关系(STM32F1)
表格
| 库函数参数 | 对应 IFCR 寄存器位 | 清除的标志 |
|---|---|---|
| DMA1_IT_HT1 | 位 1(CHTIF1) | HTIF1(半传输) |
| DMA1_IT_TC1 | 位 2(CTCIF1) | TCIF1(全传输) |
| DMA1_IT_TE1 | 位 3(CTEIF1) | TEIF1(传输错误) |
| DMA1_IT_GL1 | 位 0(CGIF1) | GIF1(全局) |
注意:永远不需要手动清除 GIF 标志!库函数里提供的
DMA1_IT_GL1参数几乎没人用,因为完全没必
四、总结
嵌入式调试最容易踩的坑:
- 不要以为断点会暂停所有外设,CPU 和外设是独立运行的!
- 中断函数里的多个标志判断,一定要注意互斥,别用两个独立的 if!
- DMA 循环传输调试的时候,不要在中断处打断点,不然中断会堆到你怀疑人生。
本博客仅记录自己的学习进程,无任何商业用途,基于论坛上已经存在的内容结合自己实操过程完成了此博客,自己也做了一些细节验证,在此记录下来作为学习沉淀。如有侵权,联系速删。
更多推荐



所有评论(0)