DMA控制器架构:内部结构、通道与请求、地址与传输宽度
DMA控制器,说白了就是CPU的「数据搬运工」。CPU告诉它「把这里的数据搬到那里」,它就能自己干完,不用CPU一趟趟跑腿。我刚开始接触DMA时,总觉得它很神秘,后来拆开看了内部结构才发现——其实没那么复杂。
DMA控制器的内部结构
一个典型的DMA控制器,内部由几个核心模块组成。我画了张图,你先有个整体印象。
你看,DMA内部其实就这几大块。控制逻辑是大脑,通道仲裁是交警,地址生成器是导航,传输引擎是变速箱。每个模块各司其职。
通道与请求机制
DMA通道,你可以理解成「专用车道」。每个外设——比如UART、SPI、ADC——都有自己的专用通道。为什么需要多个通道?因为外设们不会排队等,它们随时可能要求传输数据。
关键点:通道数量决定了DMA能同时服务多少个外设。STM32F4系列有8个DMA通道,而某些高端MCU有16个甚至更多。
请求机制是这样的:外设发出DMA请求信号 → DMA控制器收到后,检查该通道是否使能 → 如果使能,就启动传输。我遇到过一个问题:两个外设同时请求DMA,结果数据乱了。后来发现是优先级没配好。
| 通道特性 | 说明 | 我踩过的坑 |
|---|---|---|
| 通道优先级 | 硬件固定优先级或可编程优先级 | 高优先级通道会抢占低优先级,导致低优先级外设超时 |
| 请求源映射 | 每个通道可以绑定到特定外设请求 | 映射错了,DMA搬的数据全是错的 |
| 自动重载 | 传输完成后自动重新开始 | 循环模式没关,数据覆盖了缓冲区 |
⚠️ 避坑指南:我曾经在一个项目中,把UART的DMA请求映射到了SPI的通道上。结果UART发数据时,SPI莫名其妙开始传输。查了两天才发现是请求映射配置错了。所以,每次配置完一定要核对通道与外设的对应关系。
源地址与目的地址
DMA传输的核心就是「从哪来,到哪去」。源地址和目的地址决定了数据流动的方向。常见的场景有:
- 外设→内存:比如ADC采集数据存入RAM
- 内存→外设:比如从RAM发送数据到UART
- 内存→内存:比如把一个大数组从一个地址复制到另一个地址
地址模式有两种:固定地址和递增地址。外设的地址通常是固定的——比如UART的数据寄存器地址不会变。而内存地址需要递增,否则数据会覆盖同一个位置。
💡 我的习惯:配置源地址和目的地址时,我会先确认哪个是固定地址,哪个是递增地址。比如外设到内存:源地址固定(外设寄存器),目的地址递增(内存缓冲区)。反过来,内存到外设:源地址递增,目的地址固定。
举个例子,配置代码大概长这样:
// 配置DMA通道:外设到内存
DMA_Channel->CPAR = (uint32_t)&USART1->DR; // 源地址:UART数据寄存器(固定)
DMA_Channel->CMAR = (uint32_t)buffer; // 目的地址:内存缓冲区(递增)
DMA_Channel->CNDTR = 256; // 传输256个数据
你想想看,如果我把源地址和目的地址搞反了,数据就会从内存搬到外设——那UART就会疯狂往外发数据,接收端根本收不到。嗯,这种事我干过。
传输宽度与突发传输
传输宽度,就是DMA一次搬运多少数据。常见的有:
- 字节(8位):适合UART、I2C等串行外设
- 半字(16位):适合定时器、PWM等
- 字(32位):适合ADC、DAC、内存复制等
这里有个坑:源和目的的传输宽度必须匹配。比如源是8位,目的是32位,DMA会怎么处理?它会自动对齐,但效率会下降。我建议尽量保持一致。
| 传输宽度 | 适用场景 | 注意事项 |
|---|---|---|
| 8位(字节) | UART、I2C、SPI(8位模式) | 地址递增步长为1 |
| 16位(半字) | 定时器、PWM、音频数据 | 地址递增步长为2 |
| 32位(字) | ADC、DAC、内存复制 | 地址递增步长为4 |
突发传输,说白了就是DMA一次申请多个总线周期,连续搬一堆数据。为什么需要突发?因为每次申请总线都有开销,一次申请搬多个数据,效率更高。
关键点:突发传输的大小通常有4、8、16拍(beat)。比如突发大小为4,每次传输4个数据单元。但要注意:突发传输会占用总线较长时间,如果系统中有高优先级的中断或实时任务,可能会被阻塞。
我曾经在一个音频项目中,用了16拍的突发传输。结果音频数据是流畅了,但触摸屏的响应变得迟钝。后来把突发大小降到4拍,问题解决了。这就是典型的「过度优化」——突发太大,总线被DMA占得太久。
⚠️ 避坑指南:突发传输不是越大越好。如果你的系统中有多个外设共享总线,建议用4拍或8拍。如果只有DMA在跑,可以用16拍。记住:突发大小要和系统负载匹配。
配置突发传输的代码示例:
// 配置DMA突发传输(以STM32为例)
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_4; // 外设侧突发4拍
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_8; // 内存侧突发8拍
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
嗯,这里要注意:外设侧和内存侧的突发大小可以不同。比如外设是FIFO,可以一次发4个数据;内存是SRAM,可以一次收8个。DMA会自动处理这个差异。
总结一下这一节的核心:DMA控制器架构不复杂,但细节很多。通道配置、地址模式、传输宽度、突发大小——每个参数都影响系统性能。我的建议是:先理解原理,再动手配置。遇到问题别慌,回头检查这几个参数,八成能找到原因。