PCI / PCIe 基础理论与配置空间结构深度剖析第3篇PCI Introduction 前言在前面的文章中我们学习了如何利用 MMIO 经典公式去访问一个特定 BDF 的物理地址。但是想要在 UEFI 环境下自由地驱动显卡、网卡或者 NVMe 固态硬盘我们必须对PCI / PCIePeripheral Component Interconnect Express的总线拓扑和配置空间Configuration Space有一个极其清晰、透彻的认知。作为 BIOS/固件工程师无论是做初始化Pei/Dxe 阶段、资源分配还是写特定的设备驱动Option ROM每天都要和 PCI 配置空间打交道。本文将作为 PCI 领域的保姆级敲门砖带你从物理拓扑、访问机制演进一字不漏地深度拆解那张固件面试必考的256 字节/4KB 配置空间标准结构图。 一、 PCI / PCIe 总线拓扑网络PCIe 虽然采用的是点对点Point-to-Point的双向串行差分总线架构但在软件和逻辑层面上它依然100%继承了传统 PCI 的树状拓扑结构。这种设计保证了向后兼容性。一个标准的 PCI 拓扑系统就像一棵大树主要由以下三类核心组件构成1. 核心组件Root Complex (RC)根复合体。它是整个 PCI 树的“根部”通常集成地区 CPU 内部或北桥中。它负责将 CPU 的内存存取请求Memory Request转换为 PCI 事务Transaction并启动 PCI 总线。Switch交换机。用于扩展总线。它可以将一个上游端口Upstream Port转换为多个下游端口Downstream Port允许在一条总线上挂载更多的外设类似于网络交换机。Endpoint (EP)端点设备。即这棵树上的“叶子”是真正的物理硬件功能实体例如独立显卡、NVMe 固态硬盘、网卡、声音芯片等。为了在这棵复杂的树上精准定位到每一个设备PCIe 沿用了经典的三元组寻址机制简称为BDFBus总线号范围0 ~ 255。Bus 0 通常分配给 Root Complex。Device设备号范围0 ~ 31。一条总线上最多可以挂载 32 个逻辑设备。Function功能号范围0 ~ 7。一个物理网卡如果带两个网口通常会表现为 Function 0 和 Function 1。 二、 配置空间访问机制的演进从 Port IO 到 MMIO硬件设备不能像瞎子一样盲目工作它必须向 BIOS 声明“我是谁”以及“我需要多少系统资源内存/IO空间”。这段专门用于设备身份识别和资源配置的硬件寄存器阵列就是配置空间Configuration Space。随着计算机架构的发展配置空间的大小和访问方式经历了两次重大演进1. 传统 PCI 阶段256 字节 Port IO在传统的 PCI 时代每个设备的配置空间仅有256 字节Bytes。CPU 无法直接通过内存地址访问它们必须借助 CPU 独立的 64KB I/O 端口空间中的两个经典端口进行“套娃式”间接访问0xCF8 (CONFIG_ADDRESS)配置地址端口。写入你想要访问的Bus:Dev:Func:Reg组合信息。0xCFC (CONFIG_DATA)配置数据端口。写入地址后从这个端口读写 4 字节的数据。2. 现代 PCIe 阶段4KB MMIO / ECAM随着 PCIe 设备功能日益复杂加入了电源管理、高级错误报告 AER、MSI-X 向量中断等256 字节完全不够用了。因此PCIe 规范将配置空间横向扩展到了4KB4096 字节。前 256 字节称为传统配置空间Compatible PCI Configuration Space保持与老设备 100% 兼容。后 3940 字节称为扩展配置空间PCIe Extended Configuration Space。由于 4KB 空间太大老旧的 0xCF8/0xCFC 端口无力支撑如此庞大的寻址。因此PCIe 引入了ECAM (Enhanced Configuration Access Mechanism)机制也就是将所有设备的 4KB 配置空间直接映射到 CPU 的全局物理内存地址空间中MMIO。正如我们上一篇文章所述只要知道了平台的PCIe_Base_Address我们就可以用纯内存指针或IoLib库直接读取任何一个寄存器。️ 三、 核心重点标准 Header 类型 0Device前 64 字节结构图虽然每个设备有 4KB 空间但其最核心的、决定设备命运的是前 64 字节0x00 ~ 0x3F这一段被称为PCI Header。Header 的类型由寄存器Header Type (0x0E)决定0x00Type 0代表普通的叶子节点外设Endpoint。0x01Type 1代表桥接设备PCI-to-PCI Bridge如 Switch。下面是面试必问、固件开发必背的Type 0 标准配置空间头结构图 字节偏移 (Offset)Bit 31 ~ 24Bit 23 ~ 16Bit 15 ~ 8Bit 7 ~ 00x00[-------- Device ID --------][-------- Vendor ID --------]0x04[--------- Status ---------][-------- Command --------]0x08Base ClassSub-ClassInterfaceRevision ID0x0CBISTHeader TypeLatency TimerCache Line Size0x10[------------------ Base Address Register 0 (BAR0) ------------------]0x14[------------------ Base Address Register 1 (BAR1) ------------------]0x18[------------------ Base Address Register 2 (BAR2) ------------------]0x1C[------------------ Base Address Register 3 (BAR3) ------------------]0x20[------------------ Base Address Register 4 (BAR4) ------------------]0x24[------------------ Base Address Register 5 (BAR5) ------------------]0x28[--------------------- CardBus CIS Pointer ---------------------]0x2C[---- Subsystem Device ID ----][— Subsystem Vendor ID —]0x30[----------------- Expansion ROM Base Address -----------------]0x34[------------ Reserved ------------]Capabilities Pointer0x38[------------------------ Reserved ------------------------]0x3CMax_LatMin_GntInterrupt PinInterrupt Line 四、 核心关键寄存器功能深度拆解为了在写代码时不当复读机我们必须理解这些核心寄存器真实的物理意义1. Vendor ID / Device ID (0x00)Vendor ID由 PCI-SIG 组织统一分配给各大厂商的唯一编码。例如 Intel 是0x8086AMD 是0x1022NVIDIA 是0x10DE。Device ID厂商自己为特定芯片定义的型号代码。避坑防错如果尝试读取某个 BDF读出来的 Vendor ID 是0xFFFF代表该位置没有物理设备挂载总线上拉电阻自动返回全1。2. Command 寄存器 (0x04)控制设备的基本行为和开关。Bit 0 (I/O Space Enable)如果置 0设备将拒绝响应任何传统的 I/O 端口请求。Bit 1 (Memory Space Enable)核心开关。如果置 0即使 BIOS 为该设备分配了 MMIO 物理地址设备也无法接收和响应任何内存读写请求。BIOS 必须在枚举完资源后将其手动置 1。Bit 2 (Bus Master Enable)决定设备能不能主动发起 DMA直接内存存取传输请求。3. Class Code 复合编码 (0x08)由三个独立的字节联合组成在 EDK2 结构体中对应为一个长度为 3 的UINT8数组ClassCode[0]Interface接口类型 / ProgIf。ClassCode[1]Sub-Class子类如 NVMe 或 AHCI。ClassCode[2]Base Class大类如存储控制器或显示控制器。作用让 BIOS 能够在没有任何具体硬件驱动的情况下一眼识别出当前卡槽里插的是什么类型的设备。4. Header Type (0x0E)Bit 6 ~ 0标志 Header 的类型0x00是标准外设0x01是桥设备。Bit 7 (Multi-Function Device Flag)极重要。如果这一位是1说明这个物理芯片是个多功能设备即它不仅有 Function 0可能还有 Function 1~7。5. BAR (Base Address Registers, 0x10 ~ 0x24)Type 0 设备拥有 6 个 32 位的 BAR 寄存器。它们是 BIOS 资源分配的核心。原理向其写入全10xFFFFFFFF再读回通过读回的值中低位有多少个0BIOS 就能计算出这个设备向系统申请了多大尺寸的内存映射空间。随后 BIOS 会分配一段无冲突的物理空地把起始物理基地址写入这个 BAR。 五、 在 UEFI 中解析 PCI 配置空间工程级代码实战在 EDK2 体系中官方在IndustryStandard/Pci.h中已经为我们精细定义好了标准头结构的结构体PCI_TYPE00。以下是完整可编译的代码演示了如何通过官方结构体正确索引ClassCode数组下标、判断多功能位精准格式化输出 PCI 属性。你可以将它直接粘贴进你的 UEFI Shell 应用工程中编译#includeUefi.h#includeLibrary/UefiLib.h#includeLibrary/IoLib.h#includeIndustryStandard/Pci.h// EDK2 官方标准 PCI 结构头定义#includeLibrary/UefiApplicationEntryPoint.h// 假定当前测试平台的 PCIe MMIO 基地址为 0xE0000000 (通常由 Intel 平台提供)#definePCIE_BASE_ADDRESS0xE0000000EFI_STATUS EFIAPIUefiMain(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE*SystemTable){Print(L\n);Print(L UEFI PCI Introduction Parser Advanced Demo \n);Print(L\n\n);// 我们以读取 Bus 0, Dev 2, Func 0 为例通常是 CPU 的集成北桥功能或核显UINT8 Bus0;UINT8 Dev2;UINT8 Func0;// 根据经典 ECAM 公式计算该 BDF 对应的全局物理内存地址UINTN DeviceConfigAddrPCIE_BASE_ADDRESS((UINTN)(Bus)20)((UINTN)(Dev)15)((UINTN)(Func)12);// 强转为标准 EDK2 PCI_TYPE00 结构体指针PCI_TYPE00*PciHeader(PCI_TYPE00*)DeviceConfigAddr;// 1. 安全读取第一个 16 位的 Vendor IDUINT16 VidMmioRead16((UINTN)(PciHeader-Hdr.VendorId));// 2. 判断设备是否存在 (0xFFFF 或 0x0000 均代表物理上无应答)if(Vid0xFFFF||Vid0x0000){Print(L[PCI ERROR] No physical device detected at BDF - %d:%d:%d\n,Bus,Dev,Func);returnEFI_SUCCESS;}// 3. 读取其他核心成员变量UINT16 DidMmioRead16((UINTN)(PciHeader-Hdr.DeviceId));UINT16 CommandMmioRead16((UINTN)(PciHeader-Hdr.Command));UINT8 HdrTypeMmioRead8((UINTN)(PciHeader-Hdr.HeaderType));// 正确通过官方定义的 ClassCode 数组下标读取对应的行业编码UINT8 InterfaceMmioRead8((UINTN)(PciHeader-Hdr.ClassCode[0]));// 编程接口 (ProgIf)UINT8 SubClassMmioRead8((UINTN)(PciHeader-Hdr.ClassCode[1]));// 子类 (Sub-Class)UINT8 BaseClassMmioRead8((UINTN)(PciHeader-Hdr.ClassCode[2]));// 基类 (Base Class)// 4. 格式化打印设备核心报告Print(L[Device Found]\n);Print(L - Location: Bus %d, Device %d, Function %d\n,Bus,Dev,Func);Print(L - Identity: Vendor ID 0x%04x, Device ID 0x%04x\n,Vid,Did);Print(L - CommandReg: 0x%04x (MemoryEnable: %s, BusMaster: %s)\n,Command,(CommandBIT1)?LYES:LNO,(CommandBIT2)?LYES:LNO);Print(L - ClassCode: BaseClass 0x%02x, SubClass 0x%02x, ProgIf 0x%02x\n,BaseClass,SubClass,Interface);// 5. 深度解析 Header Type 及其多功能属性Print(L - HeaderType: 0x%02x ,HdrType);if((HdrTypeHEADER_TYPE_MULTI_FUNCTION)!0){Print(L[Multi-Function Device]\n);}else{Print(L[Single-Function Device]\n);}// 6. 如果是标准的 Type 00 设备提取其第一个基地址寄存器BAR0if((HdrTypeHEADER_TYPE_MASK)HEADER_TYPE_DEVICE){UINT32 Bar0MmioRead32((UINTN)(PciHeader-Device.Bar[0]));Print(L - Resource: BAR0 Memory Base Address 0x%08x\n,Bar0);}elseif((HdrTypeHEADER_TYPE_MASK)HEADER_TYPE_PCI_TO_PCI_BRIDGE){Print(L - Layout: This is a PCI-to-PCI Bridge (Type 1)\n);}Print(L\n\n);returnEFI_SUCCESS;}