描述符结构(Descriptor)
描述符是什么?说白了,就是一张“任务卡片”。CPU 把搬运任务写在卡片上,扔给 DMA 去执行。卡片上写了:从哪搬、搬到哪、搬多少、怎么搬、下一张卡片在哪。
我刚开始接触 Scatter Gather 时,总觉得描述符就是几个寄存器而已。后来调一个 PCIe 的驱动,发现描述符里错了一个控制位,数据就全乱了。从那以后,我每配一个字段都得对着手册再读一遍。
下面我们逐个拆开看看。
源地址(Source Address)
源地址就是数据从哪取。可以是内存地址,也可以是外设地址。比如你想把 FPGA 里一个 FIFO 的数据搬到 DDR,源地址就指向那个 FIFO 的地址空间。
具体配置时,源地址通常是 32 位或 64 位宽,取决于你的系统地址总线。
目的地址(Destination Address)
目的地址就是数据往哪写。同样可以是内存或外设。比如把 DDR 里的数据搬到显示器缓存,目的地址就是 Framebuffer 的地址。
嗯,这里要注意:有些 DMA 支持源地址和目的地址同时递增,有些只支持一方递增。比如从内存到外设,外设地址通常是固定的,目的地址不递增;从外设到内存,源地址不递增。
核心理解: 源地址和目的地址的递增模式由控制位里的“地址模式”决定。我见过很多新手把两个都设成递增,结果写外设时地址一直跳,数据全写到不对的地方去了。
长度(Length / Transfer Count)
长度字段表示这次搬运多少个字节。注意,不是多少个字,是字节。有些 DMA 控制器用“字节数”,有些用“字长×字数”,要小心。
我个人的经验:长度字段最容易犯错的地方是“边界”问题。比如你设长度 = 1024 字节,但源地址从 0x1000 开始,目的地址从 0x2000 开始,一切正常。但如果长度设成 0,DMA 会怎么处理?
答案是:大部分 DMA 会把 0 解释为 65536(或 0 次传输),具体看芯片手册。反正别设 0,我曾经踩过这个坑,一个死循环卡了半天。
| 场景 | 长度字段 | 实际搬运 | 备注 |
|---|---|---|---|
| 连续搬 1 个块 | 块大小(字节) | 1 次传输完成 | 最常见 |
| 块大小固定 | 块大小 | 每块触发一次 | 可用于循环模式 |
| 0 长度 | 0 | 未定义 | 不要用 |
控制位(Control Flags)
控制位是描述符的“灵魂”。它决定了这次传输怎么干。常见的控制位包含:
- 地址模式:源地址递增 / 固定?目的地址递增 / 固定?
- 传输方向:内存到内存?内存到外设?外设到内存?
- 中断使能:完成这次传输后是否触发中断?
- 链式结束:这个描述符是不是最后一个?如果是,DMA 就停在这。
- 错误处理:遇到错误是停止还是跳过?
我曾经踩过的坑: 调试一个网络包解包场景,我忘了设“链式结束”标志,结果最后一个描述符执行完后,DMA 继续从内存里随机读数据当描述符,系统直接崩溃。
建议:每次配完一组描述符链,一定要把最后一个描述符的“链结束位”置 1。
控制位字段的定义每个芯片都不一样,但核心逻辑大同小异。我习惯先把所有需要的中断位关掉,调试通了再开中断,否则中断风暴会把你淹死。
链指针(Next Descriptor Pointer / Link Pointer)
链指针就是描述符链的“锁链”。它指向下一个描述符的地址。如果是最后一个描述符,一般设成 NULL(0)或者一个特殊标记(根据控制位里的结束标志)。
链指针可以是物理地址,也可以是 DMA 描述符基地址加上偏移。我个人推荐用物理地址,简单直接,调试时也能直接看到指针指向哪。
注意事项: 链指针所指向的内存必须是非缓存(Non-Cacheable)区域,或者经过 cache 一致性处理。否则 CPU 更新了描述符,DMA 读到的还是老数据。
设计描述符链时,我常用一个环形缓冲(Ring Buffer),每个描述符的链指针指回缓冲头部,形成循环。这样不需要每次重新分配内存,性能更好。
// 描述符结构体示例(C语言风格)
struct sg_descriptor {
uint32_t src_addr; // 源地址
uint32_t dst_addr; // 目的地址
uint32_t length; // 传输长度(字节)
uint32_t ctrl; // 控制位
uint32_t next_ptr; // 链指针(指向下一个描述符)
};
你看,一个描述符就这 5 个字段,20 个字节(如果每个字段 4 字节)。但就是这 20 个字节,你得把它配得一丝不苟。
描述符链工作流程
下面这张图展示了 3 个描述符组成的链如何工作。DMA 从第一个描述符开始,搬完数据后根据链指针跳到下一个,直到遇到结束标志。
图中的红、绿、蓝三个描述符颜色不同,只是为了区分,实际没有特殊含义。每个描述符内部都包含了完整的 5 个字段。DMA 硬件会按顺序取出并执行,像流水线一样流畅。
个人经验: 我调试时,喜欢在第一个描述符之前预留一个“哨兵”描述符,专门用来触发中断或者记录统计。这样不会干扰实际数据链,排查问题很方便。
好了,描述符的 5 个字段我们都讲完了。你只要把这 5 个字段理解透,Scatter Gather 的配置就已经完成了 80%。剩下的是中断处理、错误恢复这些辅助逻辑,但核心都在这里了。