3、DMA 传输基本要素:源地址、目的地址、传输计数、传输宽度
做 DMA 配置,说白了就是跟四个数字打交道:
- 源地址 —— 数据从哪来
- 目的地址 —— 数据送到哪去
- 传输计数 —— 总共搬多少笔
- 传输宽度 —— 每笔搬多宽的数据
这四个要素拧在一起,就构成了 DMA 传输的基本骨架。我记得刚接手第一个 DMA 驱动时,觉得寄存器就那么几个,填就是了。结果数据搬出来全是乱的。嗯,后来才发现,地址递增方向和宽度没配好。
3.1 源地址与目的地址
我做嵌入式这几年的经验是,地址配置最容易出问题的地方,不是地址本身写错,而是递增模式搞反了。
大多数 DMA 控制器支持三种模式:
- 固定地址:每次传输后地址不变,适合访问 FIFO 或外设数据寄存器
- 递增地址:每次传输后地址增加(增加量 = 传输宽度),适合搬运内存块
- 环形地址:地址到达边界后自动回绕,适合循环缓冲
我曾经调试过一个音频播放项目,DMA 从内存搬数据到 I2S 发送 FIFO。内存地址设成了递增,没问题;目的地址也设成了递增……结果 FIFO 的地址递增后就写到别处去了,播放声音全是噼啪噪声。所以外设目标地址通常要设成固定模式。
3.2 传输计数
传输计数,就是 DMA 要搬多少次。注意,不是搬多少字节,而是搬多少笔。每次搬多少字节由传输宽度决定。
举个例子:你想搬 100 个 32 位整数(400 字节)。如果传输宽度设成 32 位(4 字节),那么计数就是 100。如果宽度设成 8 位(1 字节),计数就得是 400。
大多数 DMA 控制器的计数寄存器宽度是有限的,比如 16 位(最大 65535)或 24 位(最大 16777215)。
| 传输宽度 | 计数=100 时总字节数 | 适用场景 |
|---|---|---|
| 8 位 (1 字节) | 100 字节 | 字节流、UART 传输 |
| 16 位 (2 字节) | 200 字节 | 16 位音频、SPI 半字交换 |
| 32 位 (4 字节) | 400 字节 | 内存复制、缓冲区搬运 |
| 64 位 (8 字节) | 800 字节 | 高性能数据通道(Cortex‑M7 等) |
3.3 传输宽度
传输宽度决定了 DMA 每次搬运多少字节。常见宽度有:
- 字节 (8 位) —— 适合串口、SPI 等逐字节外设
- 半字 (16 位) —— 适合 16 位外设或音频采样
- 字 (32 位) —— 最适合内存到内存的搬运,效率最高
- 双字 (64 位) —— 部分高端 MCU 才支持
宽度选不对,轻则性能下降,重则数据错位。原因是对齐问题——如果源地址是 0x20001001(非字对齐),但你设成 32 位宽度,某些 DMA 会产生 bus error 或者硬件阻塞。
我有个习惯:只要是内存间的 DMA,优先用 32 位宽度,除非芯片明确不支持。但外设侧,一定要看外设数据寄存器的实际位宽。比如 USART 数据寄存器通常是 8 位,你设个 32 位读它,可能会读到无关的相邻字节。
3.4 四个要素的协同配置
下面给一个实际配置的例子(伪代码风格),假设我们要从内存 buf 搬 256 个字到 SPI 数据寄存器:
DMA_InitTypeDef dma_cfg;
dma_cfg.src_addr = (uint32_t)buf; // 内存源
dma_cfg.src_inc = DMA_INC_ENABLE; // 源地址递增(每次+4)
dma_cfg.dst_addr = (uint32_t)&spi->DR; // SPI 数据寄存器
dma_cfg.dst_inc = DMA_INC_DISABLE; // 目的地址固定
dma_cfg.transfer_count = 256; // 搬 256 次
dma_cfg.transfer_width = DMA_WIDTH_32BIT; // 每次搬 4 字节
DMA_Config(&dma_cfg);
注意,这个例子中传输计数 256,宽度 32 位,总共 1024 字节 —— 完全对应 buf 的大小。如果计数写错成 1024,宽度不变,那 DMA 就会把内存后面无关的数据也搬出去。
你可能会问:为什么 DMA 不直接按字节计数,而是搞出一个宽度来?
原因很简单:效率。硬件一次搬 32 位比搬 4 次 8 位快得多。而且在总线设计上,宽度固定可以减少地址线上的冗余计算。所以 DMA 把「一次搬多少」和「总共搬多少次」分开,让你灵活匹配不同外设和内存对宽度的要求。
3.5 我个人总结的配置顺序
- 确定传输宽度 —— 看源和目的两端中较小的位宽(保证不会读/写多出数据)。
- 计算传输计数 —— 总字节数 ÷ 宽度,注意整除。
- 写源地址 —— 起始地址,选择递增或固定。
- 写目的地址 —— 起始地址,选择递增或固定。
- 检查对齐 —— 地址是否按宽度对齐?不对齐要降宽度或者用字节模式。
这个顺序我用过不下几十次,没出过大错。你也可以把它贴在电脑旁,省得每次都要重新想。