DMA 关键寄存器详解:源地址、目的地址、传输数量、CCR、ISR 与 IFCR

DMA 控制器说白了就是一堆寄存器在干活。你配置好它们,它就能自己搬数据。搬完了,它还会告诉你结果。

我个人习惯把 DMA 寄存器分成三组:地址类、控制类、状态类。咱们一个一个拆开看。

源地址寄存器 (CMARx / CPARx)

源地址,就是数据从哪来。对于内存到外设的传输,源是内存;对于外设到内存,源是外设的数据寄存器。

  • CMARx:内存地址寄存器。存放内存缓冲区的基地址。
  • CPARx:外设地址寄存器。存放外设数据寄存器的地址。
我踩过的坑: 地址必须按传输宽度对齐。比如你配了 32 位传输,地址必须是 4 字节对齐。否则 DMA 会 stuck,或者产生总线错误。我曾经在 STM32F4 上因为地址没对齐,查了整整一个下午。

目的地址寄存器

数据往哪搬,目的地址就指向哪。和源地址一样,也分内存和外设两个角色。

传输方向 源地址 目的地址
内存 → 外设 CMARx(内存) CPARx(外设)
外设 → 内存 CPARx(外设) CMARx(内存)
内存 → 内存 CMARx(源内存) 另一个 CMARx(目的内存)

嗯,这里要注意:内存到内存传输,两个地址寄存器都是 CMARx。外设地址寄存器 CPARx 这时候不用。

传输数量寄存器 (CNDTRx)

这个寄存器告诉 DMA:你要搬多少个数据单元。注意,是数据单元个数,不是字节数。

  • 每次传输完成后,CNDTRx 自动减 1。
  • 减到 0 时,DMA 传输完成,产生传输完成中断。
  • 你可以在中断服务程序里重新给 CNDTRx 赋值,实现循环传输。
重要提醒: CNDTRx 是递减计数器。你在调试时读它,看到的是剩余次数,不是总次数。我见过不少新手把这个搞反了。

控制配置寄存器 (CCRx)

CCR 是 DMA 的大脑。它决定了传输的一切行为。我把它拆成几个关键位来讲。

位域 功能 我的建议
EN 使能 DMA 通道 最后再置位,先配好其他所有寄存器
DIR 传输方向:0=外设到内存,1=内存到外设 配错方向,数据就飞了
CIRC 循环模式:1=使能 环形缓冲区必须开
PINC / MINC 外设/内存地址增量:1=使能 连续传输必须开,固定地址(如 ADC 数据寄存器)关掉
PSIZE / MSIZE 外设/内存数据宽度:00=8位,01=16位,10=32位 两边宽度不一致时,DMA 会自动对齐,但效率会下降
PL[1:0] 通道优先级:00=低,01=中,10=高,11=最高 实时性要求高的外设(如音频 I2S)给高优先级
TCIE / HTIE / TEIE 中断使能:传输完成/半传输/传输错误 调试阶段全开,生产环境按需开
核心经验: 我建议你每次配置 CCR 时,都先清零再赋值。因为 DMA 通道可能被之前的中断残留状态污染。清零是个好习惯。

中断状态寄存器 (ISR)

ISR 是 DMA 的「告示牌」。它告诉你每个通道发生了什么事件。每个通道有 4 个标志位:

  • TCIFx:传输完成标志。CNDTRx 减到 0 时置位。
  • HTIFx:半传输标志。CNDTRx 减到一半时置位。环形缓冲区常用。
  • TEIFx:传输错误标志。总线错误、地址错误时置位。
  • GIFx:全局中断标志。上述任一事件发生时置位。

ISR 是只读的。你不能写它。要清除标志,得用 IFCR。

调试技巧: 我调试 DMA 时,第一件事就是在 ISR 里看 TEIF 位。如果它置位了,说明硬件层面有问题——地址不对、总线冲突、或者外设没准备好。别急着查软件逻辑,先看硬件。

中断清除寄存器 (IFCR)

IFCR 是 ISR 的「橡皮擦」。你往对应的位写 1,就能清除 ISR 里对应的标志。

IFCR 位 清除的 ISR 标志
CTCIFx TCIFx
CHTIFx HTIFx
CTEIFx TEIFx
CGIFx GIFx(同时清除所有标志)
常见错误: 中断服务程序里,一定要先清除标志,再处理数据。否则你刚清完,DMA 又产生新中断,标志又置位了,导致中断嵌套或者丢失事件。我见过有人把顺序搞反,结果环形缓冲区数据错乱,查了两天。

寄存器操作流程图

下面这张图,是我自己总结的 DMA 寄存器配置流程。你照着走,基本不会出错。

DMA 寄存器配置与中断处理流程 1. 配置源/目的地址寄存器 2. 配置传输数量 CNDTRx 3. 配置 CCR(方向/宽度/优先级/中断) 4. 置位 CCR.EN,启动传输 5. 中断服务:读 ISR → 清 IFCR → 处理数据 ⚠️ 关键顺序 • 地址 → 数量 → CCR • EN 最后置位 • 中断里先清标志 💡 调试建议 • 先读 ISR 看 TEIF • 检查 CNDTRx 递减

实战:环形缓冲区中的寄存器配合

环形缓冲区用 DMA 时,CCR 的 CIRC 位必须置 1。这样 CNDTRx 减到 0 后会自动重载,DMA 继续传输。

中断处理里,我一般这样写:

void DMA1_Channel1_IRQHandler(void)
{
    // 1. 读 ISR,判断中断类型
    if (DMA1->ISR & DMA_ISR_TCIF1)
    {
        // 2. 先清除标志
        DMA1->IFCR |= DMA_IFCR_CTCIF1;

        // 3. 再处理数据
        // 更新环形缓冲区的读指针
        ring_buf->read_index = (ring_buf->read_index + BLOCK_SIZE) % BUF_SIZE;

        // 4. 通知应用层
        semaphore_post(&data_ready);
    }

    // 半传输中断同理
    if (DMA1->ISR & DMA_ISR_HTIF1)
    {
        DMA1->IFCR |= DMA_IFCR_CHTIF1;
        ring_buf->half_flag = 1;
    }

    // 错误处理
    if (DMA1->ISR & DMA_ISR_TEIF1)
    {
        DMA1->IFCR |= DMA_IFCR_CTEIF1;
        // 记录错误,重启 DMA
        error_handler();
    }
}
我的习惯: 每次中断都检查 TEIF。哪怕你觉得不会出错。因为总线错误、外设时钟没开、DMA 通道冲突,都会导致 TEIF 置位。早发现早解决。