2. PCIe总线基础

PCIe(Peripheral Component Interconnect Express)不是一条共享总线,而是一套点对点、分层、可扩展的串行互连体系。理解它的拓扑、协议分层、事务包格式、配置空间与地址映射机制,是编写 Linux PCIe 主机控制器驱动(Host Controller Driver)的前提。本节从硬件结构到软件访问路径,梳理驱动工程师必须掌握的七个核心概念。

一、PCIe 拓扑结构:从 Root Complex 到 Endpoint

PCIe 拓扑是一棵以 Root Complex(RC) 为根的树。RC 位于 CPU 与 PCIe 之间,负责把 CPU 的内存访问和配置访问转换为 PCIe 事务。RC 下方可以连接 Root Port、Switch、Endpoint(EP) 以及 PCIe-to-PCI 桥。Switch 内部由多个虚拟 PCI 桥组成,用于扩展下游端口;Endpoint 是真正完成功能的设备,如 NVMe SSD、网卡、GPU。

每个 PCIe 设备在系统中由 BDF(Bus/Device/Function) 唯一标识:总线号 8 位、设备号 5 位、功能号 3 位。一个物理设备可包含最多 8 个功能,例如多功能网卡。驱动通过 BDF 定位设备,再通过配置空间读取其资源需求。

CPU 内存访问 / MMIO Root Complex (RC) 配置空间访问 / 地址转换 / 中断聚合 Root Port 0 Bus 0 Root Port 1 Bus 1 PCIe Switch 内部虚拟 PCI 桥 / 端口扩展 NVMe SSD BDF 01:00.0 网卡 02:00.0 GPU 03:00.0 RC / CPU Root Port Switch Endpoint

二、分层协议:事务层、数据链路层、物理层

PCIe 协议分为三层,发送方向自上而下封装,接收方向自下而上解封装:

  • 事务层(Transaction Layer):生成和解析 TLP(Transaction Layer Packet),负责 Memory Read/Write、Config Read/Write、Message、Completion 等事务。它管理流量控制信用(Credit)和 QoS。
  • 数据链路层(Data Link Layer):为 TLP 添加序列号与 LCRC,生成 DLLP(Data Link Layer Packet),负责 ACK/NAK 重传、链路状态管理。
  • 物理层(Physical Layer):完成字节条带化、加扰、8b/10b 或 128b/130b 编码、差分信号发送与接收,管理 LTSSM 链路训练状态机。

三、TLP 与 DLLP:包格式与作用

TLP 是 PCIe 上承载事务的基本单元,典型结构为:Header + Data Payload + ECRC(可选)。Header 中携带 Requester ID、Tag、地址、长度、事务类型等字段。DLLP 不承载用户数据,只用于链路层控制,如 Ack/Nak、电源管理、流控信用更新。

包类型所在层典型用途是否含数据负载
TLP事务层Memory/Config/Message/Completion 事务读请求通常无,写请求有
DLLP数据链路层ACK/NAK、流控信用、电源管理无
Ordered Set物理层链路训练、时钟补偿、电气空闲无

四、配置空间、BAR 与 Capability

每个 PCIe 功能都有 4KB 配置空间。前 256 字节兼容 PCI,包含 Vendor ID、Device ID、Command、Status、BAR 等;256 字节之后是 PCIe 扩展配置空间,用于存放 Capability 结构。

BAR(Base Address Register)是设备向系统声明地址窗口的寄存器。系统启动时,BIOS 或内核枚举设备,读取 BAR 中的“所需空间大小”,再分配一段系统物理地址写回 BAR。驱动随后通过 pci_iomap() 或 ioremap() 映射该地址,访问设备寄存器。

Capability 是配置空间中的链表结构,用于描述设备支持的高级功能,如 Power Management、MSI/MSI-X、PCI Express Capability、SR-IOV。驱动通过遍历 Capability 链表识别设备能力。

关键点:BAR 决定“设备寄存器在系统地址空间中的位置”,Capability 决定“设备具备哪些可编程能力”,二者共同构成驱动初始化时的资源发现基础。

五、ECAM:配置空间的 MMIO 访问方式

传统 PCI 使用 0xCF8/0xCFC 端口访问配置空间,每次只能访问 4 字节,效率低。PCIe 引入 ECAM(Enhanced Configuration Access Mechanism),把配置空间映射为一段 MMIO 区域。ACPI 的 MCFG 表描述 ECAM 基地址与总线范围,内核据此建立 pci_mmcfg 映射。

ECAM 地址计算公式为:

ECAM_BASE + (bus << 20) + (dev << 15) + (func << 12) + offset

其中 bus 为总线号,dev 为设备号,func 为功能号,offset 为配置空间内偏移(0~4095)。Linux 内核通过 pci_mmcfg_read() / pci_mmcfg_write() 完成访问,驱动通常不直接操作 ECAM,而是使用 pci_read_config_dword() 等封装接口。

六、案例:驱动枚举一个 NVMe 设备

假设系统启动后,内核枚举到 BDF 为 01:00.0 的 NVMe 设备。驱动加载时典型流程如下:

  1. 通过 pci_get_device() 或 pci_get_subsys() 匹配 Vendor/Device ID。
  2. 调用 pci_enable_device() 使能设备内存与 I/O 空间。
  3. 读取 BAR0,得到 NVMe 控制器寄存器基地址。
  4. 调用 pci_request_mem_regions() 声明资源占用。
  5. 使用 pci_iomap() 映射 BAR0,访问 NVMe 寄存器。
  6. 遍历 Capability,确认 MSI-X 支持并申请中断。
  7. 设置 DMA 掩码,初始化队列,启动控制器。
lspci -vv -s 01:00.0
# 输出中可看到:
# Region 0: Memory at f7c00000 (64-bit, non-prefetchable) [size=16K]
# Capabilities: [50] MSI-X: Enable+ Count=16 Masked-
# Capabilities: [b0] Express (v2) Endpoint, MSI 00

七、误区与复盘

常见误区:
  • 把 BAR 当成“设备内存地址”直接使用,忽略 CPU 物理地址与 PCIe 总线地址之间的转换。RC 可能需要进行地址映射。
  • 认为配置空间只有 256 字节,忽略 4KB 扩展空间与 Capability 链表。
  • 在驱动中直接读写 0xCF8/0xCFC,而不是使用内核提供的 pci_read_config_* 接口,导致在 ECAM 平台上失效。
  • 忽略 DLLP 重传机制,误以为 TLP 丢失后事务层会重发;实际上链路层负责 ACK/NAK 与重传。
复盘建议:调试 PCIe 驱动时,先确认设备是否被枚举(lspci),再检查 BAR 是否分配成功(/proc/iomem),然后确认 Capability 中的 MSI/MSI-X 是否使能,最后才进入设备寄存器与 DMA 队列调试。这个顺序能快速定位问题位于枚举、资源分配、中断还是设备初始化阶段。