DMA传输类型:内存到内存、外设到内存、内存到外设、外设到外设
DMA传输听起来抽象,说白了就是数据搬运。但往哪儿搬、从哪儿搬,决定了硬件怎么连线、软件怎么配寄存器。我个人习惯把这四种类型画成一张图再动手写代码,不然很容易搞混。
① 内存到内存(Memory-to-Memory)
这是最“无聊”也最常用的一种。说白了就是把A内存块的数据原封不动拷到B内存块。CPU也能干,但DMA不占用CPU时间,适合大块数据的搬移。
典型场景:协议栈做数据缓冲、音视频帧拷贝、双缓冲轮转。我做过一个千兆以太网的项目,收包后要把数据从描述符指向的buffer搬到应用层的ring buffer。初始版本用CPU memcpy,结果带宽一上来CPU直接100%。换成DMA内存到内存传输,CPU占用降到了15%以下。
关键配置点:
- 源地址寄存器设为起始内存地址A
- 目的地址寄存器设为起始内存地址B
- 传输尺寸设为块大小(注意有些DMA要求对齐到字或缓存行)
- 触发方式一般选软件触发(无需外设请求)
我的小习惯:我会在传输完成后检查剩余计数寄存器,如果非零说明有错误或提前终止。很多芯片的DMA会给出错误状态位,建议读一下——我吃过亏,没查状态结果数据错了一半。
② 外设到内存(Peripheral-to-Memory)
这是DMA最经典的应用。外设“喊”一声,DMA就把数据搬到内存,CPU可以继续干别的事。比如UART、SPI、I2C、ADC连续采样,都适合。
你想想看:串口每收到一个字节就中断一次,CPU要拼命处理。开DMA以后,收到256字节才触发一次中断——CPU效率提升两个数量级。
我曾经调一个AD9361高速射频采集卡,12bit并行接口,数据率超过200MB/s。CPU直接读?不可能的。用DMA外设到内存,配上双缓冲(ping-pong buffer),才算跑通了。嗯,这里要注意外设的握手信号——你是用边沿触发还是电平触发?我之前误配成电平触发,结果DMA一次搬完就卡住了,因为它以为还有数据。
| 参数 | 推荐配置 | 忌讳 |
|---|---|---|
| 字节对齐 | 外设数据宽度 = 总线宽度 | 宽度不对齐导致总线异常 stall |
| 请求源 | 硬件触发(外设REQ线) | 软件触发会让外设数据错过 |
| 地址增量 | 源固定(外设FIFO),目的自增 | 同时自增会变成覆盖同一地址 |
③ 内存到外设(Memory-to-Peripheral)
反过来,CPU准备好数据,DMA送给外设。典型:音频I2S播放、LCD显示、GPIO波形控制、DAC发送。
我最早接触的是把一段WAV数据通过DMA灌给I2S发送FIFO。刚开始写代码时,我忘了设置外设地址固定(只写FIFO),结果DMA把数据写到内存里递增去了——完全没声音。后来检查才发现外设地址没锁住。
避坑指南:我曾经遇到过DMA传输的最后一个数据卡在外设FIFO里没发出去。原因是DMA传输完成后自动停止了时钟或片选信号,而外设还需要一个额外的写周期才能把最后的数据推出去。解决办法:配置流控模式或手动额外生成一个边缘。具体查芯片手册的"last transfer"行为——别想当然。
很多DMA控制器支持链路(LinkedList)模式,你可以做多个描述符链起来,让DMA自动切到下一个节点。适合做环形缓冲区——这样你不需要在CPU里打断去重配寄存器。
④ 外设到外设(Peripheral-to-Peripheral)
这个用得最少,但有时是性能法宝。数据从一个外设FIFO直接搬到另一个外设FIFO,完全不沾内存,甚至CPU完全感知不到。
举个实际例子:我需要把SPI从机的数据实时转发到UART输出。传统做法:SPI中断→CPU读→CPU写UART。延迟累加。用DMA外设到外设:SPI的RX请求触发DMA,源是SPI数据寄存器,目的是UART数据寄存器,搬运一个字后自动结束或循环。这样延迟只有几十纳秒,还不用CPU。
不过,这种模式下要注意:两个外设的时钟域可能不同。如果SPI工作在50MHz而UART工作在115200bps,DMA控制器能不能跨越时钟域做流控?我见过的许多DMA不支持这种跨异步场景的握手,需要额外加FIFO或使用共享SRAM做桥接。说白了,硬碰硬容易丢数据。
我个人的评估清单(决定用外设到外设之前):
- 两个外设是否在同一个时钟域?
- 它们的数据宽度是否匹配(8/16/32位)?
- DMA是否支持同时固定源地址和固定目的地址?
- 是否不需要数据缓存/格式转换?
- 传输结束后的同步机制是否足够?
如果以上都是YES,大胆用。否则老老实实走内存中转,更稳妥。
四种类型速查表
| 传输类型 | 源地址模式 | 目的地址模式 | 触发方式 | 典型应用 |
|---|---|---|---|---|
| 内存→内存 | 自增 | 自增 | 软件触发 | 数据拷贝、帧搬运 |
| 外设→内存 | 固定(FIFO) | 自增 | 硬件请求 | ADC采集、UART接收 |
| 内存→外设 | 自增 | 固定(FIFO) | 硬件请求 | 音频播放、LCD刷屏 |
| 外设→外设 | 固定 | 固定 | 硬件请求 | 数据重定向、协议桥接 |
这四种类型覆盖了95%以上的DMA使用场景。在实际项目中,你往往会组合使用——比如先外设到内存做数据采集,再内存到外设做数据输出。搞清楚每种类型下地址怎么变、触发源是谁,写起驱动来心里就有底了。