3、DMA控制器内部架构与关键寄存器归纳总结

3.1 先看整体:DMA控制器长什么样?

你打开芯片手册,看到DMA模块框图,密密麻麻的信号线。其实核心部件就那么几个。我刚开始搞DMA时,被那些通道优先级、突发传输这些概念绕晕过。后来发现——你只需要记住四个关键单元:主控状态机、通道仲裁器、地址计算单元、传输计数器。其他都是围绕它们转的。

下面这张图是我对DMA内部结构的理解,每个箭头都有实际意义。

DMA控制器内部架构 主控状态机 空闲→请求→读取 →写入→完成 通道仲裁器 固定优先级 / 轮询 决定哪个通道先跑 地址计算单元 源地址 + 增量 目的地址 + 增量 传输计数器 CxNDTR 递减至0触发中断 数据缓冲FIFO 缓解总线竞争 深度:4/8/16字 寄存器组 CSAR / CDAR / CNDTR CCR / CEN / CIFCR AHB / APB 总线接口 每个通道 独立拥有 CSAR/CDAR CNDTR/CCR

核心要点:DMA内部的数据流走的是“源地址→FIFO→目的地址”流水线,状态机控制每个节拍。仲裁器决定哪个通道占用总线——你调优先级时,改的就是仲裁器的权重。

3.2 关键寄存器分类与用途

寄存器是DMA的灵魂。说白了,配置寄存器就是告诉DMA:从哪里读、写到哪里、传多少、用什么模式。我列了一个常用寄存器清单,ST公司、NXP、TI的DMA都大同小异,名称可能不同,功能一样。

寄存器缩写全称位宽核心作用常见坑
CSARChannel Source Address Register32源数据起始地址没对齐4字节导致传输错位
CDARChannel Destination Address Register32目的数据起始地址目的地址不支持自增时死循环
CNDTRChannel Number of Data Register16或32待传输的数据数量(递减至0)写0后不传输,但中断标志可能误置
CCRChannel Control Register32方向、优先级、突发大小、增量模式方向位写反,源地址变目的地址
CENChannel Enable Register1使能/禁止通道传输完成前清使能,DMA跑到一半卡住
CIFCRChannel Interrupt Flag Clear Register32写1清除对应中断标志忘记清标志,中断永远不触发第二次

个人习惯:我每次配DMA都会先写死CCR的突发传输大小为1(单次传输),调通后再改大爆发模式。这样万一地址算错了,至少每笔数据是完整的,好定位。

3.3 寄存器配置实战:一段典型初始化代码

以STM32F4为例,我们初始化通道2,从UART接收缓冲区搬运到内存。这里重点看寄存器配置的顺序和时序要求。

// DMA2 Stream2 配置(UART1 RX)
void DMA_UART_Init(void)
{
    // 1. 关闭通道,避免配置冲突
    DMA2->S2CR &= ~DMA_SxCR_EN;
    while (DMA2->S2CR & DMA_SxCR_EN);  // 等待硬件真正停掉

    // 2. 设置控制寄存器(一次性写,避免中间状态)
    DMA2->S2CR = (DMA_SxCR_CHSEL_3)        // 通道选择(UART1对应CH4)
                | DMA_SxCR_PL_0             // 优先级:中等
                | DMA_SxCR_MSIZE_0          // 存储器数据宽度:8bit
                | DMA_SxCR_PSIZE_0          // 外设数据宽度:8bit
                | DMA_SxCR_MINC             // 存储器地址递增
                | DMA_SxCR_DIR_0;           // 方向:外设→存储器

    // 3. 设置源地址(外设寄存器地址)
    DMA2->S2PAR = (uint32_t)&(USART1->DR);
    // 4. 设置目的地址(内存缓冲区)
    DMA2->S2M0AR = (uint32_t)uart_rx_buf;
    // 5. 设置传输数量(CNDTR)
    DMA2->S2NDTR = UART_RX_BUF_SIZE;

    // 6. 开启传输完成中断
    DMA2->S2CR |= DMA_SxCR_TCIE;
    // 7. 使能通道
    DMA2->S2CR |= DMA_SxCR_EN;
}

我曾经遇到的坑:先使能通道,再写源地址/目的地址。结果DMA一启动就去抓一个未初始化的地址,直接硬件错误。正确的顺序一定是:关通道→配置所有寄存器→最后开使能。顺序写反,后果自负。

3.4 寄存器状态怎么读?怎么验证配置生效?

配置完了不能只看代码自嗨。你要用调试器或者示波器确认几件事:

  • 看CSAR/CDAR:读回来是不是你写的值?如果被硬件改了,说明自增模式已开启。
  • 看CNDTR:每次传输后递减了吗?如果一直不动,说明DMA根本没跑起来。
  • 看CCR的EN位:使能后硬件会自动置1。如果一直为0,检查时钟或外部DMA请求。
  • 看中断状态寄存器(CISR):传输完成、半传、传输错误,该置位的有没有?

一个查错小技巧:在中断服务函数里先读CISR,看看是不是意外产生了传输错误中断(TEIF)。TEIF常由源/目的地址访问权限问题引起——说白了,就是你的地址不在DMA可访问的地址空间内。我之前配SPI DMA时,把源地址写成了Flash地址(只读),结果TEIF一直触发。

3.5 中断延迟与吞吐率相关的寄存器陷阱

你以为寄存器配置正确就完了?别急,两个跟性能直接相关的陷阱:

  1. 突发传输大小(burst size):CCR里的MBURST/PBURST。设得太大,占总线时间太长,CPU和其他DMA通道都要等。设得太小,总线效率低。一般外围设备(比如UART、I2C)用单次传输,内存搬运用4或8字突发。
  2. FIFO阈值(FIFO level):有些DMA有可调的FIFO阈值寄存器(如DMA_SxFCR)。阈值太低,每收一个字节就请求总线,中断频繁,延迟变差。阈值太高,数据包延迟增大。我一般设阈值=数据包大小的一半,平衡实时性和吞吐率。
  3. 中断标志的清除时机:CIFCR必须在中断服务函数末尾写1清除。如果在函数开头就清,新中断会立即嵌套,栈溢出。
寄存器位推荐值效果不推荐
MBURST[1:0]0b10 (4次突发) 或 0b11 (8次突发)吞吐率最高,但延迟增加0b00 (单个) 或 0b01 (INCR4无效)
FIFO阈值[1:0]0b11 (FIFO满才请求) 或 0b10 (半满)减少总线占用0b00 (1/4深度) 中断过密
优先级[1:0]根据实际紧急度高优先级通道延迟低全部设为相同优先级,竞争随机

3.6 一个错误配置引发的血案

我曾经有个项目,用DMA搬运音频数据到I2S发送。播放时有间歇性噗噗声。用逻辑分析仪抓了GPIO,发现DMA有时200μs才响应一次请求。查寄存器发现:我把DMA优先级设成了最低(PL=00),而系统中另外两个UART DMA也在同时跑。仲裁器每次都先处理UART(高优先级),音频DMA被饿死了。修改优先级后,噗噗声消失。

所以记住:DMA中断延迟不只是中断服务函数长短的事,跟仲裁器、FIFO阈值、突发大小都有直接关系。排查时,先看寄存器配置是不是“内耗”了。

本章从寄存器角度拆了DMA内部架构。下个知识点,我们聊聊如何用逻辑分析仪抓取DMA请求的时序,把理论变成可视化的波形。