3. DMA 传输类型:四种模式深度解析
各位好,这一章我们聊聊DMA的四种传输类型。很多资料讲得挺清楚,但我在项目里发现,不少工程师对这几种类型的本质区别其实没有吃透。结果就是配置错了,数据丢了,还得回头查手册。说实话,我也是踩过坑才真正搞明白的。
3.1 内存到内存传输
这是最基础的模式,也是很多新手最先接触到的。说白了,就是DMA自己把数据从一块内存搬到另一块内存。CPU只负责发个启动指令,剩下的事DMA全包了。
| 属性 | 说明 |
|---|---|
| 源地址 | 系统内存(DDR/SRAM) |
| 目的地址 | 系统内存(DDR/SRAM) |
| 典型场景 | 图像帧缓冲拷贝、协议栈数据包搬运、内存加密/校验前数据整理 |
| 关键参数 | 源地址增量/固定、目的地址增量/固定、传输宽度(字节/半字/字) |
我在项目中遇到过一个问题:用DMA做内存拷贝,源和目的地址都设了增量,结果拷贝出来的数据是错的。排查了半天,发现是地址对齐没处理好。比如你要拷贝一个未对齐的字节数组,DMA如果配置成32位传输,就会出错。你想想看,硬件不知道你的数据结构是什么,它只管按配置的宽度去搬数据。
3.2 内存到外设传输
这个模式最常见。CPU先把要发送的数据准备好,放在内存里,然后告诉DMA:「帮我把这些数据送到外设的发送FIFO里去。」我习惯把这个过程叫做「往外倒数据」。
配置要点主要有三个:
- 地址属性:源地址来自内存(通常设增量模式),目的地址是外设寄存器地址(设固定模式)。
- 触发信号:一般由外设的「发送就绪」信号触发。举个例子,UART的TXFIFO空时,会向DMA发送请求,DMA收到后就搬一次数据。
- 突发传输:如果你的外设支持突发(比如AHB总线上的外设),可以把DMA配置成一次传输多个数据,这样效率更高。但别贪心,突发长度受外设FIFO深度限制。
// 伪代码:配置DMA进行内存到外设传输
dma_config_t cfg;
cfg.src_addr = (uint32_t)tx_buffer; // 内存缓冲区
cfg.dst_addr = (uint32_t)&UART->DR; // UART数据寄存器
cfg.src_inc = DMA_INC_ENABLE; // 源地址递增
cfg.dst_inc = DMA_INC_DISABLE; // 目的地址固定
cfg.transfer_width = DMA_WIDTH_8BIT; // 1字节传输
cfg.burst_size = DMA_BURST_4; // 4次突发
cfg.trigger = DMA_TRIG_UART_TX; // UART发送触发
dma_start(&cfg);
嗯,这里要注意一点:配置好触发信号后,一定要确认外设的DMA请求是否已经使能。我记得有一次我怎么调都没有数据发出去,查了半天,原来是UART模块内部的DMA发送请求位没有置位。这个细节,数据手册里通常写在小字部分,容易被忽略。
3.3 外设到内存传输
和上面正好相反,这是把外设收到的数据搬回内存。最常见的场景:ADC采集、UART接收、SPI从设备接收、以及摄像头数据采集。
我个人觉得,这个模式对实时性的要求比前两个都高。为什么?因为外设的接收FIFO通常很小,比如就16字节。你不及时搬走,FIFO溢出了,数据就丢了。
| 外设类型 | FIFO深度(典型值) | 建议DMA传输策略 |
|---|---|---|
| UART 低速 | 16字节 | 单次传输,触发阈值设到8字节再请求DMA |
| SPI 高速 | 32-64字节 | 突发传输,开启循环模式持续接收 |
| ADC 连续采样 | 单个寄存器 | 每次转换完成立即触发DMA,采用循环模式 |
配置时,地址对齐是另一个容易踩的坑。外设数据寄存器的地址可能不是自然对齐的。比如某个8位宽度的外设,它的数据寄存器地址是0x4000_1001。如果你把DMA配置成32位传输,那就出问题了——它会尝试从0x4000_1000读32位数据,结果读到了其他外设的寄存器上。我建议你遇到这种外设时,老老实实按外设数据宽度去配置DMA。
3.4 循环传输模式
循环模式就是当传输完成后,地址计数器自动回滚到初始值,然后继续下一轮传输。这个模式在连续数据流的场景下非常有用,比如音频播放、数据采集、以及电机控制的电流环。
循环模式的关键参数只有一个:周期长度。就是每轮传输多少字节。比如我要采集1024个点的数据,每轮传输完成后自动从头开始填。
// 伪代码:配置循环传输
dma_config_t cfg;
cfg.src_addr = (uint32_t)&ADC->DR; // ADC数据寄存器
cfg.dst_addr = (uint32_t)adc_buf; // 内存缓冲区
cfg.dst_inc = DMA_INC_ENABLE; // 目的地址递增
cfg.block_size = 1024; // 每轮1024次传输
cfg.circular = DMA_CIRCULAR_ENABLE; // 开启循环模式
cfg.trigger = DMA_TRIG_ADC_READY; // ADC转换完成触发
dma_start(&cfg);
但在实战中,循环模式有个大坑——数据竞争。你想想看,DMA在往缓冲区写数据的时候,CPU也在读同一块缓冲区。如果指针管理不好,CPU读到一半的数据被DMA覆盖了,那就产生坏数据了。
3.5 四种类型对比速查
| 传输类型 | 源地址 | 目的地址 | 典型触发源 | 常用配置 |
|---|---|---|---|---|
| 内存→内存 | 内存(增量) | 内存(增量/固定) | 软件启动 | 对齐检查、突发传输 |
| 内存→外设 | 内存(增量) | 外设(固定) | 外设请求 | 触发匹配、FIFO深度 |
| 外设→内存 | 外设(固定) | 内存(增量) | 外设请求 | 地址对齐、双缓冲 |
| 循环传输 | 取决于功能 | 取决于功能 | 外设请求 | 周期长度、数据竞争保护 |
最后总结一句:不管用哪种类型,核心就是搞清楚你的数据「从哪里来、到哪里去」。源地址和目的地址的归属决定了你选哪种传输类型,而传输类型又决定了DMA控制器的配置方式。把这些理清了,DMA基本就不会出大问题。