第4章:DMA传输方向与数据宽度——外设到内存、内存到外设、内存到内存;字节、半字、字对齐规则

DMA传输,说白了就是数据搬运。但往哪搬、搬多大、怎么对齐,这三个问题搞不清楚,你的DMA性能就永远上不去。我见过太多工程师在数据宽度上栽跟头,今天咱们就把这块硬骨头啃下来。

4.1 三种传输方向:谁跟谁说话?

STM32F407的DMA支持三种传输方向,每种都有它的脾气。我习惯把它们分成两类:一类是外设参与的,一类是纯内存的。

传输方向 源地址 目标地址 典型场景
外设→内存 外设数据寄存器 SRAM变量/数组 ADC采集、USART接收、SPI接收
内存→外设 SRAM变量/数组 外设数据寄存器 DAC输出、USART发送、SPI发送
内存→内存 SRAM地址A SRAM地址B 数据拷贝、缓冲区搬移、图像处理

4.2 外设到内存:最常用的模式

这个模式我用的最多。ADC采集、串口接收,全是它。核心逻辑是:外设每产生一个数据,DMA就自动把它搬到内存里。

关键配置:外设地址固定,内存地址递增。外设寄存器就一个地址,每次读同一个位置;内存数组需要地址自动加1,才能把数据依次存进去。

// 外设到内存的典型配置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  // 外设是源
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  // 外设地址不增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  // 内存地址递增

嗯,这里要注意:外设地址递增一定要关掉。我刚开始学的时候犯过这个错,结果数据全写到同一个地址去了,读出来的全是最后一个值。

4.3 内存到外设:发送数据的正确姿势

这个方向跟上面正好反过来。内存地址递增,外设地址固定。发送数据时,DMA从数组里依次取出数据,写到外设的发送寄存器。

个人经验:用DMA发送数据时,记得先检查外设的发送完成标志。我曾经在USART还在发送时就修改了缓冲区数据,结果发出去的全是乱码。建议用DMA传输完成中断来同步。

// 内存到外设的典型配置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;  // 外设是目标
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

4.4 内存到内存:最灵活也最坑

内存到内存传输,说白了就是DMA帮你做memcpy。但这里有个大坑——必须使用循环模式。为什么?因为内存到内存没有外设触发,DMA传输完一次就停了,必须靠循环模式让它重新开始。

避坑指南:我曾经在内存到内存传输中忘了开循环模式,结果DMA只搬了一次数据就罢工了。调试了半天才发现是这个问题。另外,内存到内存传输会占用总线带宽,CPU访问内存会变慢,别在实时性要求高的地方用。

// 内存到内存的典型配置
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToMemory;  // 内存到内存
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  // 必须循环模式
DMA_InitStructure.DMA_PeripheralInc = DMA_MemoryInc_Enable;  // 源地址递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  // 目标地址递增

4.5 数据宽度:字节、半字、字

数据宽度决定了DMA一次搬多少数据。STM32F407支持三种宽度:字节(8位)、半字(16位)、字(32位)。

数据宽度 位数 典型外设 内存对齐要求
字节(Byte) 8位 USART、I2C 无要求
半字(Half Word) 16位 定时器、SPI(16位模式) 2字节对齐
字(Word) 32位 ADC(12位可扩展)、DAC 4字节对齐

4.6 对齐规则:不遵守就等着崩溃

对齐规则是DMA传输中最容易出问题的地方。说白了就是:源地址、目标地址、数据宽度三者必须匹配。

核心规则:

  • 字节传输:地址可以是任意值
  • 半字传输:地址必须是2的倍数
  • 字传输:地址必须是4的倍数

为什么会这样?因为STM32F407的AHB总线是32位的,访问非对齐地址会触发硬件错误。我有个项目就是因为在字传输时用了奇数地址,结果程序跑飞了,查了两天才找到原因。

// 正确的对齐方式
uint8_t byte_buffer[100];      // 字节数组,任意地址
uint16_t halfword_buffer[50] __attribute__((aligned(2)));  // 半字数组,2字节对齐
uint32_t word_buffer[25] __attribute__((aligned(4)));      // 字数组,4字节对齐

4.7 源与目标宽度不一致怎么办?

有时候外设是8位的,但你想用32位传输提高效率。DMA支持源和目标宽度不一致,但有个限制:传输次数必须匹配。

举个例子:外设是8位,内存是32位。DMA会每4次外设读取,组合成1次内存写入。反过来,外设是32位,内存是8位,DMA会1次外设读取,拆成4次内存写入。

我建议:尽量保持源和目标宽度一致。宽度不一致时,DMA内部会做打包/拆包操作,性能会下降。除非你特别需要节省传输次数,否则别这么干。

// 源8位,目标32位
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  // 外设8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;  // 内存32位
// 传输次数必须是4的倍数,因为4个字节组成1个字

4.8 知识结构图

下面这张图把本章的核心知识点串起来了,你看看就明白了。

DMA传输方向与数据宽度知识结构 传输方向 外设 → 内存 内存 → 外设 内存 → 内存 数据宽度 字节(8位) 半字(16位) 字(32位) 对齐规则 字节:任意地址 半字:2字节对齐 字:4字节对齐 核心要点 1. 外设到内存:外设地址固定,内存地址递增 2. 内存到外设:内存地址递增,外设地址固定 3. 内存到内存:必须使用循环模式,注意总线占用 4. 对齐规则:源地址、目标地址、数据宽度必须匹配

4.9 实战建议

说了这么多,总结几条我自己的经验:

  • 先定方向,再定宽度:传输方向决定了地址递增策略,数据宽度决定了对齐要求。顺序别搞反。
  • 对齐检查不能省:每次配置DMA前,用调试器看看源地址和目标地址是不是对齐的。我吃过亏,所以现在都养成习惯了。
  • 宽度一致性能最好:除非有特殊需求,否则让外设和内存的数据宽度保持一致。不一致时DMA内部要做转换,效率会打折扣。
  • 内存到内存慎用:这个模式虽然方便,但会占用总线。如果你的系统对实时性要求高,还是用CPU做memcpy吧。

最后说一句:DMA配置看起来简单,但细节决定成败。对齐、宽度、方向,这三个参数配好了,DMA就能跑得又快又稳。配不好,轻则数据错乱,重则系统崩溃。多花点时间在这上面,值得。