DMA中断机制、NVIC 联动与配置实战   

1. DMA 中断

1.1 回答上一章的遗留问题

上一章结尾问:

        "CPU 怎么知道 DMA 把数据搬迁完毕了?DMA 又怎么告诉 CPU,让 CPU 介入做后续数据处理?"

直接回答:

  • DMA(核心是通道)控制器,本质也是一个 MCU 片上外设,和串口、SPI、I2C 一样;
  • 既然其他外设能向 CPU 发送中断,DMA 也能;
  • 当 DMA 搬迁完毕数据,会通过中断的方式通知 CPU,进而让 CPU 进行中断处理;在中断处理函数内部,就可以进一步处理数据了(打印、保存、计算、启动下一批传输……)。

扩展理解:

        为什么用中断而不是 CPU 轮询?——中断是"事件驱动":搬完了才通知,CPU 平时不用操心;轮询则是 CPU 反复去查"搬完没",白白消耗 CPU。

        这正好延续了 DMA 的设计哲学:把 CPU 从等待和搬运中彻底解放出来。

1.2 中断使能位:TCIE、HTIE、TEIE(在 DMA_CCRx 中)

        既然数据搬迁工作以通道为主体,那么每一个通道一定会有对应的配置信息来支持产生中断——这个寄存器就是之前反复提到的 DMA_CCRx(偏移地址 0x08 + 20 × (通道编号 - 1),复位值 0x0000 0000)。

其中与中断有关的三个位(课件中红色标注区域):

位名称英文全称触发时机典型用途
位 3TEIETransfer error interrupt enable传输过程中发生总线错误或配置冲突

异常捕获和处理,

避免程序卡死

位 2HTIEHalf transfer interrupt enable数据传输量达到预设总额的一半

半批数据先处理,

与 DMA 搬运并行

位 1TCIETransfer complete interrupt enable该通道所有数据传输完成通知 CPU:"我已搬完,你可以处理了"(最常用)

逐条解读:

  • TCIE(传输完成中断):很好理解,就是当该通道的所有数据传输完成时触发——通知 CPU"我已经搬迁完毕数据了,你可以处理了";
  • HTIE(半传输中断):当数据传输量达到预设总额的一半时触发中断;
  • TEIE(传输错误中断):当传输过程中发生总线错误或配置冲突时触发,主要是为了进行异常捕获和处理,避免程序卡死。

重要结论:

        该寄存器每一个通道都有,所以每一个通道都可以独立配置、独立向 CPU 发送中断。(再次印证:DMA 中断以通道为载体。)

1.3 为什么会存在半传输中断?(了解)

原因:实现数据传输与数据处理的并行,消除等待时间。

  • 在只有"传输完成中断(TC)"的情况下,CPU 必须等整块数据搬运完才能开始工作;
  • 但如果有了 HT 半传输中断,就可以先传输一半数据,然后让 CPU 介入数据处理,同时 DMA 在继续搬运后半部分数据——这样数据处理和数据搬迁在逻辑上就并行进行了。

两种工作方式对比:

方式时序

只有 TC

(串行)

DMA 数据搬运完 —TC→ CPU 处理数据 —→ DMA 数据搬运完 —TC→ CPU 处理数据 ……(CPU 大量时间在"干等")

有 HT

(并行)

DMA 搬一半 —HT→ [CPU 处理前半段 ‖ DMA 继续搬后半段] —TC→ 搬一半 —HT→ ……(CPU 与 DMA 接力赛)

1.4 双缓冲(Ping-Pong Buffering):HT 最经典的应用

场景设定:假设你有一个 1024 字节的缓冲区:

  • 前 512 字节(前半段):当 DMA 填充完这部分时,触发 HT 中断;
  • 后 512 字节(后半段):此时 DMA 不会停止,它继续向后半段搬运数据;
  • 此时:CPU 在中断处理函数里开始处理前半段的数据,而 DMA 正在后台搬运后半段的数据。两者互不干扰,像"接力赛"一样循环往复。

为什么叫"Ping-Pong"?——CPU 和 DMA 就像打乒乓球:DMA 把球(前半段数据)打给 CPU(触发 HT),CPU 接球处理时 DMA 已经把下一个球(后半段数据)准备好了,如此往复,任何时刻都不空转。

✨核心场景:音频处理。

        如果"读取完音频 → 播放 → 再读取 → 再播放",中间会产生等待,出现音频卡顿;采用 HT 中断方式,可以让播放音频和拷贝音频同时进行,有效解决卡顿问题。

        这个方案后续在相关项目中会体现,此处不是目前重点。

1.5 DMA 通道触发中断的完整路径(与 NVIC 联动)

先看一张图理解整体架构(课件"最后一张图"):

Cortex-M3 内核  ←→  NVIC(嵌套向量中断控制器)
                      ├─ 6 个中断优先级寄存器
                      └─ 所有中断源/外部中断源的开关控制
                          ↑ 中断结果反馈
DMA 控制器
  ├─ DMA_CCR / DMA_CNDTR / DMA_CPAR / DMA_CMAR(寄存器组)
  ├─ 通道中断(软件使能 / 硬件使能)
  └─ 仲裁器(软件优先级 / 硬件优先级)、控制逻辑、外设通信接口
  • CPU 修改寄存器 = 设置 DMA 通道配置(这是程序员做的事);
  • 通道产生中断后,中断结果反馈给 NVIC;
  • NVIC 根据中断向量表找到对应的中断处理函数,CPU 跳转执行。

1.6 中断向量编号(stm32f10x.h 的 IRQn_Type 枚举)

Cortex-M3 内核异常(负数编号):NonMaskableInt_IRQn = -14、HardFault_IRQn = -13、MemoryManagement_IRQn = -12、BusFault_IRQn = -11、UsageFault_IRQn = -10、SVCall_IRQn = -5、DebugMonitor_IRQn = -4、PendSV_IRQn = -2、SysTick_IRQn = -1。

STM32 外设中断(正数编号),其中 DMA 相关:

中断名编号说明
DMA1_Channel1_IRQn11DMA1 通道 1 全局中断
DMA1_Channel2_IRQn12DMA1 通道 2 全局中断
DMA1_Channel3_IRQn13DMA1 通道 3 全局中断
DMA1_Channel4_IRQn14DMA1 通道 4 全局中断
DMA1_Channel5_IRQn15DMA1 通道 5 全局中断
DMA1_Channel6_IRQn16DMA1 通道 6 全局中断
DMA1_Channel7_IRQn17DMA1 通道 7 全局中断
DMA2_Channel1_IRQn56DMA2 通道 1 全局中断
DMA2_Channel2_IRQn57DMA2 通道 2 全局中断
DMA2_Channel3_IRQn58DMA2 通道 3 全局中断
DMA2_Channel4_5_IRQn59DMA2 通道 4 和通道 5 全局中断(共用)

观察规律:

        DMA1 每个通道独占一条中断线(11~17),所以在 DMA1 的中断处理函数里,理论上只用关心"自己这个通道"的事件;而 DMA2 通道 4、5 共用一个中断号,需要在处理函数里再区分是哪个通道。

✨核心结论:

        DMA 中断的配置和产生,是以通道 Channel 为载体的——这点在后续代码中会有非常强烈的体现。

1.7 最后的问题:CPU 怎么区分是"哪种中断"?

        现在 DMA 特定通道已经能配置并完成数据搬迁,也能通过中断通知 CPU 某个通道产生了中断。

但还剩最后一组问题:

        CPU 确实能收到某一个通道发来的中断,但是,CPU 怎么区分是"传输完毕(TC)""传输一半(HT)"还是"传送出错(TE)"呢?

朴素的思考:

        你一个通道也就一根中断线连接到 CPU 内的 NVIC,最多传递一个高低电平或者电脉冲,告诉 CPU"有中断了"。

        太多种类的信息传递不了——如果要传递,这不就成了通信,还得再定个协议什么的。

那么,怎么办呢?

        引入 DMA 控制器内部倒数第二个寄存器:DMA_ISR(中断状态寄存器),

        以及最后一个寄存器:DMA_IFCR(中断标志清除寄存器)。


2. DMA_ISR:DMA 中断状态寄存器

  • 偏移地址:0x00;复位值:0x0000 0000;只读(各位属性为 r)。

位布局(每通道 4 个标志,共 7 通道):

通道传输错误标志半传输标志传输完成标志全局中断标志
通道 7TEIF7(位27)HTIF7(位26)TCIF7(位25)GIF7(位24)
通道 6TEIF6(位23)HTIF6(位22)TCIF6(位21)GIF6(位20)
通道 5TEIF5(位19)HTIF5(位18)TCIF5(位17)GIF5(位16)
通道 4TEIF4(位15)HTIF4(位14)TCIF4(位13)GIF4(位12)
通道 3TEIF3(位11)HTIF3(位10)TCIF3(位9)GIF3(位8)
通道 2TEIF2(位7)HTIF2(位6)TCIF2(位5)GIF2(位4)
通道 1TEIF1(位3)HTIF1(位2)TCIF1(位1)GIF1(位0)

4 个标志位的含义:

标志x 的含义整体含义
TEIFx通道号是否发生了 DMA 数据搬迁错误(TE)
HTIFx通道号是否发生了 DMA 数据半传输完成(HT)
TCIFx通道号是否发生了 DMA 数据全部传输完成(TC)
GIFx通道号是否产生了上述任意事件(TE、HT 或 TC)
  • 这些位由硬件设置;
  • 清除方式:在 DMA_IFCR 寄存器的相应位写入 '1'。

2.1 最重要的机制:硬件自动置位

        一旦特定通道产生中断,除了给 CPU 发送中断通知之外,DMA 硬件还会自动(自动,自动,自动)设置 DMA_ISR 寄存器中特定的位。

举例:通道 5 发生了 TC,那么对应位的状态为:

TEIF5 = 0(没有传输错误)
HTIF5 = 0(没有半传输事件)
TCIF5 = 1(传输完成!)
GIF5  = 1(有事件发生)

逻辑闭环:

        CPU 一旦收到中断 → 回过头检测 DMA_ISR 寄存器 → 即可明确"是哪一个通道、产生了什么中断" → 再进行软件处理。至此,DMA 理解的逻辑闭环完成了。

顺带一提:

        这些中断标志位是只读的,不能手动直接清 0。

        如果处理完了,要清除对应的中断信息,就需要设置最后一个寄存器 DMA_IFCR,来达到清除 DMA_ISR 的目的。

2.2 DMA_IFCR:DMA 中断标志清除寄存器

  • 偏移地址:0x04;复位值:0x0000 0000。

位布局(与 DMA_ISR 一一对应,写 1 清除):

清除位对应标志作用

CTEIFx

(位27,23,19,15,11,7,3)

TEIFx1 = 清除 DMA_ISR 中的 TEIF 标志

CHTIFx

(位26,22,18,14,10,6,2)

HTIFx1 = 清除 DMA_ISR 中的 HTIF 标志

CTCIFx

(位25,21,17,13,9,5,1)

TCIFx1 = 清除 DMA_ISR 中的 TCIF 标志

CGIFx

(位24,20,16,12,8,4,0)

GIFx

(含全部)

1 = 清除 DMA_ISR 中的 GIF、TEIF、HTIF 和 TCIF 标志
  • 这些位由软件设置和清除;0 = 不起作用。

例子(理解清除逻辑):

        你把你们村里的小朋友打了,小朋友要收拾你,但不直接找你(因为打不过),ta 会去找你妈,让你妈收拾你——就这个逻辑。

解读:

        DMA 不直接"打断" CPU 的每一步(就像小朋友不直接找你打架),而是通过 ISR 标志位"告状"给中断系统(找你妈),CPU 在中断里"收拾"(处理)完之后,再通过 IFCR 把"状纸"(标志位)撕掉——处理完必须清标志,否则下次中断无法正确区分新事件。

2.3 关于 HAL 库

        关于这些寄存器的设置和清除工作,后续有 HAL 库自动为我们完成。

        我们后续阅读源码的时候,反向查看文档再来细说。

(例如 HAL 库的 HAL_DMA_IRQHandler 会自动读取 ISR、判断 TC/HT/TE、调用对应回调函数并清除标志。)

2.4 一张图看懂完整链路

核心链路:

通道产生事件(TC/HT/TE)
  → DMA 硬件自动置位 DMA_ISR 对应标志位
  → 通过中断线通知 NVIC
  → CPU 响应,进入对应通道的 NVIC 中断处理函数
  → 读 DMA_ISR:区分是哪个通道、哪种事件
  → 软件处理数据
  → 写 DMA_IFCR 对应位清除标志
  → 返回主程序

3. 配置案例

3.1 案例 1:配置 DMA(串口 DMA 接收)

使用 USART1 的 DMA 模式进行数据接收的配置示例:

端参数配置值为什么这样配
外设端 USART1数据宽度8bit(1byte)串口数据有效长度就是 8 位
地址自增?不自增只有一个数据寄存器
寄存器地址0x40013804就是 USART1 数据寄存器地址
传输过程数据数量32一次配置搬运 32 个数据项
存储器端 SRAM数据宽度8bit(1byte)与收发端宽度匹配
地址自增?自增需要连续保存串口数据寄存器的数据
存储器地址0x200000e0接收缓冲区起始地址

详细配置解释:

  1. 寄存器地址(0x40013804)其实就是 USART1 的数据寄存器地址;
  2. 因为数据有效长度就是 8 位,所以收、发端数据宽度都配置为 8 位;
  3. 因为只有一个数据寄存器,所以外设端寄存器地址需要配置为不自增模式;
  4. 因为需要连续保存串口数据寄存器的数据,所以存储器(SRAM)端的地址需要自增。

总结规律:

        外设端"死守一个寄存器"(不自增),内存端"排队存数据"(自增)——这就是串口接收最典型的"单增模式"配置。

3.2 案例 2:按图说话

需求:

  1. 传输方向为外设到存储器;
  2. 外设只有一个 8 位的寄存器保存数据,该寄存器地址为 0x40013804,记作 REG(0x40013804);
  3. 内存缓冲区长度为 256 字节,起始地址为 0x200000e0,记作 BUF(0x200000e0);
  4. 数据宽度为 8 位,外设地址不自增(只有一个),一次 DMA 传输读取 256 字节,内存缓冲区地址自增(256 个地址)。

DMA 工作过程示意图中的地址关系:

  • 外设端:寄存器地址固定 0x40013804;
  • 内存端:缓冲区从 0x200000e0 到 0x200001e0(0x200000e0 + 256 = 0x200001e0)。

完整 12 步过程:

步骤事件动作
1CPU 启动CPU 启动 DMA 数据传输
2外设就绪外设寄存器数据非空,外设向 DMA 控制器发出 DMA 请求
3首字节搬运

DMA 控制器向外设应答,

启动数据传输:REG(0x40013804) → BUF(0x200000e0)

4事件清除数据传输完成后,寄存器数据非空事件自动被清除
5外设就绪外设寄存器数据非空(又来数据了),外设向 DMA 控制器发出 DMA 请求
6第 2 字节搬运DMA 应答,启动数据传输:REG(0x40013804) → BUF(0x200000e0+1)
7事件清除数据传输完成后,寄存器数据非空事件自动被清除
8……如此往复(第 3、4、…、254 字节同理,内存地址每次 +1)
9外设就绪外设寄存器数据非空(又来数据了),外设向 DMA 控制器发出 DMA 请求
10最后 1 字节搬运

DMA 应答,

启动数据传输:REG(0x40013804) → BUF(0x200000e0+255)

11事件清除数据传输完成后,寄存器数据非空事件自动被清除
12传输完成256 字节数据传输完成,产生数据传输完成(TC)中断

推演要点:

  • 外设地址永远不变(0x40013804)——外设只有一个数据寄存器,PINC = 0;
  • 内存地址每搬 1 字节 +1(0x200000e0 → 0x200000e0+1 → … → +255)——MINC = 1,按数据宽度 8 位自增;
  • 每搬完 1 字节,外设"数据非空"事件自动清除,等于告诉外设"我这口井的水被你打走了,可以继续蓄水";
  • 搬满 NDT = 256 项后,CNDTR 归零 → 产生 TC 中断 → 通知 CPU 处理这 256 字节。

4. 总结:使用 DMA 的流程

三个阶段、一条主线:

阶段参与者配置/动作产出
① 程序员设置程序员(软件)方向、优先级、地址自增、循环模式、数据宽度DMA 通道建立
② CPU 启动CPU源/目的地址、传输数据长度启动 DMA 传输
③ DMA 自动DMA + 中断

发送:发送下一批数据;

接收:处理接收到的数据,启动新的 DMA 接收流程

传输完毕回调处理

闭环解读:

  • 程序员把"怎么搬"配好(通道建立);
  • CPU 告诉 DMA"从哪搬到哪、搬多少"(启动传输);
  • 之后 DMA 全自动搬运,搬完通过中断回调通知 CPU;
  • 发送场景:CPU 在回调里准备/发送下一批数据;
  • 接收场景:CPU 在回调里处理接收到的数据,并启动新的 DMA 接收流程(重新配 CNDTR、重新使能,循环往复)。

一句话总结:

        配通道 → 启动传输 → DMA 自动搬 → 中断回调收尾,这就是使用 DMA 的完整流程。


面试题

一、中断机制类

1. DMA 搬完数据后怎么通知 CPU?凭什么 DMA 能发中断?

        DMA 通过中断方式通知 CPU。

        因为 DMA(核心是通道)控制器本质也是一个 MCU 片上外设,和串口、SPI、I2C 一样,都有能力向 CPU 发送中断。

        CPU 收到中断后进入中断处理函数,处理已搬完的数据。

⚡ 追问:为什么用中断而不是 CPU 轮询?——事件驱动,搬完才通知,CPU 平时不被打扰;轮询要反复查状态,浪费 CPU。


2. DMA 的中断使能位在哪?有哪几个?

        在 DMA_CCRx(通道配置寄存器)中,共 3 个:

TCIE(位1,传输完成中断)、HTIE(位2,半传输中断)、TEIE(位3,传输错误中断)。

        每个通道都有自己的一套,可独立配置。

⚡ 追问:TEIE 有什么用?——传输发生总线错误或配置冲突时触发,用于异常捕获,避免程序卡死。


3. TC、HT、TE 三种中断各自什么时候触发?

        TC:该通道所有数据传输完成时;

        HT:数据传输量达到预设总额一半时;

        TE:传输过程中发生总线错误或配置冲突时。


4. 为什么要设计半传输中断 HT?

        为了实现数据传输与数据处理的并行,消除 CPU 等待时间。

        只有 TC 时 CPU 要等整块搬完才能处理;

        有 HT 时,DMA 搬一半就通知 CPU 先处理前半段,同时 DMA 继续搬后半段,逻辑上并行。

⚡ 追问:HT 的经典应用是什么?——双缓冲(Ping-Pong Buffering),典型用于音频播放,避免"读一段播一段"的卡顿。


二、双缓冲类

5. 什么是双缓冲(Ping-Pong)?怎么工作?

        以 1024 字节缓冲区为例:DMA 填满前 512 字节触发 HT 中断,CPU 在中断里处理前半段,同时 DMA 不停止、继续向后半段搬运;CPU 处理完前半段时,DMA 也快搬完后半段……两者像接力赛一样循环往复,互不干扰。

⚡ 追问:为什么能解决音频卡顿?——播放(CPU 处理)和拷贝(DMA 搬运)同时进行,不再"读完了才能播",消除了等待间隙。


三、区分与清除类

6. CPU 收到 DMA 中断后,怎么知道是哪个通道的哪种事件?

        靠 DMA_ISR(中断状态寄存器,偏移 0x00,只读)。

        每个通道有 4 个标志位:

TCIFx(传输完成)、HTIFx(半传输)、TEIFx(传输错误)、GIFx(任意事件)。

        CPU 读 ISR 即可区分通道和事件类型。

⚡ 追问:这些标志位是软件置位还是硬件置位?——硬件自动置位。

        一旦通道产生中断,DMA 硬件自动设置 ISR 对应位。

        例如通道 5 发生 TC:TEIF5=0、HTIF5=0、TCIF5=1、GIF5=1。


7. DMA_ISR 的标志位能直接清零吗?怎么清?

        不能,标志位是只读的。

        必须通过 DMA_IFCR(中断标志清除寄存器,偏移 0x04)

        对应位写 1 清除:

        CTCIFx 清 TCIF、CHTIFx 清 HTIF、CTEIFx 清 TEIF、CGIFx 清 GIF 及全部。

⚡ 追问:处理完不清标志会怎样?——旧事件标志残留,下一次中断到来时无法正确区分新事件,且可能导致中断处理逻辑错乱;所以中断处理函数末尾"清标志"是必须步骤。


8. 一个通道只有一根中断线,怎么传递"完成/半传输/错误"多种信息?

        一根中断线只能传一个电脉冲/高低电平,告诉 CPU"有中断";

        具体是哪种事件由 DMA_ISR 的标志位区分——中断线负责"通知",ISR 负责"分类",IFCR 负责"销案"。


四、NVIC 与向量表类

9. DMA 的中断号和 NVIC 是什么关系?DMA1 各通道中断号是多少?

        通道产生中断 → 反馈给 NVIC(嵌套向量中断控制器,管理中断开关与优先级)→ NVIC 按中断向量表触发对应中断处理函数。

⚡ 追问:DMA 中断以什么为载体?——以通道 Channel 为载体,通道是中断配置与产生的最小单位。


10. 完整描述一次串口 DMA 接收(256 字节)的过程。

        ① CPU 启动 DMA;

        ② 外设寄存器数据非空,向 DMA 发请求;

        ③ DMA 应答,搬 REG(0x40013804) → BUF(0x200000e0);

        ④ 数据非空事件自动清除;

        ⑤ 再来数据再请求、再搬(内存地址 +1,外设地址不变);…… 重复 256 次,第 256 字节搬到 BUF(0x200000e0+255);

        ⑫ 256 字节全部完成,产生 TC 中断通知 CPU。

⚡ 追问:为什么外设端不自增、内存端自增?——外设只有一个数据寄存器,必须固定地址;内存要连续存放多字节,所以自增排队。


五、配置与流程类

11. 串口 DMA 接收要配置哪些关键参数?

外设端:数据宽度(8 位)、不自增、外设地址(USART1 数据寄存器 0x40013804);

存储器端:数据宽度(8 位)、自增、内存地址(缓冲区);

传输过程:数据数量(NDT)、方向(DIR=0 外设→内存)、优先级(PL)、EN 使能;

外设侧还要开 USART_CR3 的 DMAR。

⚡ 追问:搬完后怎么进入下一轮接收?——在 TC 中断回调里处理数据,然后重新配置 CNDTR、重新使能通道,启动新的 DMA 接收流程(循环)。


12. 使用 DMA 的整体流程分哪几步?

三阶段:        

① 程序员设置(方向、优先级、地址自增、循环模式、数据宽度)→ 建立 DMA 通道;        

② CPU 启动(源/目的地址、传输数据长度)→ 启动 DMA 传输;

③ DMA 自动(发送下一批 / 接收处理并启动新接收)→ 传输完毕回调处理。


Logo

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

更多推荐