2. PCIe配置空间的基本概念:配置寄存器、配置周期

这一节我们聊聊配置空间。说白了,它就是PCIe设备里的一块"名片区"。你想想看,系统上电后,CPU完全不认识挂在上面的各种设备——显卡、网卡、NVMe硬盘。它怎么知道这些设备是谁?需要多少资源?

答案就在配置空间里。每个PCIe功能都有一套配置空间,可以理解为设备向系统"报到"的地方。

2.1 配置空间概述

PCIe的配置空间大小为4KB,由前256字节的标准配置头部和后续的扩展配置空间组成。嗯,这个结构是从PCI时代继承并扩展而来的。

标准配置头部(前64字节) 是所有PCIe设备必须实现的,包含最基本的设备信息和资源需求。包括Vendor ID、Device ID、Command Register、Status Register、Class Code、BAR寄存器等等。

扩展配置空间(后面3968字节) 是PCIe新增的,包含能力结构(Capability Structures),如MSI中断、Power Management、AER高级错误报告、SR-IOV等功能。我个人习惯,遇到复杂问题时先去看扩展空间里的能力寄存器。

核心要点: 配置空间是软件和硬件之间的"契约"——系统通过读写配置空间来枚举设备、分配资源、控制设备行为。搞懂配置空间,你就抓住了PCIe调试的命门。

2.2 配置寄存器

我挑几个最关键的说,这些在项目中我几乎天天打交道。

2.2.1 Vendor ID 和 Device ID

这两个寄存器合起来唯一标识一个设备。

  • Vendor ID:厂商编号,比如Intel是0x8086,AMD是0x1022。由PCI SIG分配,每个厂商一个。
  • Device ID:设备型号编号,由厂商自行定义。

我在项目中遇到过一块网卡怎么都枚举不到,读Vendor ID全是0xFF,查了半天发现是复位时序没满足。说白了,设备还没就绪,你读到的配置空间全是"没设备"的状态。

2.2.2 BAR寄存器(Base Address Register)

BAR是配置空间里最重要的资源描述寄存器。它告诉系统:我需要多大的MMIO/IO空间。

BAR的读写行为很有意思。你写全1进去,再读回来,就能算出它请求的内存空间大小。嗯,这个"测BAR大小"的手法我在调试中经常用。

偏移量寄存器名称宽度描述
0x00Vendor ID16位厂商编号
0x02Device ID16位设备编号
0x04Command16位设备控制(IO/内存使能、总线主控等)
0x06Status16位设备状态
0x08Revision ID8位版本号
0x09Class Code24位设备类别(Base/Sub/Prog IF)
0x10-0x24BAR0~BAR532位基地址寄存器,最多6个
0x3CInterrupt Line8位中断线映射(Legacy中断)
0x3DInterrupt Pin8位中断引脚(INTA~INTD)

2.3 配置周期

系统怎么读取或写入某个设备的配置空间?这就是配置周期的作用。CPU其实不会直接发配置请求——它通过某种机制让RC(Root Complex)代为发起。

2.3.1 配置周期的类型

配置周期分两种:Type 0 和 Type 1。

  • Type 0配置周期:用来访问端点的配置空间。发出的目标就是具体的设备功能。
  • Type 1配置周期:用来访问桥下游设备的配置空间。桥会接收Type 1,如果目标设备在桥的次级总线上,就把Type 1翻译成Type 0;否则继续向下转发。

你想想看,枚举过程中,系统就是从RC出发,逐级发送Type 1配置请求,直到找到端点设备。我曾经在一个多级Switch的拓扑里被这个问题卡住过,Type 1的转换没搞对,导致下游设备全部不可见。

2.3.2 配置周期的访问机制

有两种方式发起配置周期:

  1. I/O映射的配置空间(Legacy方式):通过CF8h/CFCh两个I/O端口来发起。写Configuration Address到CF8h,然后读/写CFCh来访问数据。这种方式在x86上仍然存在,但PCIe已经不推荐使用。
  2. 内存映射配置空间(MMCFG,也叫ECAM):把配置空间映射到一段内存区域,直接通过Load/Store指令访问。这是PCIe的推荐方式,也叫Enhanced Configuration Access Mechanism。

MMCFG的映射关系很简单。每个总线的配置空间占1MB(256个设备功能×4KB),总线的编号不同,映射地址也不同。举个例子,访问Bus 0 Device 0 Function 0的配置空间,地址就是MMCFG基址 + 0x000000。

调试小技巧: 如果你用Linux,查看 /sys/bus/pci/devices/ 下的内容可以快速了解设备配置。手动读配置空间的话,可以用 setpci 命令。我个人在板级调试时经常用这种方法验证设备是否正常工作。

2.4 配置空间访问机制图解

下面这张图展示了配置空间结构、BDF地址与配置周期类型之间的关系。说实话,这个图我画了好几次才理清楚,尤其是Type 0和Type 1在不同段的分工。

PCIe配置空间访问机制 配置空间结构 (4KB) 标准配置头部 Vendor/Device ID, Class Code BAR, Command/Status 扩展配置空间 能力结构列表: PM, MSI, MSI-X AER, ATC, SR-IOV …… BDF地址 Bus# (8bit) ⇩ Device# (5bit) ⇩ Function# (3bit) ⇩ Offset (12bit) 配置周期类型 Type 0 配置周期 访问端点(Endpoint) 直接读写设备配置空间 Type 1 配置周期 访问桥下游设备 桥接收后转换或转发 关键点:BDF地址决定配置周期类型 桥设备通过BDF中的Bus号判断是否需要转换Type 1→Type 0

2.5 配置空间访问的注意事项

我曾经踩过的坑: 当设备处于D3hot(休眠)状态时,部分配置空间可能是不可读的。读回来的是全1或者全0,很容易误判为设备不存在。所以,读配置空间之前,记得先查一下设备的电源状态。

还有几点:

  • 配置空间是小端序的,多字节寄存器要注意字节序。我习惯在代码里做个字节交换宏来统一处理。
  • 扩展配置空间(0x100之后的区域)必须通过MMCFG方式访问,Legacy CF8/CFC只能访问前256字节。
  • 配置写操作不能保证立即生效,可能需要等待事务完成。尤其是写Command寄存器使能总线主控时,建议后面跟一个配置读来确认。

2.6 小结

这一节我们搞清楚了配置空间的结构——标准头部加扩展空间;也明白了配置周期怎么走——Type 0直达端点,Type 1过桥转换。再加上MMCFG和Legacy两种访问方式,你已经具备了理解枚举流程的基础。

嗯,配置空间这部分我当年花了很长时间才真正摸透。现在回过头看,其实就是一张表、一套地址、两种周期。你把它当作设备向系统自我介绍的标准表格,后面所有操作都围绕这张表展开。