4. PCIe 配置空间访问:pci_read_config_* / pci_write_config_* 系列函数与 Capability 链表遍历

PCIe 配置空间是设备初始化、资源分配与特性探测的核心入口。在 Linux 内核中,驱动通过 pci_read_config_* 与 pci_write_config_* 系列函数访问配置空间,并通过 Capability 链表遍历设备支持的扩展特性(如 MSI、MSI-X、Power Management、Express Capability 等)。本章将深入解析这些接口的实现机制、使用规范与遍历方法。

4.1 PCIe 配置空间结构概述

PCIe 配置空间分为两部分:PCI 兼容配置空间(256 字节,Header Type 0/1)和 PCIe 扩展配置空间(4 KB,通过 Enhanced Configuration Access Mechanism, ECAM 访问)。驱动通常使用内核封装好的 pci_read_config_* 系列函数,无需关心底层访问机制。

PCIe 配置空间布局(4 KB) PCI 兼容配置空间 0x00 - 0xFF(256 字节) Vendor ID / Device ID (0x00) Status / Command (0x04) BAR 0-5 (0x10 - 0x24) Capabilities Pointer (0x34) PCIe 扩展配置空间 0x100 - 0xFFF(3840 字节) PCI Express Capability (0x100) Device Capabilities / Control (0x104) Link Capabilities / Control (0x10C) ARI / SR-IOV / DPC 等扩展 ECAM
关键提示:PCI 兼容配置空间的前 64 字节是预定义的 Header,包含 Vendor ID、Device ID、Class Code、BAR 等关键信息。Capabilities Pointer 寄存器(偏移 0x34)指向第一个 Capability 结构的起始偏移,是遍历 Capability 链表的入口。

4.2 pci_read_config_* / pci_write_config_* 系列函数

Linux 内核提供了一组类型安全的访问函数,支持按字节(8-bit)、半字(16-bit)和字(32-bit)读写配置空间。这些函数封装了底层 I/O 操作(PCI 配置空间 I/O 端口或 MMIO),并处理了字节序对齐等细节。

4.2.1 函数原型与参数

函数 访问宽度 返回值 说明
pci_read_config_byte 8-bit int 读取指定偏移处的字节
pci_read_config_word 16-bit int 读取指定偏移处的半字
pci_read_config_dword 32-bit int 读取指定偏移处的双字
pci_write_config_byte 8-bit int 写入指定偏移处的字节
pci_write_config_word 16-bit int 写入指定偏移处的半字
pci_write_config_dword 32-bit int 写入指定偏移处的双字

通用参数说明:

  • struct pci_dev *dev:目标 PCIe 设备结构体指针
  • int where:配置空间偏移地址(0-4095),需按访问宽度对齐(byte 任意,word 需 2 字节对齐,dword 需 4 字节对齐)
  • u8/u16/u32 *val:读取时存放结果,写入时提供数据
  • 返回值:成功返回 0,失败返回负的错误码(如 -EINVAL、-EACCES)

4.2.2 典型使用示例

#include <linux/pci.h>

/* 读取 Vendor ID 和 Device ID */
u16 vendor_id, device_id;
pci_read_config_word(dev, PCI_VENDOR_ID, &vendor_id);
pci_read_config_word(dev, PCI_DEVICE_ID, &device_id);

/* 读取并修改 Command 寄存器(偏移 0x04)*/
u16 cmd;
pci_read_config_word(dev, PCI_COMMAND, &cmd);
cmd |= PCI_COMMAND_MASTER;   /* 设置 Bus Master 位 */
cmd |= PCI_COMMAND_MEMORY;   /* 使能 Memory Space */
pci_write_config_word(dev, PCI_COMMAND, cmd);

/* 读取 BAR0(偏移 0x10)*/
u32 bar0;
pci_read_config_dword(dev, PCI_BASE_ADDRESS_0, &bar0);
if (bar0 & PCI_BASE_ADDRESS_SPACE_IO) {
    pr_info("BAR0 is I/O space\n");
} else {
    pr_info("BAR0 is Memory space\n");
}
注意事项:
  • 配置空间访问是阻塞操作,在中断上下文中应避免长时间持有自旋锁时调用。
  • 写入配置空间后,硬件可能需要一定时间(通常几微秒)才能稳定,必要时可调用 pci_read_config_* 回读确认。
  • 对于 PCIe 扩展配置空间(偏移 > 0xFF),部分老旧平台或非 ECAM 机制可能不支持,需通过 pci_read_config_dword 统一接口访问,内核会自动处理。

4.3 Capability 链表遍历

PCIe 设备通过 Capability 链表机制报告其支持的扩展功能。每个 Capability 结构包含:

  • Capability ID(1 字节):标识功能类型(如 0x01=PM, 0x05=MSI, 0x10=PCIe)
  • Next Capability Pointer(1 字节):指向下一个 Capability 结构的偏移,0x00 表示链表结束
  • Capability-specific Data:功能相关的寄存器

4.3.1 遍历流程

Capability 链表遍历流程 读取 Cap_Ptr (0x34) Cap_Ptr == 0? 是 遍历结束 否 读取 Cap ID 处理 Capability 读取 Next Next == 0? 是 遍历结束 否 跳转到 Next 偏移

4.3.2 内核辅助函数:pci_find_capability

内核提供了 pci_find_capability() 函数,用于在 Capability 链表中查找指定 ID 的 Capability 偏移。其原型为:

int pci_find_capability(struct pci_dev *dev, int cap_id);

参数:

  • dev:目标 PCIe 设备
  • cap_id:要查找的 Capability ID(如 PCI_CAP_ID_MSI、PCI_CAP_ID_EXP)

返回值:成功返回 Capability 结构的配置空间偏移(> 0),未找到返回 0。

4.3.3 手动遍历示例

#include <linux/pci.h>

/* 手动遍历所有 Capability */
u8 cap_ptr;
u8 cap_id;
u8 next;

pci_read_config_byte(dev, PCI_CAPABILITY_LIST, &cap_ptr);

while (cap_ptr != 0) {
    /* 读取 Capability ID */
    pci_read_config_byte(dev, cap_ptr + PCI_CAP_LIST_ID, &cap_id);
    
    /* 读取 Next Pointer */
    pci_read_config_byte(dev, cap_ptr + PCI_CAP_LIST_NEXT, &next);
    
    pr_info("Found Capability: ID=0x%02x at offset 0x%02x\n", cap_id, cap_ptr);
    
    /* 根据 Cap ID 进行特定处理 */
    switch (cap_id) {
    case PCI_CAP_ID_MSI:
        pr_info("  - MSI Capability\n");
        break;
    case PCI_CAP_ID_EXP:
        pr_info("  - PCI Express Capability\n");
        break;
    case PCI_CAP_ID_PM:
        pr_info("  - Power Management Capability\n");
        break;
    default:
        pr_info("  - Unknown Capability\n");
        break;
    }
    
    cap_ptr = next;
}

4.3.4 常用 Capability ID 速查表

Capability ID 宏定义 功能描述 典型用途
0x01 PCI_CAP_ID_PM Power Management 电源状态管理(D0-D3)
0x05 PCI_CAP_ID_MSI Message Signaled Interrupts MSI 中断配置
0x09 PCI_CAP_ID_MSIX MSI-X MSI-X 中断配置
0x10 PCI_CAP_ID_EXP PCI Express PCIe 设备能力、链路状态
0x11 PCI_CAP_ID_MSIX MSI-X(旧版) 兼容旧设备
0x13 PCI_CAP_ID_ARI Alternative Routing-ID ARI 扩展
0x15 PCI_CAP_ID_SRIOV Single Root I/O Virtualization SR-IOV 虚拟化

4.4 扩展配置空间访问:pcie_capability_read_* 系列

对于 PCIe 扩展配置空间(如 Device Capabilities、Link Control 等),内核提供了专用的 pcie_capability_read_* 和 pcie_capability_write_* 系列函数。这些函数基于 PCI Express Capability 结构(Cap ID = 0x10)的偏移进行计算,简化了访问流程。

4.4.1 函数原型

int pcie_capability_read_word(struct pci_dev *dev, int where, u16 *val);
int pcie_capability_read_dword(struct pci_dev *dev, int where, u32 *val);
int pcie_capability_write_word(struct pci_dev *dev, int where, u16 val);
int pcie_capability_write_dword(struct pci_dev *dev, int where, u32 val);

参数说明:

  • where:相对于 PCI Express Capability 结构起始的偏移(如 PCI_EXP_DEVCTL、PCI_EXP_LNKCTL)
  • 函数内部自动加上 Capability 结构的基地址偏移

4.4.2 使用示例:读取链路状态

#include <linux/pci.h>
#include <linux/pci_regs.h>

u16 link_status;
u32 dev_cap;

/* 读取 Link Status 寄存器(偏移 0x12 相对于 PCIe Cap)*/
pcie_capability_read_word(dev, PCI_EXP_LNKSTA, &link_status);
int speed = (link_status & PCI_EXP_LNKSTA_SPEED) >> 0;
int width = (link_status & PCI_EXP_LNKSTA_WIDTH) >> 4;
pr_info("PCIe Link: Speed=%d, Width=x%d\n", speed, width);

/* 读取 Device Capabilities 寄存器 */
pcie_capability_read_dword(dev, PCI_EXP_DEVCAP, &dev_cap);
int max_payload = 128 << ((dev_cap & PCI_EXP_DEVCAP_PAYLOAD) >> 0);
pr_info("Max Payload Size supported: %d bytes\n", max_payload);
最佳实践:优先使用 pcie_capability_read_* 系列函数访问 PCIe 扩展寄存器,而非手动计算偏移。这些函数内部会验证设备是否支持 PCIe Capability,并在不支持时返回错误码,避免访问无效地址。

4.5 配置空间访问的调试与验证

在驱动开发过程中,可以通过以下方式验证配置空间访问的正确性:

  • lspci -vvv:查看完整配置空间转储,包括所有 Capability 结构
  • setpci:命令行工具,可直接读写配置空间寄存器
  • /sys/bus/pci/devices/.../config:通过 sysfs 导出配置空间二进制文件
  • 动态调试(dynamic debug):启用 pci=debug 内核参数,打印配置空间访问日志
# 使用 setpci 读取 Vendor ID
setpci -s 01:00.0 VENDOR_ID

# 读取 Capabilities Pointer
setpci -s 01:00.0 CAP_PTR

# 读取 MSI Capability 的 Message Control 寄存器
setpci -s 01:00.0 MSI_CONTROL

# 通过 sysfs 读取配置空间(十六进制)
hexdump -C /sys/bus/pci/devices/0000:01:00.0/config

4.6 常见问题与避坑指南

问题 原因 解决方案
读取配置空间返回 -EINVAL 偏移未按宽度对齐 确保 byte 任意,word 2 字节对齐,dword 4 字节对齐
写入后读取值未更新 硬件写入延迟或寄存器只读 加入 pci_read_config_* 回读确认,检查寄存器属性
Capability 遍历死循环 Next Pointer 指向自身或形成环 限制最大遍历次数(如 20 次),检测重复偏移
pcie_capability_read_* 返回错误 设备不支持 PCIe Capability 先调用 pci_find_capability(dev, PCI_CAP_ID_EXP) 确认
扩展配置空间访问失败 平台不支持 ECAM 或 MMCONFIG 检查 /proc/bus/pci 或使用 pci=nommconf 参数
内核版本差异:在 Linux 5.0+ 中,pci_read_config_* 系列函数已支持通过 ECAM 访问扩展配置空间(偏移 > 0xFF)。旧版本内核可能需要使用 pci_read_config_dword 配合手动偏移计算。建议始终使用 pci_read_config_dword 访问扩展空间,内核会自动选择正确的访问路径。

4.7 本章小结