4. DMA传输的关键参数:源地址、目的地址、传输长度、流控制

DMA配置,说穿了就是把这四个参数填对。源地址、目的地址、传输长度、流控制。别看它少,每一个都有坑。我刚开始做DMA驱动的时候,光一个地址递增模式就让我折腾了两天。

4.1 源地址与目的地址

这两个地址决定了数据从哪来,到哪去。听着简单,但你得搞清楚DMA控制器的寻址能力。是32位还是64位?源和目的在不在同一个地址映射窗口?

我个人习惯先画一张地址映射图,把外设地址、内存地址、Flash地址都标出来。然后才去配置寄存器。这样做的好处是,一眼就能看出地址范围是否合法。

避坑指南:我曾经遇到过源地址配置在外设寄存器区,但DMA控制器不支持地址跨越。结果数据读回来全是乱的。后来我用了地址掩码寄存器,强制让DMA只访问合法的地址区间。

地址还有两个关键属性:递增还是固定。

  • 源地址递增:典型场景就是从内存搬运一块数据到外设FIFO。内存地址一条条往后走,外设地址不动。
  • 源地址固定:从外设数据寄存器连续读取。比如ADC多通道采样,每个通道数据都在同一个地址。
  • 目的地址递增/固定:同理,写到内存的缓冲区就递增,写到外设的控制寄存器就固定。

我再加一条经验:地址边界对齐问题。有些DMA要求源地址和目的地址都按32位对齐。如果你的数据没对齐……嗯,那就等着硬件错误吧。

注意:不是所有MCU都支持非对齐DMA传输。查数据手册时,确认“Misaligned transfer support”这一项。如果不支持,要么手动对齐,要么用字节拆分传输。

4.2 传输长度

传输长度,说白了就是你要搬多少数据。以字节为单位,还是以字为单位?这里面有个“传输宽度”的概念。

传输宽度 长度单位 典型场景
8-bit(字节) 1字节 串口、I2C、SPI单字节收发
16-bit(半字) 2字节 音频数据、传感器数据
32-bit(字) 4字节 高速ADC、帧缓冲区、外部存储器

你想想看,如果你的外设寄存器是32位的,但你配置了8位传输,那一次DMA只能搬1个字节,效率低得可怜。反过来,外设只支持8位,你非要用32位,数据就错位了。

传输长度还有一个隐含条件:总长度 ÷ 传输宽度 = 传输次数。这个次数必须小于DMA控制器的最大计数范围。我记得某款低端MCU的DMA计数器只有16位,最大65535次。如果你用32位宽度,那最大只能搬4×65535=262140字节,约256KB。超过这个数就得拆成多个DMA块传输。

实战经验:我做过一个图像采集项目,一帧数据是1024×768像素×2字节=1.5MB。DMA计数器只有16位,用32位宽度也搬不完。我的方案是:把一帧拆成6个DMA传输,每个传输搬256KB。靠DMA的自动链接功能,帧开始中断后依次启动6个块。

4.3 流控制

流控制,控制的是DMA传输的节奏。谁来决定下一次传输什么时候开始?

主要有两种模式:

  • 硬件流控:外设发一个请求信号,DMA搬一个单元。串口的RTS/CTS就是典型的硬件流控。你配置好DMA后,它就一直等外设的请求。外设慢了,DMA就停;外设快了,DMA就忙。
  • 软件流控:CPU通过写寄存器或中断来触发DMA。适合批量传输,比如从SD卡读一块数据到内存。你CPU先把卡初始化好,然后写一下DMA触发位,它就去搬了。

我个人更倾向硬件流控用于数据流场景。为什么呢?因为它不占用CPU。外设一有数据,DMA直接拿走。你去处理其他任务,不用管字节级别的事。

但硬件流控有个前提:DMA控制器要能正确识别外设的请求信号。信号电平、极性、脉冲宽度,一个不对,DMA就罢工。我遇到过SPI从设备拉低CS线后,DMA以为有请求,但其实是断开的,结果读了一堆无效数据。

关键点:流控制模式必须与外设的工作模式匹配。外设是主控设备,它发起请求,DMA响应;外设是从设备,那DMA不能主动拉数据。搞清楚谁控制谁。

4.4 四个参数的协同

这四个参数不是孤立的。我给你一个我常用的配置检查表:

  1. 地址范围:源和目的的所有地址都在DMA可访问的存储区域内吗?超出就直接报错。
  2. 地址递增方式:外设是递增还是固定?内存是递增还是固定?如果外设FIFO深度是4,但你想用单次传输,地址固定是对的。
  3. 传输长度与宽度:总长度能被传输宽度整除吗?不能的话,最后会留下一个尾巴字节,可能错位。
  4. 流控制与传输次数:硬件流控下,外设每请求一次,DMA搬一个宽度单元。总次数必须等于配置的传输次数,多一个少一个都不行。

举个例子。你要做UART DMA发送:

  • 源地址:内存中的发送缓冲区(地址递增)
  • 目的地址:UART数据寄存器(地址固定)
  • 传输宽度:8-bit(UART是字节设备)
  • 传输长度:缓冲区长度(单位是字节)
  • 流控制:硬件流控,UART发送缓冲区空时就发请求
// 伪代码示例
DMA_TypeDef *dma = DMA1;

// 配置源地址:内存缓冲区,递增
dma->Cx->CMAR = (uint32_t)uart_tx_buffer;
dma->Cx->CNDTR = buffer_len;       // 传输长度

// 配置目的地址:UART数据寄存器,固定
dma->Cx->CPAR = (uint32_t)&USART1->DR;

// 传输宽度:8-bit
dma->Cx->CCR |= DMA_CCR_MSIZE_8BIT | DMA_CCR_PSIZE_8BIT;

// 流控制:外设请求
dma->Cx->CCR |= DMA_CCR_DIR_M2P;   // 内存到外设
dma->Cx->CCR |= DMA_CCR_CIRC;      // 循环模式,用于连续发送
dma->Cx->CCR |= DMA_CCR_PL_VERY_HIGH;
dma->Cx->CCR |= DMA_CCR_ENABLE;    // 使能DMA通道

这个例子看着简单,但我告诉你,初学者最容易犯的错是:忘记把目的地址配成固定模式。UART数据寄存器只有一个,你的DMA每写一次,它自动从寄存器发出去了。如果地址递增,下次就写到相邻的寄存器,轻则数据错,重则硬件崩溃。

DMA传输四大参数协同关系图 DMA传输四大参数协同关系 DMA 控制器 传输引擎 + 计数器 源地址 递增 / 固定 内存 / 外设 / Flash 目的地址 递增 / 固定 内存 / 外设 / 缓冲区 传输长度 总字节 / 传输次数 宽度 + 计数器 流控制 硬件流控 / 软件流控 请求信号 / 触发位 四个参数必须同时匹配,缺一不可,错一个就全盘乱。

有了这张图,你每次配置DMA都走一遍它的逻辑。源地址、目的地址、长度、流控制,四个参数串起来,形成一条完整的数据路径。哪个不对,数据就卡在哪。我在团队培训的时候,让大家先手画这个图再写代码,效果比直接调库好很多。

核心总结:DMA配置的关键在于四个参数的配合。一个错误,轻则数据错乱,重则死机。记住:源地址告诉数据从哪里来,目的地址告诉数据到哪里去,传输长度告诉搬多少,流控制告诉什么时候搬。配好这四个,DMA就稳了。