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_* 系列函数,无需关心底层访问机制。
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 遍历流程
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 参数 |
pci_read_config_* 系列函数已支持通过 ECAM 访问扩展配置空间(偏移 > 0xFF)。旧版本内核可能需要使用 pci_read_config_dword 配合手动偏移计算。建议始终使用 pci_read_config_dword 访问扩展空间,内核会自动选择正确的访问路径。