配置空间基础:类型0与类型1头标区结构
拿到一块PCIe设备,第一个问题就是:我怎么知道它是普通端点还是桥?答案藏在头标区里。说白了,PCIe规范把配置空间头标区(Header)分成了两种——类型0和类型1。我个人习惯把类型0叫做“设备头”,类型1叫做“桥头”。
两种头标区的设计初衷
你想想看,一个网卡需要多少配置字段?设备ID、命令、状态、基地址寄存器……差不多够了。但一个PCIe桥呢?它还要管总线号、子设备号、桥的配置命令等等。如果共用一套布局,冗余太多。所以PCI-SIG干脆定义了两套头标区——0x00~0x3F的公用部分不变,后面根据Header Type字段决定用哪种布局。
头标区结构一览(0x00 – 0x3F)
下面这张表把两种头标区的主要字段罗列在一起,你会发现多数字段是一样的,差异集中在偏移0x18以后。
| 偏移 | 字段 | 类型0(设备) | 类型1(桥) |
|---|---|---|---|
| 0x00~0x01 | Vendor ID | 设备供应商编号,只读 | |
| 0x02~0x03 | Device ID | 设备型号编号,只读 | |
| 0x04~0x05 | Command | 控制命令:IO空间、MEM空间、总线主控等使能位 | |
| 0x06~0x07 | Status | 状态位:Capabilities列表、中断状态、错误状态等 | |
| 0x08 | Revision ID | 版本号 | |
| 0x09~0x0B | Class Code | 设备分类:基类/子类/编程接口,用于驱动匹配 | |
| 0x0C | Cache Line Size | 缓存线大小,需系统软件配置 | |
| 0x0D | Latency Timer | PCI传统定时器,PCIe中用不到 | |
| 0x0E~0x0F | Header Type | 决定后续布局:0x00→类型0,0x01→类型1 | |
| 0x10~0x13 | BAR0 | 2基地址寄存器 | 2基地址寄存器 |
| 0x14~0x17 | BAR1 | 2基地址寄存器 | 2基地址寄存器 |
| 0x18~0x1B | BAR2 | 2基地址寄存器 | 2次总线号码(Secondary Bus Number) |
| 0x1C~0x1F | BAR3 | 2基地址寄存器 | Subordinate Bus Number + Secondary Latency Timer |
| 0x20~0x23 | BAR4 | 2基地址寄存器 | I/O Base / I/O Limit |
| 0x24~0x27 | BAR5 | 2基地址寄存器 | Memory Base / Memory Limit |
| 0x28~0x2B | CardBus CIS Pointer | CardBus信息结构指针 | Prefetchable Memory Base / Limit |
| 0x2C~0x2D | Subsystem Vendor ID | 子系统供应商 | |
| 0x2E~0x2F | Subsystem ID | 子系统设备ID | |
| 0x30~0x33 | Expansion ROM Base | 2扩展ROM基地址 | 桥扩展ROM基地址 |
| 0x34~0x3B | Reserved / Capabilities Ptr | 保留或Capabilities指针 | 保留 |
| 0x3C | Interrupt Line | 中断线分配(由BIOS/OS填写) | |
| 0x3D | Interrupt Pin | 中断引脚(A/B/C/D,只读) | |
| 0x3E~0x3F | Bridge Control / Min_Gnt / Max_Lat | Min_Gnt / Max_Lat(传统PCI时序参数) | Bridge Control(桥内部控制寄存器) |
头标区解读关键:Header Type字段
Header Type位于偏移0x0E,8位寄存器。位7表示多功能设备(MF),位6:0才是头标类型。举个例子:读回来0x80,那么清除位7后得到0x00,表示类型0且是单功能设备。如果读回来0x81,清除位7还是0x01,表示类型1且多功能。
类型0 vs 类型1:实战中的意义
枚举树的时候,系统软件扫描到某个设备发现Header Type为0x01,就会把它当作一个桥,立刻去读取它的Primary Bus Number、Secondary Bus Number、Subordinate Bus Number。然后继续往次级总线去扫描。如果误判了类型,枚举就乱了套。
我记得有一次,客户板子上插了一个未配置的FPGA,默认Header Type寄存器全为0xFF,导致扫描软件把它当成类型1桥,然后去读总线号,读回来全是0xFF,以为下面挂着很多设备,死循环了。后来我在FPGA代码里强制把Header Type初始化为0x00,问题解决。
如何读取头标区?一个简单的例子
在x86平台,可以通过IO端口0xCF8/0xCFC发起配置读。我写一个小函数示意:
uint32_t pci_config_read(uint8_t bus, uint8_t dev, uint8_t func, uint8_t offset) {
uint32_t address = 0x80000000 |
((uint32_t)bus << 16) |
((uint32_t)dev << 11) |
((uint32_t)func << 8) |
(offset & 0xFC);
outl(0xCF8, address);
return inl(0xCFC);
}
// 示例:读取设备00:00.0的Vendor ID
uint16_t vid = pci_config_read(0, 0, 0, 0) & 0xFFFF;
printf("Vendor ID: 0x%04X\n", vid);
避坑清单
- Header Type只读但可编程? 不一定。大多数PCIe设备的Header Type由硬件固定,但FPGA或可配置控制器允许软件重写——小心别误改。
- 类型0也有Bridge Control? 标准里没有。但一些厂商在Capabilities结构里自定义了类似功能,不要和类型1混淆。
- 读BAR时低位是只读的? 是的。BAR的最低4位(对于Memory BAR)用来表示预取属性、64位标志等,软件写全1再读可以确定BAR的大小和类型。
嗯,头标区的基础就这么多。下次碰到一个PCIe设备,先看偏移0x0E,确认它是类型0还是1,再看后面的布局——这一步对了,后面的配置就顺了。