3. Linux PCI子系统框架

PCI子系统是Linux内核中连接外设与驱动核心的桥梁。理解pci_bus/pci_dev数据结构、驱动注册与探测流程以及BAR映射机制,是排查DMA传输故障的基础。本章将深入剖析这三个核心模块,帮助你在驱动开发中精准定位问题。

3.1 pci_bus与pci_dev数据结构

Linux内核使用两个核心数据结构来描述PCI拓扑:pci_bus表示总线,pci_dev表示设备。它们共同构成了从根总线到末端设备的树形结构。

3.1.1 pci_bus结构

pci_bus代表一条物理PCI总线,包含总线编号、父总线、子总线链表以及该总线上的设备链表。关键字段如下:

字段 类型 说明
number unsigned char 总线号(Bus Number)
primary unsigned char 主总线号(用于桥接设备)
secondary unsigned char 从属总线号(桥接设备下游)
subordinate unsigned char 最大从属总线号
bridge struct pci_dev * 指向桥接设备的指针(若为根总线则为NULL)
devices struct list_head 该总线上所有设备的链表头
children struct list_head 子总线链表头

3.1.2 pci_dev结构

pci_dev是单个PCI设备的抽象,包含配置空间信息、资源、驱动绑定状态等。核心字段包括:

  • 标识字段:vendor、device、subsystem_vendor、subsystem_device、class、revision
  • 拓扑字段:bus(所属总线)、devfn(设备/功能号)、slot(槽位)
  • 资源字段:resource[](最多6个BAR + ROM + 桥接窗口)
  • 驱动字段:driver(绑定的驱动)、dev(内核通用设备)
  • DMA字段:dma_mask、coherent_dma_mask、dev.dma_parms
排查要点:当DMA传输失败时,首先检查pci_dev->dma_mask是否小于设备实际需要的地址范围。例如,64位设备若dma_mask被错误设置为32位,将导致地址截断。

3.1.3 PCI拓扑结构SVG

下图展示了PCI总线-设备树形结构,从根总线到末端功能设备的完整路径:

PCI总线-设备拓扑结构 pci_bus 0 (根总线) pci_dev (PCI桥) pci_bus 1 (子总线) pci_dev 0:0.0 (网卡) pci_dev 0:1.0 (存储控制器) pci_dev 0:2.0 (GPU) pci_dev 0:2.1 (HDMI音频) 说明:每个pci_bus包含devices链表和children链表;pci_dev通过bus字段指向所属总线,通过devfn字段标识位置。 桥接设备同时属于上游总线和下游总线,负责地址转换与总线枚举。

3.2 pci_driver注册与探测流程

PCI驱动通过pci_register_driver()注册到内核,内核在枚举设备时调用驱动的probe()函数完成绑定。理解这一流程对调试驱动加载失败至关重要。

3.2.1 驱动注册核心结构

struct pci_driver是驱动开发者需要填充的核心结构:

struct pci_driver {
    const char *name;                    // 驱动名称
    const struct pci_device_id *id_table; // 支持的设备ID表
    int  (*probe)(struct pci_dev *dev, const struct pci_device_id *id);
    void (*remove)(struct pci_dev *dev);
    int  (*suspend)(struct pci_dev *dev, pm_message_t state);
    int  (*resume)(struct pci_dev *dev);
    // ... 其他回调
};

3.2.2 探测流程步骤

  1. 设备枚举:PCI子系统扫描总线,为每个设备创建pci_dev实例。
  2. ID匹配:遍历已注册驱动的id_table,匹配vendor/device/class等字段。
  3. 资源分配:内核为设备分配I/O或MMIO资源(BAR映射)。
  4. 调用probe:匹配成功后调用驱动的probe()函数。
  5. DMA初始化:在probe中设置dma_mask、申请DMA缓冲区。
  6. 设备注册:将设备注册到内核设备模型,生成设备文件或网络接口。

3.2.3 探测流程SVG

pci_driver探测流程 1. 设备枚举 PCI扫描总线,创建pci_dev 2. ID匹配 遍历id_table匹配vendor/device 3. 资源分配 BAR映射、I/O端口分配 4. 调用probe() 驱动初始化硬件 5. DMA初始化 设置dma_mask、申请DMA缓冲区 6. 设备注册 生成设备文件/网络接口

3.2.4 典型probe函数模板

static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    int ret;
    struct my_dev *dev;

    // 1. 启用PCI设备
    ret = pci_enable_device(pdev);
    if (ret) return ret;

    // 2. 设置DMA掩码(先尝试64位,失败则回退32位)
    ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
    if (ret) {
        ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(32));
        if (ret) goto err_disable;
    }

    // 3. 申请BAR资源映射
    ret = pcim_iomap_regions(pdev, BIT(0), "my_driver");
    if (ret) goto err_disable;

    // 4. 分配驱动私有数据结构
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) { ret = -ENOMEM; goto err_disable; }

    // 5. 申请DMA缓冲区
    dev->dma_buf = dma_alloc_coherent(&pdev->dev, BUF_SIZE,
                                       &dev->dma_handle, GFP_KERNEL);
    if (!dev->dma_buf) { ret = -ENOMEM; goto err_disable; }

    // 6. 注册中断
    ret = request_irq(pdev->irq, my_isr, 0, "my_driver", dev);
    if (ret) goto err_free_dma;

    // 7. 保存私有数据
    pci_set_drvdata(pdev, dev);
    return 0;

err_free_dma:
    dma_free_coherent(&pdev->dev, BUF_SIZE, dev->dma_buf, dev->dma_handle);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
常见故障:probe函数中忘记调用pci_enable_device()会导致后续所有PCI配置空间访问失败;dma_set_mask()返回错误时未回退32位,会导致64位设备在32位系统上无法工作。

3.3 pci_resource与BAR映射

PCI设备的BAR(Base Address Register)定义了设备在系统中的地址空间。Linux通过pci_resource_*系列宏访问这些资源,驱动需要正确映射BAR才能访问设备寄存器。

3.3.1 BAR类型与标志

BAR类型 标志位 地址空间 典型用途
I/O BAR bit 0 = 1 I/O端口空间 配置寄存器、状态寄存器
32位MMIO BAR bit 0 = 0, bit 2:1 = 00 32位内存空间 数据缓冲区、控制寄存器
64位MMIO BAR bit 0 = 0, bit 2:1 = 10 64位内存空间 大容量DMA缓冲区、显存
扩展ROM BAR 特殊类型 内存空间 设备固件/选项ROM

3.3.2 资源访问API

Linux提供以下宏和函数操作BAR资源:

// 获取BAR的物理地址
resource_size_t pci_resource_start(struct pci_dev *dev, int bar);
resource_size_t pci_resource_end(struct pci_dev *dev, int bar);

// 获取BAR的大小
resource_size_t pci_resource_len(struct pci_dev *dev, int bar);

// 获取BAR的标志(IORESOURCE_IO / IORESOURCE_MEM)
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);

// 映射BAR到内核虚拟地址
void __iomem *pcim_iomap(struct pci_dev *pdev, int bar, unsigned long maxlen);
void pcim_iounmap(struct pci_dev *pdev, void __iomem *addr);

// 批量映射(推荐)
int pcim_iomap_regions(struct pci_dev *pdev, int mask, const char *name);

3.3.3 BAR映射与DMA地址关系SVG

BAR映射与DMA地址关系 PCI设备 BAR0: 控制寄存器 BAR1: 数据缓冲区 BAR2: 64位DMA区域 ... 内部DMA引擎 PCI总线地址空间 BAR0 → 0xF0000000 BAR1 → 0xF0010000 BAR2 → 0x100000000 (64位) 系统内存 DMA缓冲区 (物理连续) 地址: 0x7F000000 大小: 4MB dma_handle: 0x7F000000 CPU虚拟地址: 0xFFFF... BAR映射 DMA传输 BAR映射与DMA地址关键区别 • BAR地址:设备配置空间中定义的地址,CPU通过MMIO访问设备寄存器时使用。 • DMA地址:设备访问系统内存时使用的物理地址,由dma_alloc_coherent返回。 • 两者是独立的地址空间:BAR用于CPU→设备通信,DMA地址用于设备→内存通信。 • 故障排查:检查BAR映射是否成功(ioremap),DMA地址是否在dma_mask范围内。 • 使用lspci -vvv查看BAR基地址和大小,使用cat /proc/iomem验证映射。 • 64位BAR需要两个连续的配置空间寄存器,检查是否被正确识别。

3.3.4 资源映射故障排查清单

  • BAR未分配:检查BIOS/UEFI是否分配了地址空间,使用lspci -vvv查看BAR区域是否为[disabled]。
  • 地址冲突:多个设备BAR映射到相同地址范围,检查/proc/iomem和/proc/ioports。
  • 映射大小错误:pci_resource_len()返回0或异常值,可能是设备配置空间损坏。
  • 64位BAR截断:32位系统上访问64位BAR时地址被截断,需确认内核支持CONFIG_PHYS_ADDR_T_64BIT。
  • IO BAR误用:对I/O BAR使用ioremap()而非ioport_map(),导致访问异常。
调试技巧:在probe函数中添加dev_info(&pdev->dev, "BAR0: start=0x%llx, len=%llu, flags=0x%lx\n", pci_resource_start(pdev, 0), pci_resource_len(pdev, 0), pci_resource_flags(pdev, 0));,通过dmesg验证资源分配是否正确。

3.4 本章小结

本章深入剖析了Linux PCI子系统的三大核心模块:

  • pci_bus/pci_dev数据结构:理解了总线-设备树形拓扑,以及关键字段在DMA调试中的作用。
  • pci_driver注册与探测流程:掌握了从设备枚举到probe调用的完整链路,以及常见故障点。
  • pci_resource与BAR映射:区分了BAR地址与DMA地址的本质差异,并提供了资源映射的排查清单。