4. DMA核心概念
各位同学,今天我们来啃 DMA 控制器最核心的四个寄存器。说实话,我刚接触 DMA 时,面对数据手册里那一大堆寄存器描述,头都是大的。但后来我发现,只要你把源地址、目的地址、传输计数和控制这四个寄存器搞明白,DMA 的基本操作就通了八成。
咱们一个一个来拆解。我会结合 Rust 的寄存器操作风格,把每个寄存器的位域定义、配置要点和实际坑点都讲清楚。
4.1 源地址寄存器(SAR)
源地址寄存器,顾名思义,就是存放数据来源的地址。它告诉 DMA 控制器:「嘿,数据从这里搬走。」
在大多数 MCU 中,SAR 是一个 32 位寄存器,支持字节、半字、字三种对齐方式。我个人的习惯是:能用字对齐就用字对齐,因为非对齐访问会触发总线错误,或者导致性能下降。
| 位域 | 名称 | 描述 |
|---|---|---|
| [31:0] | SA | 源地址,支持递增、递减、固定三种模式 |
这里有个关键点:源地址的递增模式。如果你从内存搬运数据到外设,源地址通常要递增;如果从外设搬运到内存,源地址往往固定(比如 ADC 的数据寄存器)。
4.2 目的地址寄存器(DAR)
DAR 和 SAR 是对称的。它存放数据要搬到的目标地址。配置逻辑和 SAR 几乎一样,只是方向相反。
| 位域 | 名称 | 描述 |
|---|---|---|
| [31:0] | DA | 目的地址,同样支持递增、递减、固定 |
嗯,这里要注意一个细节:SAR 和 DAR 的地址递增方向必须和传输方向匹配。比如从内存到外设,SAR 递增、DAR 固定;从外设到内存,SAR 固定、DAR 递增。如果搞反了,数据就会写到奇怪的地方去。
4.3 传输计数寄存器(CNDTR)
CNDTR 是 DMA 传输的「计数器」。它告诉控制器:「这次要搬多少数据?」
这个寄存器通常是一个 16 位或 32 位的递减计数器。每次传输完成一个数据单元(字节/半字/字),CNDTR 就减 1。当减到 0 时,DMA 传输完成,触发中断或事件。
| 位域 | 名称 | 描述 |
|---|---|---|
| [15:0] | NDT | 传输数据项数量,最大 65535 |
这里有个容易混淆的地方:CNDTR 的值是数据项的数量,不是字节数。如果你配置的数据宽度是 32 位(字),CNDTR = 100 表示传输 100 个字,也就是 400 字节。很多新手在这里算错,导致缓冲区溢出。
核心公式:
总传输字节数 = CNDTR × 数据宽度(字节)
数据宽度:8 位 = 1 字节,16 位 = 2 字节,32 位 = 4 字节
另外,CNDTR 在传输过程中是只读的。你不能在传输进行中修改它,否则会导致不可预知的行为。我见过有人想在传输中途「加塞」数据,直接写 CNDTR,结果 DMA 直接挂死。
4.4 控制寄存器(CR)
CR 是 DMA 的「大脑」。它控制着传输方向、数据宽度、优先级、中断使能等所有关键配置。这个寄存器通常有十几个位域,咱们挑最重要的几个讲。
| 位域 | 名称 | 描述 |
|---|---|---|
| [1:0] | EN | 通道使能,0=禁用,1=使能 |
| [4] | DIR | 传输方向:0=外设到内存,1=内存到外设 |
| [9:8] | PSIZE | 外设数据宽度:00=8位,01=16位,10=32位 |
| [11:10] | MSIZE | 内存数据宽度:00=8位,01=16位,10=32位 |
| [13:12] | PL | 优先级:00=低,01=中,10=高,11=最高 |
| [14] | MEM2MEM | 内存到内存模式使能 |
| [17:16] | TCIE | 传输完成中断使能 |
我个人觉得,CR 寄存器里最容易出问题的是 PSIZE 和 MSIZE 的匹配。如果外设数据宽度是 8 位,内存数据宽度是 32 位,DMA 会自动做数据打包/解包。但前提是地址对齐要正确。否则,你会看到数据错位或者丢失。
还有一个容易被忽略的位:MEM2MEM。这个位使能后,DMA 可以在两个内存地址之间传输数据,不需要外设触发。这在做内存拷贝、缓冲区处理时非常有用。但要注意,内存到内存模式会占用总线带宽,如果系统中有实时性要求高的外设,要谨慎使用。
4.5 寄存器配置的 Rust 实践
在 Rust 中,我们通常用 volatile_register 或者 svd2rust 生成的 PAC 库来操作这些寄存器。下面是一个典型的配置示例:
// 使用 stm32f4xx-hal 库配置 DMA1 通道 0
let dma = dp.DMA1.constrain();
let mut channel = dma.split().0; // 通道 0
// 配置源地址:内存缓冲区
let src_addr = buffer.as_ptr() as u32;
// 配置目的地址:USART 数据寄存器
let dst_addr = 0x40011004; // USART1_DR
// 配置控制寄存器
channel.cr().modify(|_, w| {
w.dir().memory_to_peripheral() // 内存到外设
.psize().bits(0b10) // 外设数据宽度 32 位
.msize().bits(0b10) // 内存数据宽度 32 位
.pl().high() // 高优先级
.tcie().enabled() // 使能传输完成中断
});
// 配置传输计数
channel.cndtr().write(|w| w.ndt().bits(256)); // 传输 256 个数据项
// 配置源地址和目的地址
channel.sar().write(|w| w.sa().bits(src_addr));
channel.dar().write(|w| w.da().bits(dst_addr));
// 使能通道
channel.cr().modify(|_, w| w.en().enabled());
这段代码看起来简单,但有几个细节要注意:
- 配置顺序很重要:先配 CR 的配置位,再配 CNDTR,最后配 SAR/DAR。我习惯最后使能 EN 位,避免在配置过程中 DMA 意外启动。
- 地址要确保有效:外设地址必须从数据手册确认,内存地址要保证缓冲区在 DMA 可访问的区域。
- 中断处理:使能 TCIE 后,记得在中断服务函数中清除标志位,否则会一直触发中断。
4.6 四个寄存器的协作关系
说了这么多,这四个寄存器到底怎么配合工作的?我画了一张图,帮你理清思路。
从这张图可以看出,四个寄存器各司其职:SAR 和 DAR 负责地址,CNDTR 负责计数,CR 负责控制。DMA 传输引擎从 SAR 指向的地址读取数据,写入 DAR 指向的地址,每完成一次传输,CNDTR 减 1,直到减为 0 触发完成中断。
总结一下四个寄存器的配置口诀:
- SAR/DAR:地址要对,方向要配
- CNDTR:数量要准,传输中别改
- CR:宽度匹配,优先级合理