3. DMA引擎工作原理:DMA描述符环、DMA引擎寄存器、DMA传输流程
DMA(Direct Memory Access)引擎是PCIe设备实现高性能数据传输的核心部件。当PCIe设备作为Bus Master发起DMA传输时,它绕过CPU,直接在系统内存与设备之间搬运数据。理解DMA引擎的工作原理,是定位pci_set_master关闭后传输超时故障的前提。
3.1 DMA描述符环(Descriptor Ring)
DMA描述符环是位于系统内存中的一个环形缓冲区,由一组固定大小的描述符(Descriptor)组成。每个描述符定义了一次DMA传输的源地址、目的地址、传输长度、控制标志以及下一个描述符的指针。设备端DMA引擎通过读取描述符环来获取传输任务,并在完成后更新描述符的状态。
关键概念:描述符环是CPU与DMA引擎之间的“任务队列”。CPU在环中填充描述符,DMA引擎按顺序消费。环的“生产者”是CPU,“消费者”是DMA引擎。
3.1.1 描述符结构
一个典型的DMA描述符包含以下字段(以64位系统为例):
| 字段 | 偏移 | 大小 | 描述 |
|---|---|---|---|
| SRC_ADDR | 0x00 | 8字节 | 源物理地址(内存或设备地址) |
| DST_ADDR | 0x08 | 8字节 | 目的物理地址 |
| NEXT_DESC | 0x10 | 8字节 | 下一个描述符的物理地址(环链接) |
| CONTROL | 0x18 | 4字节 | 传输长度、中断使能、方向标志等 |
| STATUS | 0x1C | 4字节 | DMA引擎写入:完成状态、错误码 |
排查要点:当
pci_set_master关闭后,设备无法发起内存读取(获取描述符),导致描述符环中的任务永远不被消费。此时CPU侧看到描述符状态始终为“未完成”,超时由此产生。
3.1.2 描述符环的环形结构
描述符环通过NEXT_DESC字段形成闭环。最后一个描述符的NEXT_DESC指向环的起始地址。DMA引擎内部维护两个指针:
- Head指针(生产者指针):CPU写入新描述符的位置,由CPU更新。
- Tail指针(消费者指针):DMA引擎当前正在处理或即将处理的位置,由DMA引擎更新。
当Head == Tail时,环为空;当Head + 1 == Tail时,环为满(通常预留一个空位以区分空/满状态)。
3.2 DMA引擎寄存器
DMA引擎通过一组MMIO寄存器与CPU交互。这些寄存器通常映射在PCIe设备的BAR空间中。以下是关键的DMA引擎寄存器:
| 寄存器名称 | 偏移地址 | 宽度 | 功能描述 |
|---|---|---|---|
| DMA_CTRL | 0x000 | 32位 | DMA引擎控制:启动/停止/复位/中断使能 |
| DMA_STATUS | 0x004 | 32位 | 引擎状态:忙/空闲/错误/完成计数 |
| DMA_DESC_BASE | 0x008 | 64位 | 描述符环的基地址(物理地址) |
| DMA_DESC_SIZE | 0x010 | 32位 | 描述符环的大小(描述符个数) |
| DMA_TAIL_PTR | 0x014 | 32位 | Tail指针(DMA引擎写入,CPU只读) |
| DMA_HEAD_PTR | 0x018 | 32位 | Head指针(CPU写入,DMA引擎只读) |
| DMA_INT_CAUSE | 0x01C | 32位 | 中断原因寄存器:传输完成/错误/超时 |
故障关联:当
pci_set_master关闭后,设备无法通过PCIe总线发起任何Memory Read操作。这意味着DMA引擎无法从内存中读取DMA_DESC_BASE指向的描述符环,也无法更新DMA_STATUS寄存器中的完成状态。CPU轮询DMA_STATUS时,看到的始终是“忙”状态,最终触发超时。
3.3 DMA传输流程
一次完整的DMA传输分为三个阶段:准备阶段、传输阶段、完成阶段。下面以“设备从内存读取数据”为例(Memory Read DMA):
3.3.1 准备阶段(CPU侧)
- 分配内存缓冲区:CPU分配连续的物理内存(或通过IOMMU映射),确保缓冲区物理地址连续且对齐。
- 填充描述符:CPU在描述符环中写入源地址(内存缓冲区)、目的地址(设备内部FIFO)、传输长度、中断使能标志等。
- 更新Head指针:CPU将新的Head指针值写入
DMA_HEAD_PTR寄存器,通知DMA引擎有新的任务。 - 启动DMA:CPU设置
DMA_CTRL寄存器的启动位(通常为bit 0)。
3.3.2 传输阶段(DMA引擎侧)
- 读取描述符:DMA引擎通过PCIe Memory Read从
DMA_DESC_BASE + Tail * sizeof(desc)处读取描述符。 - 解析描述符:提取源地址、目的地址、传输长度等信息。
- 执行数据传输:DMA引擎发起PCIe Memory Read从源地址读取数据,然后通过内部数据通路将数据写入目的地址(设备内部寄存器或FIFO)。
- 更新描述符状态:传输完成后,DMA引擎将描述符的STATUS字段写为“完成”,并更新Tail指针。
- 触发中断(可选):如果描述符的CONTROL字段设置了中断使能,DMA引擎向CPU发送MSI-X中断。
3.3.3 完成阶段(CPU侧)
- 处理中断或轮询:CPU收到中断(或轮询到描述符STATUS为“完成”),确认传输结束。
- 处理数据:CPU读取设备内部缓冲区中的数据,或直接使用已写入内存的数据。
- 回收描述符:CPU将已完成的描述符标记为可用,准备下一轮传输。
3.4 DMA传输的关键时序
理解DMA传输的时序对于排查超时故障至关重要。以下是一个典型的DMA传输时序序列:
| 时间点 | 执行者 | 操作 | PCIe总线操作 |
|---|---|---|---|
| T0 | CPU | 分配内存、填充描述符 | 无(本地内存操作) |
| T1 | CPU | 写DMA_HEAD_PTR寄存器 | Memory Write(CPU→设备) |
| T2 | CPU | 写DMA_CTRL启动位 | Memory Write(CPU→设备) |
| T3 | DMA引擎 | 读取描述符(从内存) | Memory Read(设备→内存) |
| T4 | DMA引擎 | 执行数据传输 | Memory Read(设备→内存) |
| T5 | DMA引擎 | 更新描述符状态、写Tail指针 | Memory Write(设备→内存) |
| T6 | DMA引擎 | 发送MSI-X中断 | Memory Write(设备→CPU) |
| T7 | CPU | 处理中断、读取完成状态 | 无(本地内存操作) |
关键洞察:从T3到T6的所有操作,都需要设备作为PCIe Bus Master发起Memory Read或Memory Write事务。当
pci_set_master关闭后,设备无法发起任何这些事务。DMA引擎在T3步骤卡住,无法继续。CPU在T7步骤轮询时,看到的描述符状态仍然是“未完成”,最终触发超时。
3.5 描述符环的软件管理
在Linux内核中,DMA描述符环的管理通常由设备驱动程序的probe函数初始化,并在数据传输过程中由start_xmit或submit_urb等函数填充。以下是一个简化的描述符环初始化代码示例:
/* 描述符环初始化(简化示例) */
struct dma_ring {
struct dma_desc *desc_virt; /* 虚拟地址(CPU访问) */
dma_addr_t desc_phys; /* 物理地址(DMA引擎访问) */
u32 size; /* 描述符个数 */
u32 head; /* 生产者指针 */
u32 tail; /* 消费者指针 */
};
int dma_ring_init(struct pci_dev *pdev, struct dma_ring *ring, u32 size)
{
/* 分配一致DMA内存(物理连续、非缓存) */
ring->desc_virt = dma_alloc_coherent(&pdev->dev,
size * sizeof(struct dma_desc),
&ring->desc_phys,
GFP_KERNEL);
if (!ring->desc_virt)
return -ENOMEM;
ring->size = size;
ring->head = 0;
ring->tail = 0;
/* 初始化描述符环:设置NEXT_DESC形成闭环 */
for (int i = 0; i < size; i++) {
struct dma_desc *desc = &ring->desc_virt[i];
desc->next_desc = ring->desc_phys +
((i + 1) % size) * sizeof(struct dma_desc);
desc->status = 0; /* 初始状态:未完成 */
}
/* 将描述符环基地址写入DMA引擎寄存器 */
writel(ring->desc_phys, pdev->bar_addr + DMA_DESC_BASE_LOW);
writel(ring->desc_phys >> 32, pdev->bar_addr + DMA_DESC_BASE_HIGH);
writel(size, pdev->bar_addr + DMA_DESC_SIZE);
return 0;
}
排查技巧:当怀疑
pci_set_master关闭导致DMA超时时,可以通过lspci -vvv检查设备的Bus Master位状态。如果该位为0,则设备无法发起任何DMA传输。此时,即使描述符环初始化正确、Head指针已更新,DMA引擎也无法读取描述符。