描述符结构(Descriptor)

描述符是什么?说白了,就是一张“任务卡片”。CPU 把搬运任务写在卡片上,扔给 DMA 去执行。卡片上写了:从哪搬、搬到哪、搬多少、怎么搬、下一张卡片在哪。

我刚开始接触 Scatter Gather 时,总觉得描述符就是几个寄存器而已。后来调一个 PCIe 的驱动,发现描述符里错了一个控制位,数据就全乱了。从那以后,我每配一个字段都得对着手册再读一遍。

下面我们逐个拆开看看。

源地址(Source Address)

源地址就是数据从哪取。可以是内存地址,也可以是外设地址。比如你想把 FPGA 里一个 FIFO 的数据搬到 DDR,源地址就指向那个 FIFO 的地址空间。

我的习惯: 配源地址时,一定要确认地址对齐。有些 DMA 引擎要求源地址按 4 字节或 8 字节对齐,否则会触发对齐错误。我记得有一次写了一个 UART 的 scatter,因为源地址没对齐,DMA 直接罢工了。

具体配置时,源地址通常是 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 从第一个描述符开始,搬完数据后根据链指针跳到下一个,直到遇到结束标志。

描述符链工作流程 描述符 0 源: 0x1000 目的: 0x2000 长度: 1024 控制: 链结束=0 Next → 描述符1 描述符 1 源: 0x1040 目的: 0x2040 长度: 2048 控制: 链结束=0 Next → 描述符2 描述符 2 源: 0x1100 目的: 0x2100 长度: 512 控制: 链结束=1 Next = NULL DMA 从描述符0开始执行,完成后跳转到描述符1,再到描述符2 直到遇到链结束标志或 NULL 指针,传输停止。

图中的红、绿、蓝三个描述符颜色不同,只是为了区分,实际没有特殊含义。每个描述符内部都包含了完整的 5 个字段。DMA 硬件会按顺序取出并执行,像流水线一样流畅。

个人经验: 我调试时,喜欢在第一个描述符之前预留一个“哨兵”描述符,专门用来触发中断或者记录统计。这样不会干扰实际数据链,排查问题很方便。

好了,描述符的 5 个字段我们都讲完了。你只要把这 5 个字段理解透,Scatter Gather 的配置就已经完成了 80%。剩下的是中断处理、错误恢复这些辅助逻辑,但核心都在这里了。