3. PCI配置空间访问:通过sysfs与内核API读取Vendor ID、Device ID、Class Code

PCI配置空间是PCI/PCIe设备最重要的软件可见寄存器集合,它包含了设备的身份信息、能力指针、资源需求以及状态控制位。对于驱动开发者而言,正确读取并解析配置空间是设备枚举、资源分配和驱动绑定的第一步。本章将系统讲解如何通过用户态sysfs接口和内核态API两种途径获取Vendor ID、Device ID和Class Code这三个最基础的配置字段。

前置知识:PCI配置空间的前64字节(Header Type 0/1)是标准化的,所有PCIe设备必须实现。Vendor ID、Device ID和Class Code均位于此区域,偏移地址固定。

3.1 PCI配置空间布局与关键字段

PCI配置空间大小为256字节(PCIe扩展为4KB,但前256字节兼容),其中前16字节的定义对所有设备通用。下表列出了与设备识别直接相关的三个关键字段:

字段名称 偏移地址 宽度 描述
Vendor ID 0x00 16位 设备制造商唯一标识(如0x8086为Intel,0x10EC为Realtek)
Device ID 0x02 16位 特定设备型号标识
Class Code 0x09 24位 设备功能分类(Base Class + Sub Class + Prog IF)

Class Code的24位进一步细分为三个字节:Base Class(偏移0x0B)、Sub Class(偏移0x0A)和Programming Interface(偏移0x09)。例如,一个标准NVMe SSD的Class Code为0x010802,其中Base Class=0x01(大容量存储)、Sub Class=0x08(非易失性存储器)、Prog IF=0x02(NVMe指令集)。

实战要点:驱动匹配时,内核优先使用Vendor ID + Device ID进行精确匹配;Class Code用于通用类驱动(如nvme、ahci)的绑定。理解这三者的层级关系对编写struct pci_device_id表至关重要。
PCI配置空间前16字节结构(Header Type 0) 偏移 (Byte) Vendor ID 0x00-0x01 Device ID 0x02-0x03 Command 0x04-0x05 Status 0x06-0x07 Rev ID 0x08 Class Code 0x09-0x0B CLS 0x0C Class Code 24位展开 Prog IF (0x09) Sub Class (0x0A) Base Class (0x0B) 示例:NVMe SSD Class Code = 0x010802 Base Class=0x01(大容量存储) | Sub Class=0x08(非易失性) | Prog IF=0x02(NVMe) 驱动匹配时:精确匹配 Vendor+Device,回退匹配 Class Code

3.2 用户态访问:通过sysfs读取配置空间

Linux内核将每个PCI设备在/sys/bus/pci/devices/目录下暴露为一个子目录,其中包含了配置空间的原始二进制接口和解析后的属性文件。这是调试和快速验证设备身份的最便捷途径。

3.2.1 定位设备sysfs路径

每个PCI设备在sysfs中的目录名采用domain:bus:device:function格式(BDF地址)。例如:

# 列出所有PCI设备
ls /sys/bus/pci/devices/

# 示例输出(部分)
0000:00:1f.2  0000:01:00.0  0000:02:00.0

# 查看特定设备
ls /sys/bus/pci/devices/0000:01:00.0/

3.2.2 读取Vendor ID、Device ID、Class Code

sysfs提供了两种读取方式:

  • 属性文件方式:直接读取vendor、device、class文件,返回十六进制字符串。
  • 原始配置空间方式:读取config文件,按偏移地址解析二进制数据。
# 方法一:属性文件(推荐)
cat /sys/bus/pci/devices/0000:01:00.0/vendor
0x10ec

cat /sys/bus/pci/devices/0000:01:00.0/device
0x8168

cat /sys/bus/pci/devices/0000:01:00.0/class
0x020000

# 方法二:原始config文件(偏移0x00-0x0B)
hexdump -s 0 -n 4 /sys/bus/pci/devices/0000:01:00.0/config
0000000 ec10 6816
# 小端序解析:0x10ec (Vendor) + 0x8168 (Device)

hexdump -s 0x09 -n 3 /sys/bus/pci/devices/0000:01:00.0/config
0000009 0002 00
# 解析:0x020000 (Class Code)
注意事项:读取config文件需要root权限或设备文件的可读权限。属性文件vendor、device、class通常对所有用户可读,但某些系统可能通过sysfs_permissions内核参数限制。

3.2.3 使用lspci工具验证

lspci工具底层也是通过sysfs或pciutils库读取配置空间,可作为交叉验证手段:

# 查看指定设备的详细信息
lspci -s 01:00.0 -vv

# 仅显示标识信息
lspci -s 01:00.0 -n
01:00.0 0200: 10ec:8168 (rev 15)
# 0200 = Class Code (Network Controller)
# 10ec:8168 = Vendor:Device

# 查看Class Code的完整含义
lspci -s 01:00.0 -m
01:00.0 "Ethernet controller" "Realtek Semiconductor Co., Ltd." "RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller"
用户态通过sysfs读取PCI配置空间流程 用户态程序 /sys/bus/pci/devices/ vendor / device / class PCI子系统 两种读取方式对比 方式一:属性文件 • 路径:/sys/bus/pci/devices/BDF/vendor • 返回:格式化十六进制字符串(如 0x10ec) • 优点:无需解析偏移,直接可读 方式二:原始config文件 • 路径:/sys/bus/pci/devices/BDF/config • 返回:二进制配置空间镜像(256字节) • 优点:可读取任意偏移的配置字段

3.3 内核态访问:使用内核API读取配置空间

在驱动开发中,我们通常在内核态通过标准API访问配置空间。Linux内核提供了两组核心函数:pci_read_config_*系列和pci_get_device/pci_get_class系列。

3.3.1 核心读取API

函数原型 读取宽度 典型用途
pci_read_config_byte(struct pci_dev *dev, int where, u8 *val) 8位 读取Revision ID、Prog IF等单字节字段
pci_read_config_word(struct pci_dev *dev, int where, u16 *val) 16位 读取Vendor ID、Device ID、Command寄存器
pci_read_config_dword(struct pci_dev *dev, int where, u32 *val) 32位 读取Class Code + Revision ID组合、BAR地址

3.3.2 驱动中读取Vendor ID、Device ID、Class Code示例

以下代码展示了一个完整的PCI驱动probe函数,在设备枚举时读取并打印三个关键标识字段:

#include <linux/pci.h>
#include <linux/kernel.h>
#include <linux/module.h>

static int my_pci_probe(struct pci_dev *pdev, 
                        const struct pci_device_id *id)
{
    u16 vendor_id, device_id;
    u32 class_code_raw;
    u8  base_class, sub_class, prog_if;
    int ret;

    /* 方法一:直接从pci_dev结构体读取(推荐) */
    vendor_id = pdev->vendor;
    device_id = pdev->device;
    class_code_raw = pdev->class;  /* 24位有效,高8位为0 */

    dev_info(&pdev->dev, "From struct: Vendor=0x%04x, Device=0x%04x, Class=0x%06x\n",
             vendor_id, device_id, class_code_raw);

    /* 方法二:使用pci_read_config_* API读取 */
    ret = pci_read_config_word(pdev, PCI_VENDOR_ID, &vendor_id);
    if (ret != PCIBIOS_SUCCESSFUL) {
        dev_err(&pdev->dev, "Failed to read Vendor ID\n");
        return -EIO;
    }

    ret = pci_read_config_word(pdev, PCI_DEVICE_ID, &device_id);
    if (ret != PCIBIOS_SUCCESSFUL) {
        dev_err(&pdev->dev, "Failed to read Device ID\n");
        return -EIO;
    }

    /* 读取Class Code(24位)+ Revision ID(8位)组合 */
    ret = pci_read_config_dword(pdev, PCI_CLASS_REVISION, &class_code_raw);
    if (ret != PCIBIOS_SUCCESSFUL) {
        dev_err(&pdev->dev, "Failed to read Class/Revision\n");
        return -EIO;
    }

    /* 解析Class Code字段 */
    base_class = (class_code_raw >> 24) & 0xFF;   /* 偏移0x0B */
    sub_class  = (class_code_raw >> 16) & 0xFF;   /* 偏移0x0A */
    prog_if    = (class_code_raw >> 8)  & 0xFF;   /* 偏移0x09 */

    dev_info(&pdev->dev, "From API: Vendor=0x%04x, Device=0x%04x\n",
             vendor_id, device_id);
    dev_info(&pdev->dev, "Class Code: Base=0x%02x, Sub=0x%02x, ProgIF=0x%02x\n",
             base_class, sub_class, prog_if);

    return 0;
}

static void my_pci_remove(struct pci_dev *pdev)
{
    dev_info(&pdev->dev, "Device removed\n");
}

/* PCI设备ID表 */
static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(0x10EC, 0x8168) },  /* Realtek RTL8168 */
    { PCI_DEVICE(0x8086, 0x153A) },  /* Intel I217-V */
    { 0, }  /* 结束标记 */
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);

static struct pci_driver my_pci_driver = {
    .name     = "my_pci_driver",
    .id_table = my_pci_ids,
    .probe    = my_pci_probe,
    .remove   = my_pci_remove,
};

module_pci_driver(my_pci_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux PCIe Driver Course");
MODULE_DESCRIPTION("Example: Reading PCI Configuration Space");
关键宏定义:内核头文件linux/pci_regs.h中定义了配置空间偏移常量,如PCI_VENDOR_ID (0x00)、PCI_DEVICE_ID (0x02)、PCI_CLASS_REVISION (0x08)。建议始终使用这些宏而非硬编码偏移,以提高代码可读性和可移植性。

3.3.3 通过Class Code查找设备

内核提供了pci_get_class函数,允许驱动按设备类别遍历总线上的设备:

#include <linux/pci.h>

/* 查找所有NVMe设备(Class Code = 0x010802) */
struct pci_dev *pdev = NULL;

while ((pdev = pci_get_class(0x010802, pdev)) != NULL) {
    dev_info(&pdev->dev, "Found NVMe device: %04x:%04x\n",
             pdev->vendor, pdev->device);
    /* 使用完毕后必须释放引用 */
    pci_dev_put(pdev);
}
引用计数管理:pci_get_device和pci_get_class返回的struct pci_dev *会增加设备的引用计数。每次调用后必须使用pci_dev_put()释放,否则会导致设备对象泄漏。遍历循环中,pci_get_class的第二个参数传入上一次返回的指针,函数内部会自动处理引用计数。

3.4 两种访问方式的对比与选择

对比维度 sysfs用户态访问 内核API访问
使用场景 调试、诊断、用户态工具(如lspci) 驱动probe/remove、内核模块
权限要求 属性文件通常可读;config文件需root 内核上下文,无额外权限限制
性能开销 系统调用 + 文件系统路径解析 直接内存读取(配置空间已映射)
实时性 每次读取都从硬件重新获取 读取pdev结构体缓存,无硬件访问延迟
编程复杂度 低(shell命令或文件I/O) 中(需理解PCI子系统API和数据结构)
可移植性 依赖sysfs文件系统挂载 所有Linux内核版本一致
内核态PCI配置空间读取架构 PCI设备驱动 probe() / remove() pci_read_config_*() pci_get_device() / pci_get_class() PCI子系统核心 配置空间缓存管理 struct pci_dev 中的关键字段(配置空间缓存) • vendor (u16) — 直接从配置空间0x00读取,设备枚举时填充 • device (u16) — 直接从配置空间0x02读取 • class (u32) — 低24位为Class Code,高8位为Revision ID • 驱动优先使用pdev->vendor/device/class,避免额外PCI配置周期 • 需要读取其他字段(如BAR、Capabilities)时再调用pci_read_config_*

3.5 常见问题与调试技巧

  • 读取返回0xFFFFFFFF:通常表示设备未使能或配置空间访问超时。检查lspci -vv中Command寄存器的"Bus Master"和"Memory Space"位是否置位。
  • Class Code与预期不符:某些多功能设备(如USB控制器集成在芯片组中)可能报告Sub Class为0x80(其他),需查阅设备数据手册确认。
  • sysfs路径不存在:确认设备已被内核枚举。使用lspci -nn查看设备BDF地址,检查dmesg中是否有PCIe链路训练失败的错误。
  • 内核API返回PCIBIOS_BAD_REGISTER_NUMBER:指定的偏移地址超出配置空间范围(标准配置空间为0x00-0xFF,扩展空间需使用pci_read_config_dword配合PCI_EXT_CAP_ID宏)。
调试利器:使用echo "1" > /sys/bus/pci/devices/BDF/remove和echo "1" > /sys/bus/pci/rescan可以在不重启的情况下模拟设备热插拔,配合dmesg -w实时观察驱动probe/remove流程。

通过本章的学习,你应该能够熟练地在用户态通过sysfs快速获取PCI设备的身份信息,并在内核驱动中使用标准API读取配置空间。这些基础操作是后续实现DMA传输、中断处理和环形缓冲区管理的前提——因为所有高级功能的第一步,都是正确识别并初始化PCIe设备。