4. DMA传输的关键参数:源地址、目的地址、传输长度、流控制
DMA配置,说穿了就是把这四个参数填对。源地址、目的地址、传输长度、流控制。别看它少,每一个都有坑。我刚开始做DMA驱动的时候,光一个地址递增模式就让我折腾了两天。
4.1 源地址与目的地址
这两个地址决定了数据从哪来,到哪去。听着简单,但你得搞清楚DMA控制器的寻址能力。是32位还是64位?源和目的在不在同一个地址映射窗口?
我个人习惯先画一张地址映射图,把外设地址、内存地址、Flash地址都标出来。然后才去配置寄存器。这样做的好处是,一眼就能看出地址范围是否合法。
地址还有两个关键属性:递增还是固定。
- 源地址递增:典型场景就是从内存搬运一块数据到外设FIFO。内存地址一条条往后走,外设地址不动。
- 源地址固定:从外设数据寄存器连续读取。比如ADC多通道采样,每个通道数据都在同一个地址。
- 目的地址递增/固定:同理,写到内存的缓冲区就递增,写到外设的控制寄存器就固定。
我再加一条经验:地址边界对齐问题。有些DMA要求源地址和目的地址都按32位对齐。如果你的数据没对齐……嗯,那就等着硬件错误吧。
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块传输。
4.3 流控制
流控制,控制的是DMA传输的节奏。谁来决定下一次传输什么时候开始?
主要有两种模式:
- 硬件流控:外设发一个请求信号,DMA搬一个单元。串口的RTS/CTS就是典型的硬件流控。你配置好DMA后,它就一直等外设的请求。外设慢了,DMA就停;外设快了,DMA就忙。
- 软件流控:CPU通过写寄存器或中断来触发DMA。适合批量传输,比如从SD卡读一块数据到内存。你CPU先把卡初始化好,然后写一下DMA触发位,它就去搬了。
我个人更倾向硬件流控用于数据流场景。为什么呢?因为它不占用CPU。外设一有数据,DMA直接拿走。你去处理其他任务,不用管字节级别的事。
但硬件流控有个前提:DMA控制器要能正确识别外设的请求信号。信号电平、极性、脉冲宽度,一个不对,DMA就罢工。我遇到过SPI从设备拉低CS线后,DMA以为有请求,但其实是断开的,结果读了一堆无效数据。
4.4 四个参数的协同
这四个参数不是孤立的。我给你一个我常用的配置检查表:
- 地址范围:源和目的的所有地址都在DMA可访问的存储区域内吗?超出就直接报错。
- 地址递增方式:外设是递增还是固定?内存是递增还是固定?如果外设FIFO深度是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都走一遍它的逻辑。源地址、目的地址、长度、流控制,四个参数串起来,形成一条完整的数据路径。哪个不对,数据就卡在哪。我在团队培训的时候,让大家先手画这个图再写代码,效果比直接调库好很多。