DMA 描述符环:Scatter-Gather、环形指针与 cache 一致性
更新时间:2026-09-19。本文是 DMA 软件接口深文,引擎数据通路见 DMA 控制器微架构,Stream 侧见 AXI4-Stream 与 DMA,burst 组织见 AXI4 burst。
本文要回答的问题
- 什么是描述符,一个描述符至少包含哪些字段?
- 环形(ring)怎么靠头/尾指针工作,谁生产谁消费?
- Scatter-Gather 怎么把多个不连续物理块串成一次 I/O?
- 完成状态怎么回写、何时中断?为什么描述符内存要处理 cache?
一、为什么需要描述符
最简单 DMA 只搬「固定源地址→固定目的地址、固定长度」一块,由软件每块启动一次,中断和寄存器开销大。网络包、磁盘 I/O、视频帧的数据缓冲在物理内存里通常不连续(分散的页),且一次 I/O 就有很多块。Scatter-Gather DMA 让软件准备一串「描述符(descriptor, BD)」,硬件自动沿链/环逐个搬运,CPU 只在批量边界介入。
二、描述符字段
典型描述符(8~32 字节,按字对齐)含:
| 字段 | 含义 |
|---|---|
| BUF_ADDR | 数据缓冲物理地址(或 S/G 链里的缓冲指针) |
| BUF_LEN / CONTROL | 本缓冲字节数;方向(MM2S/S2MM) |
| NEXT_DESC | 下一个描述符地址(链表模式) |
| STATUS/控制位 | 完成、错误、IRQ on end、wrap(环回)、TLAST 生效率 |
| (扩展) | EOF/SOF、MM2S/S2MM 专属长度、用户 sideband |
- 链表(linked list):NEXT_DESC 指向任意位置的下一描述符,灵活但地址散。
- 环形(ring):描述符在一段连续数组里,靠 wrap 位或索引回绕,NEXT 可省略;缓存友好、软件简单,网卡/存储驱动最常用。
三、环形指针:生产者-消费者
软件(生产者)与硬件(消费者)各持一个索引:
软件填 硬件搬
┌──────────────────────────────────────┐
│ done │ cur_hw → 忙 → tail_sw │ free │
└──────────────────────────────────────┘
head(软件已处理完成位置) tail(软件新追加位置)- CURDES(硬件/消费者指针):DMA 正在/即将取的描述符。
- TAILDES(软件/生产者尾指针):软件填好一批描述符后,把「最后一个之后」的位置写进 DMA 的尾指针寄存器——这一写就是门铃(doorbell),通知硬件有新活。
- 硬件从 CURDES 搬到 TAILDES 之间的描述符,每完成一个推进 CURDES;追上 TAILDES 就空闲等待。
- 软件回收:已完成(硬件回写完成位/所有权位)的描述符才可重新填。区分满/空通常留一个槽或用计数/所有权(ownership)位。
「所有权位」风格(很多网卡/存储 BD 用):描述符带一个 owner 位,软件交给硬件时置「硬件所有」,硬件做完置回「软件所有」并回写状态,天然做生产/消费分界。
四、一次 S/G 传输流程
以收包 S2MM(Stream→内存)为例:
- 软件在内存分配好若干空缓冲,把地址/长度写进描述符,置 owner=硬件,更新尾指针按门铃。
- 流上数据到来(见 AXI4-Stream),DMA 取当前描述符,按 BUF_ADDR 发起 AXI INCR burst 写入;一个缓冲写满或收到 TLAST(包结束)就关闭该缓冲,把实际字节数/完成位/错误回写进描述符。
- 若包跨多个缓冲,自动切到下一个描述符(gather/scatter);每个 burst 仍需满足不跨 4KB、长度对齐等约束(见 burst 边界),硬件或驱动负责拆分。
- 推进 CURDES;带 IRQ 位的描述符完成时拉中断,软件批量回收、处理、再补充新描述符。
MM2S 反向:硬件沿描述符从多个块读,拼成连续 Stream;EOF/TLAST 通常由标记位生成。这样用不连续的物理页承载连续的逻辑流,避免大块连续物理内存分配(零拷贝 I/O 的基础)。
五、完成回写与中断节流
- 完成计数/聚合中断:不是每个描述符都中断,而是每 N 个、或延迟一个 timer(coalescing)才中断一次,降低高吞吐下 CPU 负载。
- 错误回写:DMA 错误(总线 DECERR/SLVERR)、缓冲不足(收包时无描述符 → overflow 丢包)、长度不符,都要落进描述符 STATUS 并报错中断。
- 软件必须保证环里有足够空闲缓冲,否则 S2MM 在背靠背包时溢出(与 UART overrun 同理,只是粒度是包)。
六、cache 一致性:描述符环的头号坑
DMA 与 CPU 往往不经过 缓存一致互连,二者对同一内存各有副本:
- 软件写描述符/发送数据(MM2S)后:若写内容还停在 CPU cache,DMA 从内存读到旧值/旧数据。必须把描述符和发送缓冲做 cache clean(写回),或把该内存区映射为 non-cacheable;写完尾指针(门铃)要确保前面的 clean 已完成(顺序屏障)。
- DMA 写完、软件读完成位/接收数据(S2MM)前:CPU cache 里可能是旧行,必须先 invalidate(丢弃旧副本)再读;否则读到陈旧状态/数据。
- 描述符环最稳妥的工程做法:描述符区与 DMA 缓冲设为 non-cacheable / shareable,或系统使用 ACE-Lite 一致 DMA(由硬件维护)。性能敏感的大数据缓冲若保留 cacheable,则严格按「发送前 clean、接收后 invalidate」维护,注意 clean/invalidate 长度与 cache line 对齐,避免把相邻有效数据误弃。
- 还要保证内存屏障顺序:填好字段 → clean → 写尾指针门铃,不能让门铃先于数据可见。
七、常见的坑
- 不做 cache 维护:DMA 读到旧描述符、CPU 读到旧数据,表现为「偶发跑飞」——最常见也最难调。
- 门铃与数据乱序:先敲尾指针后写/清描述符,硬件取到半成品;用屏障保证序。
- 缓冲跨 4KB/超最大 burst:未拆分导致总线错误,描述符要按边界切。
- 环满/空判断错误:头尾追上时覆盖未处理描述符,或误以为有活;留槽或用 owner/计数。
- TLAST/EOF 没标记:S2MM 不知包边界,一个长包错误地关闭/合并缓冲。
- 回收仍为硬件所有的描述符:没查完成/owner 位就重用,硬件正在用 → 数据损坏。
- 中断过密:每描述符一中断在高带宽下打满 CPU;用聚合/定时中断。
- 虚拟地址当物理地址:DMA 走总线用的是物理/IO 地址,描述符里必须填经过 IOMMU/MMU 翻译的地址,见 MMU/TLB。
相关与延伸
引擎实现见 DMA 控制器微架构;Stream 侧见 AXI4-Stream 与 DMA;burst 见 AXI4 burst 类型;一致性见 CHI/ACE;地址翻译见 MMU/TLB;中断见 中断与 DMA 入门。
总结
DMA 描述符环把每块数据的地址、长度、控制、状态放进描述符,软件填好后只写尾指针门铃,硬件沿头到尾自动 Scatter-Gather、完成回写 owner/状态并聚合中断,把不连续物理页串成连续 I/O;正确性的命门是 cache——描述符与缓冲要么 non-cacheable,要么发送前 clean、接收后 invalidate,并用屏障保证「填数据→clean→敲门铃」的顺序,burst 仍须按 4KB 边界拆分。