DMA传输类型:内存到内存、内存到外设、外设到内存、循环链(LLI/Scatter-Gather)详解
咱们做嵌入式开发的,天天跟数据搬来搬去打交道。DMA 就是那个专门干搬砖活的“搬运工”。但不同场景下,搬运工干活的方式不一样。这一章我就把这四种传输类型给你讲透。
我个人习惯先看一张总图,再拆开细讲。你打开脑子里的缓存,先收下这张图:
好,咱们一个一个拆。
1. 内存到内存
这种最简单也最基础。说白了就是让DMA把数据从一个内存地址搬到另一个内存地址。CPU只用发个命令,然后就可以去干别的事。我印象很深,有一次处理摄像头采集的图像做颜色空间转换,8位图转24位图,CPU算的话每帧要好几毫秒。后来换成DMA内存到内存搬运加查表法,一帧降到几百微秒。
| 特点 | 说明 |
|---|---|
| 源和目标 | 都在内存地址空间 |
| 触发方式 | 软件触发(写触发寄存器或链式触发) |
| 传输单元 | 字节/半字/字/双字,可配置 |
| 典型场景 | 内存拷贝、缓冲搬迁、图像重排、数字信号处理前处理 |
2. 内存到外设
这个场景你肯定熟悉——往外设送数据。比如用SPI发送一帧波形、用UART打印日志、用I2C给传感器写配置。DMA负责把内存里的数据源源不断喂给外设,CPU只管准备Buffer。
但要注意,外设的接收端通常是一个固定深度的FIFO。如果DMA传太快,FIFO满的时候外设会拉反压信号;如果传太慢,外设会空等。所以我个人的习惯是:一定开启DMA的传输完成中断,并且在外设的FIFO半满中断里做流量控制。碰到过一个坑:某款MCU的UART DMA在波特率115200下连续发送大包,FIFO满了之后DMA不停止,直接覆盖数据。后来我在DMA描述符里加了DST_REQ位,等外设请求信号才发下一段。
3. 外设到内存
反过来,外设采集数据往内存塞。典型的:ADC连续采样、摄像头像素流、USB接收、网卡收包。这时的挑战是——数据来得快且不确定。DMA要能及时把外设FIFO里的数据搬到内存,否则FIFO溢出丢数据。
我建议你用“双缓冲”或者“环形缓冲”配合DMA。比如ADC用双缓冲:当DMA填满第一块内存后,自动切换到第二块,同时触发中断让CPU处理第一块的数据。这样采集和处理完全流水线。
| 对比项 | 内存→外设 | 外设→内存 |
|---|---|---|
| 数据流向 | 内存 → 外设 | 外设 → 内存 |
| 主要风险 | 外设FIFO满,DMA停等 | 内存写覆盖或FIFO溢出 |
| 中断设计 | 传输完成+外设错误 | 传输完成+半传输(半满)+溢出 |
| 常用硬件 | UART TX、SPI MOSI、I2C写 | ADC、I2S RX、UART RX、SPI MISO |
4. 循环链(LLI / Scatter-Gather)
这个最强大,也最容易踩坑。循环链(Linked List Item, LLI)也叫分散收集(Scatter-Gather, SG)。原理很简单:DMA不是只完成一个传输任务,而是通过链表描述多个不连续的源或目标内存块。每完成一块,自动加载下一个描述符,直到链尾。
典型的LLI描述符结构体长这样:
struct dma_lli {
uint32_t src_addr; // 源地址
uint32_t dst_addr; // 目标地址
uint32_t transfer_len; // 本次传输长度(字节)
uint32_t next_lli; // 下一个描述符的物理地址(0表示链尾)
uint32_t ctrl; // 控制位(完成中断使能、传输方向等)
};
上电后,你只需把第一个描述符的地址告诉DMA控制器的链表基址寄存器,然后启动。DMA就会按链表自动跑。我做过一个网络加速器,用LLI把收到的多个以太网帧分别放到不同存储区,每个帧大小不同,照样一次触发搞定。不需要CPU逐个处理。
dma_alloc_coherent分配内存。
Scatter-Gather 和循环链其实是同一个概念的不同叫法。有些厂商叫LLI(比如ST、NXP),有些叫SG(比如TI、Xilinx)。内核里统一叫sg_table。但要注意:循环链既可以做成单向链表(每个描述符指向下一个),也可以做成环形链表(最后一个指向第一个),实现持续循环传输。
next_lli一定要清0,否则DMA会读到随机地址并执行非法操作。更高级的用法是设置最后一块描述符的int_en位,在传输完成时触发中断,而中间的描述块不触发中断,减少中断次数。我见过有人把每个描述符都开中断,结果中断风暴直接把系统拖垮。
几种类型的选择策略
讲到这里你可能要问了:“那我到底该用哪种?” 我自己的选择法则很简单,列成表格你参考:
| 传输需求 | 推荐类型 | 原因 |
|---|---|---|
| 两块连续内存之间的拷贝 | 内存→内存 | 简单,资源消耗少 |
| 单次发送/接收固定长度数据到外设 | 内存↔外设(一次传输) | 配置快,中断少 |
| 连续采集/输出,且数据块非对称 | 内存↔外设 + 循环链 | 灵活,无中断停顿 |
| 大文件或网络包,内存不连续 | 循环链(Scatter-Gather) | 避免拷贝,节省内存带宽 |
| 高速音视频流,需要极低延迟 | 循环链(环形模式) | 连续传输,无中断gap |
最后提醒一句: 不管你用哪种传输类型,中断绑定和缓存一致性是两个永远绕不开的坎。下一章我们就要正式开始讲中断绑定——为什么在AMP(非对称多核)系统中,DMA中断必须绑定到指定核?因为如果随意分配,两个核同时处理同一个DMA中断,数据可能被乱改。我后来养成一个习惯:每个DMA通道在初始化时,明确指定它的target_cpu亲和性。
最后提醒一句: 不管你用哪种传输类型,中断绑定和缓存一致性是两个永远绕不开的坎。我开始做多核项目时,就吃过“DMA中断到处乱跑”的亏。所以建议你从一开始就在设计文档里标清楚每个DMA通道的核亲和性,后面能省很多debug时间。
这四种传输类型,我用了好多年。每次接触新SoC,我都会先翻DMA控制器的datasheet,看看它支持哪几种模式,然后快速做个benchmark。内存到内存和内存到外设基本是标配,循环链则在高性能场景下不可或缺。如果你打算做音视频、网络或者工业采集,循环链一定是你的好帮手。但记住——LLI描述符的cache一致性必须处理好,否则DMA跑起来扑朔迷离,调试到你怀疑人生。