4、DMA控制器寄存器详解:源地址、目的地址、传输计数与控制寄存器

各位同学,今天我们来啃 DMA 控制器最核心的四个寄存器。说实话,我刚接触 DMA 时,面对数据手册里那一大堆寄存器描述,头都是大的。但后来我发现,只要你把源地址、目的地址、传输计数和控制这四个寄存器搞明白,DMA 的基本操作就通了八成。

咱们一个一个来拆解。我会结合 Rust 的寄存器操作风格,把每个寄存器的位域定义、配置要点和实际坑点都讲清楚。

4.1 源地址寄存器(SAR)

源地址寄存器,顾名思义,就是存放数据来源的地址。它告诉 DMA 控制器:「嘿,数据从这里搬走。」

在大多数 MCU 中,SAR 是一个 32 位寄存器,支持字节、半字、字三种对齐方式。我个人的习惯是:能用字对齐就用字对齐,因为非对齐访问会触发总线错误,或者导致性能下降。

位域 名称 描述
[31:0] SA 源地址,支持递增、递减、固定三种模式

这里有个关键点:源地址的递增模式。如果你从内存搬运数据到外设,源地址通常要递增;如果从外设搬运到内存,源地址往往固定(比如 ADC 的数据寄存器)。

我的经验: 在配置 SAR 时,一定要确认外设的地址是否支持 DMA 访问。我曾经在 STM32F4 上踩过坑——某个外设的寄存器地址在 AHB 总线上,但 DMA 只能访问 AHB 和 APB 的特定区域。查了两天数据手册才发现问题。

4.2 目的地址寄存器(DAR)

DAR 和 SAR 是对称的。它存放数据要搬到的目标地址。配置逻辑和 SAR 几乎一样,只是方向相反。

位域 名称 描述
[31:0] DA 目的地址,同样支持递增、递减、固定

嗯,这里要注意一个细节:SAR 和 DAR 的地址递增方向必须和传输方向匹配。比如从内存到外设,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 会自动做数据打包/解包。但前提是地址对齐要正确。否则,你会看到数据错位或者丢失。

我的建议: 除非有特殊需求,否则尽量让 PSIZE 和 MSIZE 保持一致。这样既简单又安全。我在做音频 DMA 传输时,就统一用 16 位宽度,省去了对齐的烦恼。

还有一个容易被忽略的位: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 四个寄存器的协作关系

说了这么多,这四个寄存器到底怎么配合工作的?我画了一张图,帮你理清思路。

DMA 四个核心寄存器协作流程图 DMA 四个核心寄存器协作流程 源地址寄存器 SAR 目的地址寄存器 DAR 传输计数寄存器 CNDTR 控制寄存器 CR 提供源地址 提供目的地址 递减计数 配置方向/宽度/优先级 DMA 传输引擎 协作流程说明: 1. CR 配置传输方向、数据宽度、优先级等参数 2. CNDTR 设置要传输的数据项数量 3. SAR 和 DAR 提供源地址和目的地址 4. 使能后,DMA 引擎从 SAR 读数据,写入 DAR,每完成一次 CNDTR 减 1

从这张图可以看出,四个寄存器各司其职:SAR 和 DAR 负责地址,CNDTR 负责计数,CR 负责控制。DMA 传输引擎从 SAR 指向的地址读取数据,写入 DAR 指向的地址,每完成一次传输,CNDTR 减 1,直到减为 0 触发完成中断。

总结一下四个寄存器的配置口诀:

  • SAR/DAR:地址要对,方向要配
  • CNDTR:数量要准,传输中别改
  • CR:宽度匹配,优先级合理