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函数执行以下步骤:

  1. 获取设备结构体:通过struct pci_dev *pdev参数访问设备。
  2. 使能PCIe设备:调用pci_enable_device(),激活I/O和内存空间。
  3. 设置DMA掩码:调用dma_set_mask_and_coherent(),告知设备支持的DMA地址宽度(32位或64位)。
  4. 读取BAR信息:通过pci_resource_start()和pci_resource_len()获取各BAR的物理地址和长度。
  5. 申请中断:配置MSI/MSI-X并注册中断处理函数。
  6. 初始化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空间属性与映射方式对照
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地址配置错误或设备未使能。
BAR空间映射与MMIO访问流程 PCIe 设备 BAR0: 0xF0000000 PCI 配置空间 读取 BAR 基址 ioremap 虚拟地址映射 MMIO 访问 readl / writel 说明:BAR 物理地址通过 PCI 配置空间获取,ioremap 将其映射为内核虚拟地址,驱动通过 MMIO 访问设备寄存器

4.3 MSI/MSI-X中断申请

PCIe设备支持三种中断模型:INTx(传统中断线)、MSI(消息信号中断,最多32个向量)和MSI-X(扩展MSI,最多2048个向量)。对于高性能DMA传输,MSI-X是首选,因为它支持每个队列独立中断向量,减少中断处理竞争。

MSI/MSI-X申请流程:

  1. 查询设备能力:通过pci_msi_vec_count()或pci_msix_vec_count()获取最大向量数。
  2. 分配中断向量:调用pci_alloc_irq_vectors(),指定向量数量和类型标志。
  3. 注册中断处理:对每个向量调用request_irq(),绑定处理函数和私有数据。
  4. 使能设备中断:写设备控制寄存器,允许中断产生。
MSI vs MSI-X 关键差异
特性 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引擎初始化流程 分配描述符环 dma_alloc_coherent 初始化描述符 设置地址/长度/标志 配置控制寄存器 写入环基址/使能位 启动DMA引擎 状态置为就绪 说明:描述符环必须物理连续且对齐到缓存行边界(通常64字节),否则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 初始化完整性检查清单

在完成上述所有步骤后,建议通过以下清单验证初始化是否完整:

DMA驱动初始化检查清单
检查项 验证方法 预期结果
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传输的提交与完成处理,届时描述符环的正确性和中断响应的实时性将成为调试重点。