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时,我建议你注意两点:

  1. 计数值是数据单元个数,不是字节数。比如你配置32位传输,NDTR=100,表示传输100个32位数据,总共400字节。
  2. 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 四个寄存器的协同工作流程

光知道每个寄存器是干什么的还不够,你得理解它们怎么配合。下面这张图是我自己画的,帮你理清思路:

DMA四个核心寄存器协同工作流程图 DMA四个核心寄存器协同工作流程 SAR 源地址寄存器 DAR 目标地址寄存器 NDTR 传输计数寄存器 CR 控制寄存器 数据流向 计数控制 配置地址模式 配置地址模式 配置传输宽度 工作流程说明: 1. 先配置CR,设定传输方向、地址模式、数据宽度等规则 2. 设置SAR和DAR,告诉DMA从哪读到哪写 3. 设置NDTR,告诉DMA要搬多少个数据单元 4. 最后置位CR的EN位,启动DMA传输 5. 传输过程中NDTR递减,减到0时触发完成中断

配置顺序很重要。我习惯的步骤是:先配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正在传输,你突然改了配置,行为不可预测。
  • 地址递增方向搞错:从外设到内存时,通常是外设地址固定、内存地址递增。反过来从内存到外设时,是内存地址递增、外设地址固定。别搞混了。