4. 描述符概念:描述符结构、链表、环形缓冲区与描述符池

在 UART DMA 驱动中,描述符(Descriptor) 是连接 DMA 控制器与系统内存的核心数据结构。它描述了每一次 DMA 传输的源地址、目的地址、传输长度、控制标志以及下一个描述符的链接指针。理解描述符的结构与组织方式,是实现多串口并发传输的基础。

核心观点:描述符不仅仅是“内存中的一块数据”,它定义了 DMA 引擎的工作模式——是单次传输、自动链式传输,还是循环缓冲传输。驱动开发者通过配置描述符,间接控制 DMA 控制器的行为。

4.1 描述符基本结构

一个典型的 DMA 描述符(以 ARM PL080/PL081 或 Synopsys DW DMA 为例)通常包含以下字段:

字段 位宽 描述
SrcAddr 32-bit 源数据地址(内存或外设 FIFO 地址)
DestAddr 32-bit 目的数据地址
LLI 32-bit 下一个描述符的链接指针(Linked List Item)
Control 32-bit 传输控制字:传输大小、突发长度、宽度、中断使能等

其中 LLI 字段是实现描述符链的关键:当 DMA 完成当前描述符的传输后,硬件自动从 LLI 字段加载下一个描述符的地址,无需 CPU 干预。

/* 描述符结构体定义示例(DW DMA 控制器) */
struct dma_descriptor {
    uint32_t src_addr;      /* 源地址 */
    uint32_t dest_addr;     /* 目的地址 */
    uint32_t lli;           /* 下一个描述符地址(LLI) */
    uint32_t ctrl;          /* 控制字:size | burst | width | int_en */
};
避坑提示:描述符必须对齐到 8 字节或 16 字节边界(取决于 DMA 控制器规格),否则可能触发总线错误或传输异常。在分配描述符内存时,应使用 dma_alloc_coherent() 或 kmem_cache_create() 配合对齐标志。

4.2 描述符链表(Scatter-Gather 链)

描述符链表是 DMA 实现分散-聚集(Scatter-Gather)传输的基础。多个描述符通过 LLI 指针串联成单向链表,DMA 硬件自动遍历执行。

描述符链表结构(Scatter-Gather) 描述符 1 Src: 0x1000 Dst: 0x5000 LLI: → Desc2 Ctrl: 64B 描述符 2 Src: 0x2000 Dst: 0x6000 LLI: → Desc3 Ctrl: 128B 描述符 3 Src: 0x3000 Dst: 0x7000 LLI: NULL Ctrl: 256B DMA 硬件自动从 Desc1 → Desc2 → Desc3 顺序执行, LLI = NULL 表示链结束,触发完成中断。

链表结构的优势在于:

  • 非连续内存传输:源或目的地址可以不连续,适合网络包、文件系统缓冲等场景。
  • CPU 卸载:一次提交多个描述符,DMA 硬件自动遍历,减少中断次数。
  • 动态扩展:运行时可以追加新的描述符到链尾(需注意原子操作)。

4.3 环形缓冲区(Ring Buffer)描述符

对于 UART 这种连续流式数据场景,链表结构存在“链尾回链首”的维护成本。更高效的方式是使用环形缓冲区描述符:描述符在内存中组织成环形,最后一个描述符的 LLI 指向第一个描述符,形成闭环。

环形缓冲区描述符结构 描述符 0 LLI → Desc1 Ctrl: 64B 描述符 1 LLI → Desc2 Ctrl: 64B 描述符 2 LLI → Desc3 Ctrl: 64B 描述符 3 LLI → Desc0 Ctrl: 64B Desc3 的 LLI 指向 Desc0,形成硬件自动回绕的环形

环形缓冲区的关键特性:

  • 硬件自动回绕:DMA 完成最后一个描述符后,自动跳回第一个,无需软件干预。
  • 适用于连续收发:UART 的 RX/TX 通道非常适合环形描述符,数据流不会中断。
  • 水位线管理:驱动通过维护 producer index(硬件当前处理位置)和 consumer index(软件已处理位置)来管理缓冲区的满/空状态。
注意:环形缓冲区描述符的 LLI 必须在初始化时一次性配置完成。运行时修改 LLI 可能导致 DMA 控制器读到不一致的指针,产生不可预期的行为。如果需要动态调整环形大小,建议先停止 DMA 通道,再重新配置描述符池。

4.4 描述符池(Descriptor Pool)

在多串口并发场景下,每个 UART 端口可能需要多个描述符(例如 8 个 RX 描述符 + 8 个 TX 描述符)。如果每个端口都独立分配内存,会导致碎片化和分配延迟。描述符池 是一种预分配、统一管理的优化策略。

特性 独立分配 描述符池
内存碎片 高(频繁分配/释放) 低(一次性预分配)
分配延迟 高(可能触发页面回收) 低(O(1) 从池中获取)
缓存局部性 差(描述符分散) 好(连续内存区域)
多端口共享 不支持 支持(按需分配)

描述符池的典型实现:

/* 描述符池结构体 */
struct desc_pool {
    struct dma_descriptor *base;   /* 预分配的连续描述符数组 */
    unsigned int total;            /* 池中描述符总数 */
    unsigned int free_head;        /* 空闲链表头索引 */
    spinlock_t lock;               /* 池操作锁 */
};

/* 从池中获取一个描述符 */
struct dma_descriptor *desc_pool_get(struct desc_pool *pool)
{
    struct dma_descriptor *desc;
    unsigned long flags;

    spin_lock_irqsave(&pool->lock, flags);
    if (pool->free_head >= pool->total) {
        spin_unlock_irqrestore(&pool->lock, flags);
        return NULL;  /* 池已空 */
    }
    desc = &pool->base[pool->free_head];
    pool->free_head++;
    spin_unlock_irqrestore(&pool->lock, flags);

    return desc;
}
工程实践:在初始化阶段,根据系统最大串口数(例如 8 个 UART)和每个串口的描述符需求(例如 16 个),一次性分配 8 × 16 = 128 个描述符。每个描述符的大小固定(通常 16 字节),128 个描述符仅占用 2KB 内存,性价比极高。

4.5 三种组织方式的对比与选型

组织方式 适用场景 优点 缺点
单描述符 简单单次传输(如配置寄存器) 实现简单,开销最小 无法处理大数据量,中断频繁
描述符链表 Scatter-Gather、非连续内存传输 灵活,支持动态追加 链尾维护复杂,回绕需软件干预
环形缓冲区 UART 连续流、音频流、视频流 硬件自动回绕,中断少,吞吐高 初始化后 LLI 固定,不易动态调整

在 UART DMA 驱动中,推荐使用环形缓冲区描述符 + 描述符池 的组合:环形结构保证数据流不中断,描述符池提供高效的内存管理。对于需要处理非连续数据包的特殊场景(如 RS485 多机通信),可以退化为链表模式。

选型建议:如果硬件 DMA 控制器支持“自动回绕模式”(如 STM32 MDMA 的 Circular Mode),优先使用硬件环形支持;否则在软件中通过 LLI 手动构建环形。两种方式在性能上差异不大,但硬件模式可以减少一次 LLI 加载的延迟。