第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 知识结构图
下面这张图把本章的核心知识点串起来了,你看看就明白了。
4.9 实战建议
说了这么多,总结几条我自己的经验:
- 先定方向,再定宽度:传输方向决定了地址递增策略,数据宽度决定了对齐要求。顺序别搞反。
- 对齐检查不能省:每次配置DMA前,用调试器看看源地址和目标地址是不是对齐的。我吃过亏,所以现在都养成习惯了。
- 宽度一致性能最好:除非有特殊需求,否则让外设和内存的数据宽度保持一致。不一致时DMA内部要做转换,效率会打折扣。
- 内存到内存慎用:这个模式虽然方便,但会占用总线。如果你的系统对实时性要求高,还是用CPU做memcpy吧。
最后说一句:DMA配置看起来简单,但细节决定成败。对齐、宽度、方向,这三个参数配好了,DMA就能跑得又快又稳。配不好,轻则数据错乱,重则系统崩溃。多花点时间在这上面,值得。