4. Type 0 配置空间头部:Vendor ID、Device ID、Command、Status 等寄存器
好,咱们进入正题。PCIe 设备上电之后,软件怎么知道你是谁?你是个什么设备?你当前能不能干活?
答案就在配置空间的前 64 字节里,也就是 Type 0 配置头。这玩意儿是所有 PCIe 设备的身份证,也是操作系统的枚举器第一个要扒开看的东西。下面几个寄存器,我逐个给你拆开讲。
4.1 Vendor ID 和 Device ID
先看偏移地址 0x00 的这两个 16 位寄存器。
说真的,它们几乎是我判断设备身份的第一道关卡。
| 偏移 | 寄存器 | 宽度 | 描述 |
|---|---|---|---|
| 0x00 | Vendor ID | 16 bit | 厂商 ID,由 PCI-SIG 分配。0xFFFF 表示空。 |
| 0x02 | Device ID | 16 bit | 设备 ID,由厂商自己定义。与 Vendor ID 一起唯一标识设备。 |
Vendor ID:每个硬件厂商会分到一个唯一的 Vendor ID。比如 Intel 是 0x8086,AMD 是 0x1022,NVIDIA 是 0x10DE。我见过最坑的案例是某次调试时发现读出来全是 0xFFFF——嗯,配置空间没使能,或者链路根本没上来。
Device ID:厂商自己定义的设备型号。比如同是 Intel 网卡,不同代号的 ID 就不一样。这两个 ID 合起来,操作系统就能判断该加载哪个驱动。
4.2 Revision ID 和 Class Code
接下来偏移 0x08 的地方藏着两个更有意思的东西。
| 偏移 | 寄存器 | 宽度 | 描述 |
|---|---|---|---|
| 0x08 | Revision ID | 8 bit | 修订版本号,相同 Device ID 的步进版本。 |
| 0x09 | Class Code | 24 bit | 分类码:Base Class、Sub Class、Programming Interface。 |
Revision ID:简单理解就是硬件版本号。我在做 FPGA 原型验证时,经常靠 Revision ID 来区分不同版次的 RTL 代码。这招很管用,不用每次拔插看丝印。
Class Code:这个很关键。操作系统枚举完设备,第一件事就是读 Class Code。比如 0x01 表示大容量存储控制器,0x02 是网络控制器。你在 Linux 下看 lspci 输出的那个 "Ethernet controller",其实就是从 Class Code 翻译过来的。
4.3 Command 和 Status 寄存器
接下来这两个寄存器是设备的“开关”和“告示板”。地址在偏移 0x04。
| 偏移 | 寄存器 | 宽度 | 描述 |
|---|---|---|---|
| 0x04 | Command | 16 bit | 控制指令:I/O、Memory、Bus Master 等使能位。 |
| 0x06 | Status | 16 bit | 状态标志:Interrupt Status、Capabilities List 等。 |
Command 寄存器
说白了,这就是设备的电源总闸。Bit 0 控制 I/O Space 使能,Bit 1 控制 Memory Space 使能,Bit 2 控制 Bus Master。我记得刚入行时总忘写 Bus Master 使能位——结果 DMA 死活不干活,浪费一上午。
这里有个重要位我单独拎出来说:
- Bit 0 (I/O Space Enable):允许设备响应 I/O 请求。现在的 PCIe 设备基本不使能这个了。
- Bit 1 (Memory Space Enable):允许设备响应 Memory 请求。这个是大多数设备的标配。
- Bit 2 (Bus Master Enable):允许设备发起 Memory Read/Write 请求。做 DMA 前必须置 1。
- Bit 10 (Interrupt Disable):控制 INTx 中断是否被屏蔽。注意 MSI/MSI-X 中断不受这个位影响,这个坑我踩过两次。
Status 寄存器
Status 寄存器是设备状态的反光镜。这里最常用的几个位:
- Bit 3 (Interrupt Status):指示 INTx 中断状态(只读)。
- Bit 4 (Capabilities List):如果等于 1,说明设备还有 Capability 结构体在后面。这标志着配置空间不只是 64 字节,而是能扩展到 256 字节甚至 4KB。几乎所有 PCIe 设备这个位都置 1。
- Bit 5 (66MHz Capable):PCI 时代的遗留物,PCIe 里不再使用。
- Bits 10:09 (Target/Slave Abort):会告诉上层驱动出错了。调试时如果遇到这两个位置位,基本上说明总线出大事了。
我个人习惯在驱动探测的时候,先读一遍 Status 寄存器,如果有 UFE (Unsupported Request) 或 Completer Abort 等错误,就记录下来。这在我早期调试 FPGA 设备时帮了大忙。
4.4 其他头部字段
除了上面这些,前 64 字节里还有几个重要字段我也一块讲了:
| 偏移 | 字段 | 宽度 | 说明 |
|---|---|---|---|
| 0x0C | Cache Line Size | 8 bit | 系统 Cache Line 大小。PCIe 原生没强制要求,但建议设成与 CPU 一致。 |
| 0x0D | Latency Timer | 8 bit | PCI 时代的产物,PCIe 里基本无视。 |
| 0x0E | Header Type | 8 bit | 0x00 表示 Type 0(Endpoint),0x01 表示 Type 1(Bridge)。这是枚举器判断设备类型的根本依据。 |
| 0x0F | BIST | 8 bit | 内建自测试。大部分设备不支持,直接返回 0。 |
| 0x10-0x24 | Base Address Registers (BAR) | 6 × 32 bit | 设备地址映射窗口,决定系统软件能看到哪段内存。 |
| 0x2C | Subsystem Vendor ID / Subsystem ID | 2 × 16 bit | 子系统级别的身份标识。有时候主芯片 Vendor ID 是 Intel,但板卡厂商会另写自己的 Subsystem ID。 |
| 0x30 | Expansion ROM Base Address | 32 bit | 扩展 ROM 基址,用于存放 option ROM。 |
| 0x34 | Capabilities Pointer | 8 bit | 指向 Capability 结构体的首地址。如果 Status 寄存器 Bit 4 为 1,这个指针必须有效。 |
这里我多说一句 Header Type。它的 Bit 7 指示设备是不是多功能设备。如果是 0x80,说明这是一个多功能端点。在调试时,一个物理设备下藏了好几个逻辑功能,这时候就要小心了,必须等所有功能都枚举完,才能做进一步的资源分配。
4.5 知识结构图
下面我画一张图,把刚才讲的 Type 0 头部结构总结一下。这样你在脑子里可以形成一个完整的映射。
4.6 常见寄存器读写场景
理论讲完了,我结合实操经验给你几个代码片段。假设我们通过 MMIO 访问配置空间,base_addr 就是映射好的配置空间基址。
/* 读取 Vendor ID 和 Device ID */
uint16_t vendor_id = *(volatile uint16_t*)(base_addr + 0x00);
uint16_t device_id = *(volatile uint16_t*)(base_addr + 0x02);
/* 使能 Memory Space 和 Bus Master */
uint16_t cmd = *(volatile uint16_t*)(base_addr + 0x04);
cmd |= (1 << 1) | (1 << 2); /* 置位 Bit1 和 Bit2 */
*(volatile uint16_t*)(base_addr + 0x04) = cmd;
/* 读取 Status 寄存器,检查是否有 Capabilities List */
uint16_t status = *(volatile uint16_t*)(base_addr + 0x06);
if(status & (1 << 4))
printf("Device supports Capabilities List\n");
说实话,这部分内容虽然基础,但根基不牢地动山摇。如果你能把这些头部的每个字段吃透,后面谈 Capability 结构、谈 BAR 空间、谈 MSI 中断,你就知道软件是怎么一步步摸清设备底细的了。