4. DMA驱动初始化:PCIe设备探测(probe)、BAR空间映射、MSI/MSI-X中断申请、DMA引擎初始化
DMA驱动的初始化是PCIe设备在Linux内核中正常工作的基石。本章将详细拆解从设备探测到DMA引擎就绪的完整流程,涵盖probe回调、BAR空间映射、MSI/MSI-X中断申请以及DMA引擎初始化四个关键阶段。每个阶段都直接影响后续DMA传输的稳定性与性能,是调试DMA异常时必须掌握的核心知识。
调试要点: 在DMA传输异常时,超过60%的问题根源可追溯到初始化阶段的配置错误,例如BAR映射地址错误、中断向量申请失败或DMA描述符环未正确对齐。务必逐项验证初始化返回值。
4.1 PCIe设备探测(probe)流程
当Linux内核的PCI子系统枚举到匹配的PCIe设备时,会调用驱动注册的probe函数。该函数是驱动与硬件交互的入口点,负责完成设备识别、资源分配和初始化准备。
典型的probe函数执行以下步骤:
- 获取设备结构体:通过
struct pci_dev *pdev参数访问设备。 - 使能PCIe设备:调用
pci_enable_device(),激活I/O和内存空间。 - 设置DMA掩码:调用
dma_set_mask_and_coherent(),告知设备支持的DMA地址宽度(32位或64位)。 - 读取BAR信息:通过
pci_resource_start()和pci_resource_len()获取各BAR的物理地址和长度。 - 申请中断:配置MSI/MSI-X并注册中断处理函数。
- 初始化DMA引擎:分配描述符环、配置DMA控制寄存器。
关键代码片段:probe函数骨架
static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
int ret;
// 1. 分配私有数据结构
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
// 2. 使能PCIe设备
ret = pci_enable_device(pdev);
if (ret) {
dev_err(&pdev->dev, "pci_enable_device failed: %d\n", ret);
return ret;
}
// 3. 设置DMA掩码(64位优先)
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(&pdev->dev, "DMA mask setup failed\n");
goto err_disable;
}
}
// 4. 映射BAR空间
dev->bar_phys = pci_resource_start(pdev, 0);
dev->bar_len = pci_resource_len(pdev, 0);
dev->bar_virt = pcim_iomap(pdev, 0, dev->bar_len);
if (!dev->bar_virt) {
dev_err(&pdev->dev, "BAR0 iomap failed\n");
ret = -ENOMEM;
goto err_disable;
}
// 5. 申请MSI-X中断(见4.3节)
// 6. 初始化DMA引擎(见4.4节)
pci_set_drvdata(pdev, dev);
return 0;
err_disable:
pci_disable_device(pdev);
return ret;
}
常见陷阱: 在probe中如果
pci_enable_device()失败,不要直接返回错误码而不调用pci_disable_device()。必须确保资源释放的对称性,否则后续设备热插拔或驱动重载时会导致内核警告。
4.2 BAR空间映射
BAR(Base Address Register)是PCIe设备暴露给主机的内存或I/O空间。驱动必须将BAR的物理地址映射到内核虚拟地址空间,才能通过MMIO(Memory-Mapped I/O)访问设备寄存器。
BAR空间映射的典型步骤:
- 获取BAR物理基址:
pci_resource_start(pdev, bar_index) - 获取BAR长度:
pci_resource_len(pdev, bar_index) - 映射到虚拟地址:
pcim_iomap(pdev, bar_index, max_len)或ioremap() - 验证映射:读取设备ID寄存器确认映射正确。
| BAR类型 | 资源标志 | 映射函数 | 访问方式 |
|---|---|---|---|
| Memory BAR (32-bit) | IORESOURCE_MEM |
pcim_iomap() |
readl/writel |
| Memory BAR (64-bit) | IORESOURCE_MEM + 64-bit flag |
pcim_iomap() |
readq/writeq (需对齐) |
| I/O BAR | IORESOURCE_IO |
pci_iomap() |
inb/outb 系列 |
调试技巧: 使用
lspci -vvv -s 01:00.0查看BAR的物理地址和大小。如果映射后访问寄存器返回全0或全F,通常表示BAR地址配置错误或设备未使能。
4.3 MSI/MSI-X中断申请
PCIe设备支持三种中断模型:INTx(传统中断线)、MSI(消息信号中断,最多32个向量)和MSI-X(扩展MSI,最多2048个向量)。对于高性能DMA传输,MSI-X是首选,因为它支持每个队列独立中断向量,减少中断处理竞争。
MSI/MSI-X申请流程:
- 查询设备能力:通过
pci_msi_vec_count()或pci_msix_vec_count()获取最大向量数。 - 分配中断向量:调用
pci_alloc_irq_vectors(),指定向量数量和类型标志。 - 注册中断处理:对每个向量调用
request_irq(),绑定处理函数和私有数据。 - 使能设备中断:写设备控制寄存器,允许中断产生。
| 特性 | MSI | MSI-X |
|---|---|---|
| 最大向量数 | 32(1, 2, 4, 8, 16, 32) | 2048 |
| 向量连续要求 | 必须连续 | 可独立分配 |
| 每个向量独立配置 | 否 | 是(每个向量有独立消息地址和数据) |
| 典型应用 | 少量中断场景 | 多队列DMA、NVMe |
MSI-X申请代码示例:
// 查询最大MSI-X向量数
int nvec = pci_msix_vec_count(pdev);
if (nvec < 0) {
dev_err(&pdev->dev, "MSI-X not supported\n");
return nvec;
}
// 分配4个MSI-X向量
int ret = pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSIX);
if (ret < 0) {
dev_err(&pdev->dev, "MSI-X allocation failed: %d\n", ret);
return ret;
}
// 注册每个向量的中断处理函数
for (int i = 0; i < ret; i++) {
int irq = pci_irq_vector(pdev, i);
snprintf(dev->irq_name[i], 32, "mydev-%d", i);
ret = request_irq(irq, my_interrupt_handler, 0,
dev->irq_name[i], &dev->dma_chan[i]);
if (ret) {
dev_err(&pdev->dev, "request_irq failed for vector %d\n", i);
goto err_free_vectors;
}
}
调试要点: 如果MSI-X申请失败,检查
/proc/interrupts确认中断向量是否分配。常见失败原因包括:BIOS中MSI-X被禁用、设备固件未正确报告能力、或内核启动参数pci=nomsi被设置。
4.4 DMA引擎初始化
DMA引擎初始化是驱动准备进行数据传输的最后一步。核心任务是配置DMA控制器的描述符环(Descriptor Ring)和控制寄存器,使设备能够自主搬运数据。
初始化步骤:
- 分配描述符环内存:使用
dma_alloc_coherent()分配物理连续的内存,用于存放DMA描述符。 - 初始化描述符环:设置每个描述符的源地址、目的地址、传输长度和控制标志。
- 配置DMA控制寄存器:通过MMIO写入描述符环基址、环大小、使能位等。
- 启动DMA引擎:设置引擎状态为就绪,等待上层提交传输请求。
DMA描述符环初始化代码示例:
#define RING_SIZE 256
#define DESC_SIZE 32 // 每个描述符32字节
struct dma_desc {
__le64 src_addr;
__le64 dst_addr;
__le32 length;
__le32 flags;
} __attribute__((packed));
// 分配描述符环(物理连续,64字节对齐)
dev->ring_virt = dma_alloc_coherent(&pdev->dev,
RING_SIZE * DESC_SIZE,
&dev->ring_phys,
GFP_KERNEL);
if (!dev->ring_virt) {
dev_err(&pdev->dev, "Failed to allocate descriptor ring\n");
return -ENOMEM;
}
// 初始化所有描述符为无效状态
memset(dev->ring_virt, 0, RING_SIZE * DESC_SIZE);
// 配置DMA控制寄存器(MMIO)
writel(lower_32_bits(dev->ring_phys), dev->bar_virt + DMA_RING_BASE_LOW);
writel(upper_32_bits(dev->ring_phys), dev->bar_virt + DMA_RING_BASE_HIGH);
writel(RING_SIZE, dev->bar_virt + DMA_RING_SIZE);
writel(DMA_ENGINE_ENABLE, dev->bar_virt + DMA_CONTROL);
性能优化: 描述符环应分配在DMA友好的内存区域(如ZONE_DMA或ZONE_DMA32),避免跨NUMA节点访问。使用
dma_set_coherent_mask()确保设备能访问分配的内存地址。
4.5 初始化完整性检查清单
在完成上述所有步骤后,建议通过以下清单验证初始化是否完整:
| 检查项 | 验证方法 | 预期结果 |
|---|---|---|
| PCIe设备使能 | cat /sys/bus/pci/devices/0000:01:00.0/enable |
返回 1 |
| BAR映射成功 | 读取BAR0寄存器(如设备ID) | 返回非零值 |
| MSI-X向量分配 | cat /proc/interrupts | grep mydev |
显示对应中断号 |
| 描述符环物理地址 | 读取DMA_RING_BASE_LOW/HIGH寄存器 | 与dma_alloc_coherent返回地址一致 |
| DMA引擎状态 | 读取DMA_STATUS寄存器 | Engine Ready 位为 1 |
调试红线: 如果DMA引擎初始化后状态寄存器显示错误(如描述符环地址无效),立即检查描述符环的物理地址是否在设备支持的地址范围内。某些老设备不支持64位地址,即使设置了64位DMA掩码,实际地址仍可能被截断。
通过以上四个阶段的严谨初始化,DMA驱动已具备执行数据传输的基本能力。下一阶段将进入DMA传输的提交与完成处理,届时描述符环的正确性和中断响应的实时性将成为调试重点。