一、前言

最近调试 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?

  1. 有更高优先级的中断阻塞了 DMA 中断,等阻塞结束,两个标志都已经置位了
  2. 调试断点暂停 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 标志

这个库函数的行为是非常明确的:

  1. 它只会清除你指定的那个事件标志DMA1_IT_HT1对应的是 DMA_IFCR 寄存器的CHTIF1位,写 1 只会清除 HTIF1 半传输标志,不会影响 TCIF1、TEIF1,也不会直接清除 GIF1。

  2. 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参数几乎没人用,因为完全没必

四、总结

嵌入式调试最容易踩的坑:

  1. 不要以为断点会暂停所有外设,CPU 和外设是独立运行的!
  2. 中断函数里的多个标志判断,一定要注意互斥,别用两个独立的 if!
  3. DMA 循环传输调试的时候,不要在中断处打断点,不然中断会堆到你怀疑人生。

本博客仅记录自己的学习进程,无任何商业用途,基于论坛上已经存在的内容结合自己实操过程完成了此博客,自己也做了一些细节验证,在此记录下来作为学习沉淀。如有侵权,联系速删。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐