Linux PCI驱动开发与DMA传输设计 - 第1节

1.1 PCI总线历史:从ISA到PCIe的演进

PCI(Peripheral Component Interconnect)总线自1992年由Intel提出以来,经历了从并行共享总线到串行点对点互连的深刻变革。理解这一演进过程,是掌握PCI驱动架构的基础。

总线标准 推出年份 总线宽度 工作频率 峰值带宽 拓扑结构
ISA 1981 8/16 bit 8.33 MHz ~8 MB/s 共享并行
PCI 1992 32/64 bit 33/66 MHz 133/533 MB/s 共享并行
PCI-X 1998 64 bit 66~533 MHz ~4.3 GB/s 共享并行
PCIe 1.0 2003 1~32 Lane 2.5 GT/s 250 MB/s per Lane 点对点串行
PCIe 4.0 2017 1~16 Lane 16 GT/s ~2 GB/s per Lane 点对点串行
关键转折点:PCIe采用差分串行信号和交换架构,彻底解决了并行总线在高速下的信号同步与反射问题,使带宽可随Lane数线性扩展。
PCI总线演进时间线 ISA (1981) PCI (1992) PCI-X (1998) PCIe 1.0 (2003) 演进核心驱动力 • 并行→串行:消除时钟偏移与信号反射,提升频率上限 • 共享→交换:每个设备独享带宽,支持热插拔与原生QoS • 软件兼容:PCIe配置空间与枚举机制向后兼容PCI,驱动复用率高 • 物理层:差分对替代单端信号,抗干扰能力大幅提升

1.2 PCI配置空间:设备发现的基石

PCI配置空间是驱动与硬件通信的“身份证”。每个PCI功能(Function)都拥有独立的256字节配置空间(PCIe扩展至4KB),由Host Bridge在系统启动时通过配置读写周期访问。

1.2.1 配置空间布局(前64字节)

偏移 寄存器名称 宽度 描述
0x00 Vendor ID 16 bit 厂商编号(如Intel 0x8086)
0x02 Device ID 16 bit 设备型号编号
0x04 Command 16 bit 控制I/O、内存、DMA、中断使能
0x06 Status 16 bit 中断状态、Capabilities链表等
0x08 Revision ID 8 bit 硅片修订版本
0x09 Class Code 24 bit 设备分类(Base/Sub/Prog IF)
0x10~0x24 Base Address Registers (BAR0~5) 32 bit each 映射I/O或内存地址空间
0x3C Interrupt Line 8 bit 中断向量号(由BIOS/OS填写)
0x3D Interrupt Pin 8 bit INTA#~INTD#(1~4)
驱动开发要点:Linux内核通过 pci_read_config_*() / pci_write_config_*() 系列函数访问配置空间。BAR寄存器在驱动探测阶段由内核自动分配资源,驱动只需调用 pci_ioremap_bar() 映射到虚拟地址空间。
PCI配置空间结构(前64字节) Vendor ID (0x00) Device ID (0x02) Command (0x04) | Status (0x06) Revision ID Class Code (0x09) Cache Line | Latency Timer BAR0 ~ BAR5 (0x10 ~ 0x24) 6 × 32-bit Cardbus CIS Pointer (0x28) | Subsystem IDs (0x2C~0x30) Expansion ROM Base Address (0x30) Capabilities Pointer (0x34) | Reserved (0x35~0x3B) Interrupt Line (0x3C) Interrupt Pin (0x3D) PCIe Capability结构从0x34指向的链表开始,扩展至4KB

1.3 PCI地址空间:三种地址域与DMA基础

PCI总线定义了三种独立的地址空间:内存空间、I/O空间和配置空间。驱动开发者必须理解这三种空间的映射关系,才能正确操作设备寄存器并配置DMA传输。

1.3.1 三种地址空间对比

特性 内存空间 I/O空间 配置空间
访问指令 MOV / LD / ST IN / OUT (x86) 配置读写周期
地址宽度 32/64 bit 16/32 bit 8 bit (Bus:Dev:Func)
BAR类型 Memory BAR (bit0=0) I/O BAR (bit0=1) 不通过BAR
Prefetchable 支持(可合并写) 不支持 N/A
DMA支持 主控DMA必须使用 不支持DMA 仅用于配置
Linux API ioremap() + readl/writel ioport_map() + inb/outb pci_read_config_*()
常见陷阱:许多现代PCIe设备只实现Memory BAR,不再支持I/O空间。驱动开发时应优先使用内存映射I/O(MMIO),避免依赖I/O空间以保证跨平台兼容性(如ARM/ARM64不支持x86的IN/OUT指令)。

1.3.2 地址转换与DMA

DMA传输涉及三个地址域:CPU虚拟地址 → CPU物理地址 → PCI总线地址。IOMMU(如Intel VT-d)在中间层提供映射服务,使设备可以访问非连续的物理内存。

PCI地址空间与DMA地址转换 CPU虚拟地址 (VA) 0x7f00_0000 MMU CPU物理地址 (PA) 0x1_0000_0000 IOMMU PCI总线地址 (BDA) 0x8000_0000 DMA地址映射关键API(Linux) 1. dma_alloc_coherent() — 分配一致性DMA缓冲区,返回CPU VA和总线地址 2. dma_map_single() — 将已有缓冲区映射到总线地址空间(流式DMA) 3. pci_set_dma_mask() — 设置设备DMA地址宽度(32/64 bit)

1.4 PCI中断机制:从INTx到MSI/MSI-X

PCI中断机制经历了从传统的边带中断(INTx)到消息信号中断(MSI/MSI-X)的演进。MSI/MSI-X通过内存写事务传递中断,避免了INTx的共享中断线问题,是高性能设备驱动的首选。

1.4.1 中断类型对比

特性 INTx (传统) MSI MSI-X
信号方式 专用中断线(INTA~INTD) 内存写TLP 内存写TLP
中断向量数 最多4个/设备 最多32个/功能 最多2048个/功能
共享中断 支持(需驱动处理共享) 不支持共享 不支持共享
每个向量独立配置 否 否(连续分配) 是(独立地址+数据)
Linux使能API request_irq() pci_enable_msi() pci_enable_msix()
适用场景 兼容旧设备 通用高性能 多队列/多核负载均衡
性能建议:NVMe SSD、万兆网卡等高性能设备应优先使用MSI-X,为每个硬件队列分配独立中断向量,配合CPU亲和性设置实现中断负载均衡。

1.4.2 MSI/MSI-X中断传递流程

MSI/MSI-X中断传递流程 PCIe设备 (Function) 1. 写MSI Message Memory Write TLP 中断控制器 (IOAPIC/GIC) 2. 解析Message→IRQ# 中断信号 CPU Core (执行ISR) 3. 执行中断服务例程 MSI Message格式(64-bit) Address: 0xFEE0_0000 + (Destination ID << 12) | Data: 向量号 + 触发模式

1.4.3 中断处理编程模板

/* MSI-X中断使能示例 */
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    int ret;
    struct my_device *dev;
    struct msix_entry entries[NR_IRQS];
    int i;

    /* 1. 分配设备私有结构 */
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    pci_set_drvdata(pdev, dev);

    /* 2. 使能PCI设备 */
    ret = pci_enable_device(pdev);
    if (ret) goto err_free;

    /* 3. 设置DMA掩码(64-bit优先) */
    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) goto err_disable;

    /* 4. 请求MMIO资源 */
    ret = pcim_iomap_regions(pdev, BIT(0), KBUILD_MODNAME);
    if (ret) goto err_disable;
    dev->regs = pcim_iomap_table(pdev)[0];

    /* 5. 分配MSI-X向量 */
    for (i = 0; i < NR_IRQS; i++)
        entries[i].entry = i;   /* 硬件队列索引 */

    ret = pci_enable_msix_exact(pdev, entries, NR_IRQS);
    if (ret) goto err_disable;

    /* 6. 注册中断处理 */
    for (i = 0; i < NR_IRQS; i++) {
        ret = devm_request_irq(&pdev->dev, entries[i].vector,
                               my_interrupt_handler, 0,
                               dev_name(&pdev->dev), &dev->queue[i]);
        if (ret) goto err_free_msix;
    }

    return 0;

err_free_msix:
    pci_disable_msix(pdev);
err_disable:
    pci_disable_device(pdev);
err_free:
    kfree(dev);
    return ret;
}
最佳实践:使用 devm_* 系列API(Managed Device Resource)可以自动管理资源释放,减少错误处理路径的复杂度。上述示例中 pcim_iomap_regions 和 devm_request_irq 在设备移除时自动释放。

本章小结

PCI总线基础是设备驱动开发的必修课。本章覆盖了四个核心知识点:

  • 总线历史:从ISA到PCIe的演进,理解并行到串行、共享到交换的架构变革。
  • 配置空间:256字节标准配置空间是设备枚举和资源分配的基础,BAR寄存器决定了设备与系统的地址映射关系。
  • 地址空间:三种地址域(内存/I/O/配置)各有用途,DMA传输必须通过IOMMU完成地址转换。
  • 中断机制:MSI/MSI-X取代传统INTx成为主流,多队列设备应充分利用MSI-X实现中断负载均衡。