4. 描述符链表初始化:内存分配、描述符填充、链表链接、硬件寄存器配置

描述符链表(Descriptor Chain)是 DMA 传输的核心数据结构,它定义了内存与设备之间的数据搬运路径。初始化阶段需要完成四个关键步骤:内存分配、描述符填充、链表链接和硬件寄存器配置。任何一个环节出错,都会导致 DMA 传输失败或数据损坏。

关键原则:描述符链表必须在物理地址连续的内存区域中构建,或者使用支持分散/聚集(SG)的硬件链表结构。驱动开发者必须确保描述符所在内存区域不会被 CPU 缓存污染(通常使用一致性 DMA 映射或非缓存标志)。

4.1 内存分配:为描述符预留物理连续区域

描述符链表的内存分配通常使用 dma_alloc_coherent() 或 dmam_alloc_coherent() 函数,这两个函数保证返回物理地址连续且 CPU 与设备缓存一致的内存块。

/* 分配描述符链表内存 - 以 32 个描述符为例 */
#define NR_DESCRIPTORS  32
#define DESC_SIZE       64   /* 每个描述符 64 字节 */

struct dma_chan {
    struct dma_desc *desc_virt;   /* CPU 虚拟地址 */
    dma_addr_t       desc_phys;   /* 设备物理地址 */
    unsigned int     nr_desc;
};

static int alloc_descriptor_chain(struct device *dev, struct dma_chan *chan)
{
    size_t total_size = NR_DESCRIPTORS * DESC_SIZE;

    chan->desc_virt = dma_alloc_coherent(dev, total_size,
                                         &chan->desc_phys, GFP_KERNEL);
    if (!chan->desc_virt)
        return -ENOMEM;

    chan->nr_desc = NR_DESCRIPTORS;
    memset(chan->desc_virt, 0, total_size);

    dev_info(dev, "DMA desc chain: virt=%p phys=%pad size=%zu\n",
             chan->desc_virt, &chan->desc_phys, total_size);
    return 0;
}
常见陷阱:不要使用 kmalloc() 或 __get_free_pages() 分配描述符内存,除非你能保证物理连续性并手动处理缓存一致性。对于超过 4KB 的描述符链表,dma_alloc_coherent() 会自动处理大页分配。

4.2 描述符填充:配置传输参数

每个描述符包含传输所需的全部参数:源地址、目标地址、传输长度、控制标志和下一个描述符指针。以下是一个典型的 DMA 描述符结构体定义:

/* 硬件描述符结构 - 以某 SoC 为例 */
struct dma_desc {
    __le32 src_addr;       /* 源地址(物理地址) */
    __le32 dst_addr;       /* 目标地址(物理地址) */
    __le16 transfer_size;  /* 传输字节数 */
    __le16 ctrl;           /* 控制标志位 */
    __le32 next_desc;      /* 下一个描述符物理地址 */
    __le32 reserved;       /* 保留/状态字段 */
} __attribute__((packed));

/* 控制标志位定义 */
#define DESC_CTRL_OWN      BIT(15)  /* 硬件所有权位 */
#define DESC_CTRL_IE       BIT(14)  /* 传输完成中断使能 */
#define DESC_CTRL_CHAIN    BIT(13)  /* 链表模式使能 */
#define DESC_CTRL_FIRST    BIT(12)  /* 链表中的第一个描述符 */
#define DESC_CTRL_LAST     BIT(11)  /* 链表中的最后一个描述符 */

填充描述符时,需要按照传输方向设置源/目标地址,并正确配置控制标志位:

static void fill_descriptor(struct dma_desc *desc,
                            dma_addr_t src, dma_addr_t dst,
                            size_t len, bool is_first, bool is_last)
{
    desc->src_addr      = cpu_to_le32(src);
    desc->dst_addr      = cpu_to_le32(dst);
    desc->transfer_size = cpu_to_le16(len);

    /* 构建控制字 */
    u16 ctrl = DESC_CTRL_OWN | DESC_CTRL_CHAIN;
    if (is_first)
        ctrl |= DESC_CTRL_FIRST;
    if (is_last)
        ctrl |= DESC_CTRL_LAST | DESC_CTRL_IE;  /* 最后一个使能中断 */
    desc->ctrl = cpu_to_le16(ctrl);

    /* next_desc 由链表链接阶段填充 */
    desc->next_desc = 0;
}
所有权位(OWN bit)机制:描述符的 OWN 位用于协调 CPU 与 DMA 控制器对描述符的访问。当 OWN=1 时,描述符归 DMA 硬件所有,CPU 不能修改;传输完成后硬件清除 OWN 位,将所有权交还给 CPU。这是描述符链表安全操作的核心。

4.3 链表链接:构建单向或循环链表

描述符链表有两种常见拓扑:单向链表(最后一个描述符的 next 指针为 NULL)和循环链表(最后一个指向第一个)。循环链表常用于连续数据流场景,如音频、视频采集。

性能建议:对于高吞吐量场景,优先使用循环链表。它可以避免链表遍历到末尾时的重新加载延迟,DMA 控制器自动回绕到链表头部,实现无中断的连续传输。
static void link_descriptor_chain(struct dma_desc *base_virt,
                                  dma_addr_t base_phys,
                                  unsigned int count, bool is_circular)
{
    unsigned int i;
    dma_addr_t next_phys;

    for (i = 0; i < count; i++) {
        if (i == count - 1) {
            /* 最后一个描述符 */
            if (is_circular)
                next_phys = base_phys;  /* 回绕到第一个 */
            else
                next_phys = 0;          /* 单向链表结束 */
        } else {
            next_phys = base_phys + (i + 1) * sizeof(struct dma_desc);
        }
        base_virt[i].next_desc = cpu_to_le32(next_phys);
    }
}
描述符链表拓扑:单向 vs 循环 单向链表 描述符 0 next → 描述符 1 描述符 1 next → 描述符 2 描述符 2 next = NULL ✕ 传输结束 循环链表 描述符 0 next → 描述符 1 描述符 1 next → 描述符 2 描述符 2 next → 描述符 0 ↻ 自动回绕,连续传输

4.4 硬件寄存器配置:启动 DMA 引擎

描述符链表构建完成后,需要将链表基地址写入 DMA 控制器的寄存器,并配置传输参数。典型的寄存器配置序列如下:

寄存器名称 偏移地址 配置值 说明
DMA_LLPx 0x100 + 0x40*x desc_phys 链表基地址寄存器,指向第一个描述符
DMA_CTLx 0x104 + 0x40*x 0x0000_0001 控制寄存器:使能通道,设置优先级
DMA_CFGx 0x108 + 0x40*x 0x0000_0003 配置寄存器:链表模式 + 中断使能
DMA_SWx 0x10C + 0x40*x 0x0000_0001 软件触发寄存器:写入 1 启动传输
static void configure_dma_hw(struct device *dev, struct dma_chan *chan,
                              dma_addr_t chain_phys)
{
    void __iomem *base = chan->reg_base;
    int ch = chan->channel_id;

    /* 1. 写入链表基地址 */
    writel(chain_phys, base + DMA_LLP(ch));

    /* 2. 配置控制寄存器:使能通道 */
    writel(DMA_CTL_CH_EN, base + DMA_CTL(ch));

    /* 3. 配置模式寄存器:链表模式 + 完成中断 */
    u32 cfg = DMA_CFG_CHAIN_MODE | DMA_CFG_IE_COMPLETE;
    writel(cfg, base + DMA_CFG(ch));

    /* 4. 软件触发启动 */
    writel(DMA_SW_TRIGGER, base + DMA_SW(ch));

    dev_dbg(dev, "DMA ch%d started: LLP=0x%08x\n", ch, chain_phys);
}
硬件依赖警告:不同 SoC 的 DMA 控制器寄存器布局差异很大。上述示例基于通用 Synopsys DW DMA 控制器。实际开发时,务必查阅芯片参考手册(TRM)确认寄存器偏移和位域定义。错误的寄存器配置可能导致总线错误或系统挂死。

4.5 初始化流程总结

完整的描述符链表初始化流程可以归纳为以下步骤:

  1. 分配内存:使用 dma_alloc_coherent() 分配物理连续、缓存一致的内存块。
  2. 清零并填充:将内存清零,然后为每个描述符设置源/目标地址、传输长度和控制标志。
  3. 链接链表:设置每个描述符的 next_desc 指针,决定单向或循环拓扑。
  4. 配置硬件:将链表基地址写入 DMA 控制器的 LLP 寄存器,设置模式并启动。
  5. 验证所有权:确认第一个描述符的 OWN 位已置位,硬件已取得控制权。
调试技巧:在初始化完成后,可以通过读取描述符内存来验证链表完整性。打印每个描述符的 src_addr、dst_addr、ctrl 和 next_desc 字段,与预期值比对。使用 dev_dbg() 在关键步骤输出日志,便于定位问题。
描述符链表初始化完整流程 ① 内存分配 dma_alloc_coherent() ② 清零与填充 memset + 设置传输参数 ③ 链表链接 设置 next_desc 指针 单向链表 last→next=0 循环链表 last→next=first ④ 硬件寄存器配置 LLP + CTL + CFG + SW DMA 传输进行中

4.6 初始化后的验证检查清单

完成初始化后,建议逐项检查以下内容,确保描述符链表正确配置:

检查项 验证方法 预期结果
内存物理连续性 检查 dma_alloc_coherent 返回值 非 NULL,且物理地址在预期范围内
描述符 OWN 位 读取第一个描述符的 ctrl 字段 OWN 位为 1(硬件拥有)
链表完整性 遍历所有描述符的 next_desc 指针连续且最后一个正确终止或回绕
硬件 LLP 寄存器 读取 DMA_LLP 寄存器 值与 desc_phys 一致
中断使能 检查最后一个描述符的 IE 位 IE 位为 1(使能完成中断)
性能调优切入点:描述符大小和数量直接影响 DMA 传输效率。对于大数据块传输,建议使用较大的描述符(如 128 字节)和较少的数量(如 8-16 个);对于小数据块高频传输,使用较小的描述符(如 32 字节)和较多的数量(如 64-128 个)。具体参数需根据实际带宽和延迟要求调整。