3、DMA控制器内部架构与关键寄存器归纳总结
3.1 先看整体:DMA控制器长什么样?
你打开芯片手册,看到DMA模块框图,密密麻麻的信号线。其实核心部件就那么几个。我刚开始搞DMA时,被那些通道优先级、突发传输这些概念绕晕过。后来发现——你只需要记住四个关键单元:主控状态机、通道仲裁器、地址计算单元、传输计数器。其他都是围绕它们转的。
下面这张图是我对DMA内部结构的理解,每个箭头都有实际意义。
核心要点:DMA内部的数据流走的是“源地址→FIFO→目的地址”流水线,状态机控制每个节拍。仲裁器决定哪个通道占用总线——你调优先级时,改的就是仲裁器的权重。
3.2 关键寄存器分类与用途
寄存器是DMA的灵魂。说白了,配置寄存器就是告诉DMA:从哪里读、写到哪里、传多少、用什么模式。我列了一个常用寄存器清单,ST公司、NXP、TI的DMA都大同小异,名称可能不同,功能一样。
| 寄存器缩写 | 全称 | 位宽 | 核心作用 | 常见坑 |
|---|---|---|---|---|
| CSAR | Channel Source Address Register | 32 | 源数据起始地址 | 没对齐4字节导致传输错位 |
| CDAR | Channel Destination Address Register | 32 | 目的数据起始地址 | 目的地址不支持自增时死循环 |
| CNDTR | Channel Number of Data Register | 16或32 | 待传输的数据数量(递减至0) | 写0后不传输,但中断标志可能误置 |
| CCR | Channel Control Register | 32 | 方向、优先级、突发大小、增量模式 | 方向位写反,源地址变目的地址 |
| CEN | Channel Enable Register | 1 | 使能/禁止通道 | 传输完成前清使能,DMA跑到一半卡住 |
| CIFCR | Channel Interrupt Flag Clear Register | 32 | 写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 中断延迟与吞吐率相关的寄存器陷阱
你以为寄存器配置正确就完了?别急,两个跟性能直接相关的陷阱:
- 突发传输大小(burst size):CCR里的MBURST/PBURST。设得太大,占总线时间太长,CPU和其他DMA通道都要等。设得太小,总线效率低。一般外围设备(比如UART、I2C)用单次传输,内存搬运用4或8字突发。
- FIFO阈值(FIFO level):有些DMA有可调的FIFO阈值寄存器(如DMA_SxFCR)。阈值太低,每收一个字节就请求总线,中断频繁,延迟变差。阈值太高,数据包延迟增大。我一般设阈值=数据包大小的一半,平衡实时性和吞吐率。
- 中断标志的清除时机: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请求的时序,把理论变成可视化的波形。