第2章 DMA控制器架构:通道、请求映射与传输模式
DMA控制器,说白了就是CPU的「数据搬运工」。CPU告诉它「把A搬到B」,它就能自己干完,CPU腾出手来处理更重要的计算任务。这一章我们深入看看这个搬运工的内部结构和工作模式。
2.1 DMA通道与请求映射
每个DMA控制器都有多个通道(Channel),每个通道独立管理一条数据传输链路。我最早接触STM32的DMA时,看到它有7个通道,心想「这么多怎么用?」后来发现,每个通道其实对应一组固定的外设请求。
核心概念:通道是DMA传输的独立执行单元,每个通道有自己的源地址、目的地址、传输计数和配置寄存器。多个通道可以分时复用总线,但同一时刻只能有一个通道传输数据。
请求映射表(以STM32F4为例)
| DMA通道 | 外设请求1 | 外设请求2 | 外设请求3 |
|---|---|---|---|
| 通道0 | SPI1_RX | USART1_TX | TIM2_UP |
| 通道1 | SPI1_TX | USART1_RX | TIM3_CH1 |
| 通道2 | I2C1_RX | USART2_TX | TIM4_UP |
| 通道3 | I2C1_TX | USART2_RX | TIM5_CH2 |
| 通道4 | SPI2_RX | USART3_TX | TIM1_UP |
这张表怎么读?举个例子:你想用DMA从SPI1接收数据,那就必须用通道0。想用DMA发送数据到USART1,也得用通道0。同一个通道可以服务多个外设,但不能同时——你得在运行时切换请求映射。
我的经验:设计电路时,我会提前把DMA通道和外设的对应关系画成表格。有一次项目里SPI和I2C抢同一个DMA通道,调试了两天才发现是映射冲突。从那以后,我习惯在工程文档里专门列一页「DMA资源分配表」。
2.2 DMA传输类型
DMA能搬数据,但数据从哪搬到哪?这就引出了三种传输类型。我按实际项目中的使用频率排序讲。
2.2.1 外设到内存(Peripheral-to-Memory)
这是最常见的场景。ADC采集完数据,DMA自动搬到内存数组里;UART收到一帧数据,DMA直接存到接收缓冲区。CPU只需要在传输完成后去读内存就行。
// 配置ADC用DMA传输到内存
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory; // 外设→内存
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)adc_buffer;
DMA_InitStruct.DMA_BufferSize = 100;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_Init(DMA2_Stream0, &DMA_InitStruct);
这里有个坑:外设地址寄存器通常是固定的,但内存地址可以递增。配置时记得把内存地址增量打开,外设地址增量关掉——除非你连着读一串外设寄存器。
2.2.2 内存到外设(Memory-to-Peripheral)
反过来,CPU准备好数据,DMA负责发送给外设。比如用DMA发送一串数据到DAC生成波形,或者通过SPI发送显示缓冲区到LCD屏幕。
我曾经踩过的坑:用DMA发送数据到USART时,忘了检查USART的TXE(发送寄存器空)标志。DMA发得太快,USART还没准备好,数据就丢了。解决办法是配置DMA请求时,确保外设的请求信号正确——USART只有在TXE=1时才会发出DMA请求。
2.2.3 内存到内存(Memory-to-Memory)
这个模式不需要外设触发,DMA自己就能搬。常用于:
- 快速拷贝大数组(比CPU用for循环快得多)
- 图像数据在帧缓冲区间搬移
- 协议栈中的数据包重组
// 内存到内存传输配置
DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToMemory; // 内存→内存
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)src_buffer;
DMA_InitStruct.DMA_Memory1BaseAddr = (uint32_t)dst_buffer;
DMA_InitStruct.DMA_BufferSize = 1024; // 一次搬1024个字节
// 注意:内存到内存模式不需要外设触发,配置完直接启动
但要注意,内存到内存传输会占用总线。如果CPU也在同时访问内存,DMA会通过「总线仲裁」和CPU抢带宽。我做过一个测试:DMA搬1MB数据时,CPU的指令执行速度下降了约30%。
2.3 DMA传输模式
传输模式决定了DMA「搬一次还是搬多次」。三种模式对应不同的应用场景。
2.3.1 单次传输(Single Transfer)
每次触发只传输一个数据单元(字节/半字/字)。传输完成后,通道自动关闭。适合:
- 按键扫描(按一次读一次)
- 单次ADC采样
- 调试时手动触发
2.3.2 循环传输(Circular Mode)
传输完设定的数据量后,自动回到起始地址重新开始。这是我最常用的模式,特别适合:
- 音频数据流(环形缓冲区)
- 连续ADC采集(比如电力监控)
- 实时传感器数据记录
关键点:循环模式下,内存地址会回绕。你需要保证缓冲区大小是2的幂次,这样地址回绕时不会越界。我习惯用#define BUF_SIZE 256而不是#define BUF_SIZE 250。
2.3.3 突发传输(Burst Transfer)
一次触发传输多个数据单元。突发长度通常可配置为4、8、16个节拍。好处是:
- 减少总线仲裁次数(一次申请用多次)
- 提高传输效率(尤其适合高速外设)
- 降低CPU干预频率
但突发传输也有代价:它会长时间占用总线,影响其他高优先级任务。我在一个多传感器项目中,DMA用16节拍突发传输,结果定时器中断响应延迟了20μs——差点导致PWM波形失真。
2.4 知识结构总览
下面这张图把DMA控制器的核心要素串起来了。我建议你把它截图保存,调试时对照着看。
2.5 三种传输模式对比
| 特性 | 单次传输 | 循环传输 | 突发传输 |
|---|---|---|---|
| 触发方式 | 每次触发传1个单元 | 自动回绕连续传 | 一次触发传多个单元 |
| 适用场景 | 单次采样、按键 | 音频、连续ADC | 高速外设、大数据块 |
| 总线占用 | 低(瞬间完成) | 持续占用 | 高(突发期间独占) |
| 中断频率 | 每次传输都中断 | 每轮完成中断 | 每突发完成中断 |
| 配置复杂度 | 简单 | 中等(注意缓冲区大小) | 较高(需匹配外设时序) |
我的选择原则:如果外设速度慢(比如UART 115200bps),用单次传输就够了。如果外设速度快且数据量大(比如音频I2S),用循环传输配合环形缓冲区。突发传输我一般只在DMA带宽成为瓶颈时才用——比如驱动高分辨率LCD时。
2.6 调试避坑指南
最后分享几个我实际调试中遇到的坑:
- 通道优先级冲突:高优先级通道会打断低优先级通道的传输。如果两个通道同时请求,高优先级的先执行。我遇到过定时器触发的DMA被SPI DMA打断,导致波形出现毛刺。
- 传输完成标志未清除:DMA传输完成后,硬件会置位完成标志。下次启动前必须软件清除,否则DMA不会重新开始。这个坑我至少踩过三次。
- 地址对齐问题:有些DMA要求源地址和目的地址按数据宽度对齐。比如传输32位数据时,地址必须是4的倍数。不对齐时,DMA可能产生错误传输或直接不工作。