DMA核心寄存器详解:源地址、目标地址、传输计数与控制寄存器
各位同学,今天咱们来啃DMA最硬核的部分——那四个核心寄存器。说实话,我刚接触DMA时,看着数据手册里那一堆寄存器描述,头都是大的。但后来我发现,只要你把这四个寄存器搞明白了,DMA配置就掌握了八成。
DMA传输的本质是什么?说白了就是一句话:从哪来,到哪去,传多少,怎么传。这四个问题,正好对应四个核心寄存器。
核心寄存器速览表
| 寄存器名称 | 英文缩写 | 功能一句话 |
|---|---|---|
| 源地址寄存器 | SAR (Source Address Register) | 告诉DMA从哪个地址拿数据 |
| 目标地址寄存器 | DAR (Destination Address Register) | 告诉DMA把数据放到哪个地址 |
| 传输计数寄存器 | NDTR (Number of Data Transfer Register) | 告诉DMA要搬多少数据 |
| 控制寄存器 | CR (Control Register) | 告诉DMA怎么搬、什么时候搬 |
嗯,咱们一个一个来拆解。
2.1 源地址寄存器(SAR)——数据从哪来
SAR寄存器存放的是DMA读取数据的起始地址。这个地址可以是外设寄存器的地址,也可以是内存数组的地址。我习惯把它想象成「水龙头」——数据从这儿流出来。
配置时有个关键点:地址对齐。比如你配置的是32位传输,源地址必须是4字节对齐的。我曾经在一个项目里踩过这个坑——地址没对齐,结果DMA搬出来的数据全是乱的,排查了整整一下午。
我的经验:配置SAR时,建议先用宏定义把地址值算好,不要手写硬编码。比如:
#define ADC1_DR_ADDR ((uint32_t)&(ADC1->DR))
#define SRC_BUF_ADDR ((uint32_t)src_buffer)
DMA1->SAR = ADC1_DR_ADDR; // 从ADC数据寄存器读
// 或者
DMA1->SAR = SRC_BUF_ADDR; // 从内存数组读
SAR还有一个重要特性——地址更新模式。你可以配置传输完成后地址是否自增、自增多少。这个在控制寄存器里设置,但直接影响SAR的行为。
- 固定模式:地址不变,适合从同一个外设寄存器反复读取
- 递增模式:每次传输后地址+1/+2/+4,适合搬运连续内存块
2.2 目标地址寄存器(DAR)——数据到哪去
DAR和SAR是对称的。它存放的是数据要写往的地址。你可以把它想象成「水桶」——数据流到这里。
配置DAR时,我通常会注意一个细节:源和目标是否在同一个地址空间。比如从外设到内存,或者从内存到内存,甚至从外设到外设。不同场景下,DAR的配置策略不一样。
注意:有些MCU的DMA不支持内存到内存传输,比如早期的STM32F1系列。如果你非要用,DMA会直接罢工。我当年就被这个坑过——代码写好了,仿真一看,传输计数纹丝不动。
DAR同样支持固定和递增两种模式。举个例子,如果你要把ADC采集的数据存到一个数组里,DAR就要配置成递增模式:
#define DST_BUF_ADDR ((uint32_t)dst_buffer)
DMA1->DAR = DST_BUF_ADDR; // 写到内存数组
// 配置递增模式(在CR中设置)
DMA1->CR |= DMA_CR_DINC; // 目标地址递增
2.3 传输计数寄存器(NDTR)——搬多少数据
NDTR,全称是Number of Data Transfer Register。它告诉DMA:这次任务要搬多少个数据单元。
这个寄存器有个特点:它是递减的。每传输一个数据单元,NDTR自动减1。当减到0时,DMA传输完成,可以触发中断。
配置NDTR时,我建议你注意两点:
- 计数值是数据单元个数,不是字节数。比如你配置32位传输,NDTR=100,表示传输100个32位数据,总共400字节。
- NDTR在传输过程中是只读的。如果你想中途修改传输长度,得先停掉DMA。
典型配置示例:
#define TRANSFER_LEN 256 // 传输256个数据单元
DMA1->NDTR = TRANSFER_LEN; // 设置传输计数
// 启动DMA后,NDTR会从256递减到0
你想想看,如果NDTR配置成0会怎样?DMA会认为没有数据要传输,直接完成。嗯,这个坑我也踩过——初始化时忘了给NDTR赋值,结果DMA一启动就立刻触发完成中断,数据根本没搬。
2.4 控制寄存器(CR)——怎么搬、什么时候搬
CR是四个寄存器里最复杂的,它决定了DMA的工作模式。我把它比作「总指挥」——所有传输规则都在这里定义。
CR里常见的配置位包括:
| 配置位 | 作用 | 我的建议 |
|---|---|---|
| EN (使能位) | 启动/停止DMA传输 | 最后再置1,所有配置就绪后再启动 |
| DIR (传输方向) | 外设→内存 / 内存→外设 / 内存→内存 | 根据实际场景选择,别搞反了 |
| CIRC (循环模式) | 传输完成后自动重新开始 | 适合ADC连续采样、定时器触发等场景 |
| PINC/MINC (地址递增) | 外设/内存地址是否自增 | 外设地址通常固定,内存地址通常递增 |
| PSIZE/MSIZE (数据宽度) | 8位/16位/32位 | 源和目标宽度可以不同,DMA会自动对齐 |
| TCIE (传输完成中断) | 传输完成时触发中断 | 建议开启,方便做后续处理 |
我个人习惯把CR的配置写成结构体或者宏,这样可读性更好:
// 配置DMA控制寄存器
DMA1->CR = 0;
DMA1->CR |= DMA_CR_DIR_P2M; // 外设到内存
DMA1->CR |= DMA_CR_CIRC; // 循环模式
DMA1->CR |= DMA_CR_MINC; // 内存地址递增
DMA1->CR |= DMA_CR_PSIZE_32; // 外设数据宽度32位
DMA1->CR |= DMA_CR_MSIZE_32; // 内存数据宽度32位
DMA1->CR |= DMA_CR_TCIE; // 开启传输完成中断
2.5 四个寄存器的协同工作流程
光知道每个寄存器是干什么的还不够,你得理解它们怎么配合。下面这张图是我自己画的,帮你理清思路:
配置顺序很重要。我习惯的步骤是:先配CR,再配SAR和DAR,最后配NDTR。为什么?因为CR里定义了数据宽度和地址模式,这些会影响你对SAR和DAR的理解。而NDTR是最后才设置的,设置完就可以启动传输了。
避坑指南:我曾经在一个项目中,先配了NDTR再配CR,结果CR里的数据宽度配置和NDTR的计数值不匹配——NDTR是按32位算的,但CR里配成了16位。DMA搬出来的数据全对不上。从那以后,我都是严格按照「CR → SAR/DAR → NDTR」的顺序来。
2.6 寄存器配置的常见陷阱
最后,我总结几个实际项目中容易踩的坑:
- SAR和DAR搞反了:特别是从外设到内存的传输,有人会把外设地址配到DAR里。结果数据从内存往外设写,外设寄存器被覆盖,系统直接死机。
- NDTR忘记重新赋值:循环模式下,每次传输完成NDTR都会归零。如果你在中断里想重新启动传输,记得重新写NDTR。
- CR的EN位操作顺序:修改CR的其他位之前,一定要先清EN位。否则DMA正在传输,你突然改了配置,行为不可预测。
- 地址递增方向搞错:从外设到内存时,通常是外设地址固定、内存地址递增。反过来从内存到外设时,是内存地址递增、外设地址固定。别搞混了。