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);
}
}
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 初始化流程总结
完整的描述符链表初始化流程可以归纳为以下步骤:
- 分配内存:使用
dma_alloc_coherent()分配物理连续、缓存一致的内存块。 - 清零并填充:将内存清零,然后为每个描述符设置源/目标地址、传输长度和控制标志。
- 链接链表:设置每个描述符的
next_desc指针,决定单向或循环拓扑。 - 配置硬件:将链表基地址写入 DMA 控制器的 LLP 寄存器,设置模式并启动。
- 验证所有权:确认第一个描述符的 OWN 位已置位,硬件已取得控制权。
调试技巧:在初始化完成后,可以通过读取描述符内存来验证链表完整性。打印每个描述符的 src_addr、dst_addr、ctrl 和 next_desc 字段,与预期值比对。使用
dev_dbg() 在关键步骤输出日志,便于定位问题。
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 个)。具体参数需根据实际带宽和延迟要求调整。