3. PCI配置空间访问:通过sysfs与内核API读取Vendor ID、Device ID、Class Code
PCI配置空间是PCI/PCIe设备最重要的软件可见寄存器集合,它包含了设备的身份信息、能力指针、资源需求以及状态控制位。对于驱动开发者而言,正确读取并解析配置空间是设备枚举、资源分配和驱动绑定的第一步。本章将系统讲解如何通过用户态sysfs接口和内核态API两种途径获取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指令集)。
nvme、ahci)的绑定。理解这三者的层级关系对编写struct pci_device_id表至关重要。
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"
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内核版本一致 |
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设备。