1.链路的训练
(硬件上电后,自发行为, 不需要软件参与)
当外部 PCIe 设备链路训练成功(LTSSM → L0)后,链路状态进入L0状态,这个是host 能够枚举到pcie 外设的前提。
只有进入L0阶段, RC才能发送TLP报文给pcie设备,读取PCIE的BAR 寄存器, 获取pcie设备信息
多级拓扑(Root Port → Switch → EP)的时候:每一段物理链路都必须进入 L0,任意一段不通,下游设备全不可见
L0: 正常工作模式
L0S: 省电模式
L1 休眠
L2:深度休眠
2. 从 Bus0 开始:遍历所有 Device / Function
PCIe 固定寻址规则:
- Bus:0~255(8 位)
- Device:0~31(5 位)
- Function:0~7(3 位)
内核循环:for (bus = 0; ...) for (dev = 0; dev < 32; dev++) for (fn = 0; fn < 8; fn++)对每个(bus, dev, fn),做一件事:读配置空间偏移 0x00 的 Vendor ID
PCIe 兼容 PCI 的配置空间寻址机制。在标准的32位配置地址寄存器(Configuration Address Register)中,地址被划分为以下几个部分:
表格
| 字段 | 位数 (Bits) | 范围 | 说明 |
|---|---|---|---|
| Reserved | 7 | - | 保留位 |
| Bus Number | 8 | 0 ~ 255 | 总线号(决定最大支持256条总线) |
| Device Number | 5 | 0 ~ 31 | 设备号 (每条总线最多32个设备) |
| Function Number | 3 | 0 ~ 7 | 功能号 (每个设备最多8个功能) |
| Register Offset | 6 | 0 ~ 63 | 寄存器偏移 (指向配置空间内的具体寄存器,需左移2位) |
3. 判断设备是否存在(最关键一步)
- 读到
0xFFFF:无设备 / 无响应→ 跳过 - 读到有效 Vendor ID(非全 1):存在设备→ 内核创建
pci_dev结构体
4. 区分是「端点 EP」还是「桥 Bridge」
读配置空间0x0E寄存器:Header Type
- Type 0 (00h)→ Endpoint(普通外设:网卡、GPU、FPGA、USB 控制器)
- Type 1 (01h)→ PCIe Bridge(Root Port、Switch 上游 / 下游口)
5. 如果是桥(Type1)→ 分配新总线,递归扫描下游
这是PCIe 树形拓扑能被扫全的核心:
- 给桥分配:
- Primary Bus:当前总线(上游)
- Secondary Bus:新总线号(下游总线起点)
- Subordinate Bus:预留最大总线号
- 以Secondary Bus为新根,递归执行步骤 2~4
- 扫完下游,回到上一级继续扫
这就是为什么 Switch 下面的设备也能被扫到:桥 = 通往新一层总线的 “门”
6. 收集所有设备后:统一分配资源
内核遍历所有pci_dev,做:
- 解析 BAR(大小、类型、预取、32/64 位)
- 分配物理地址(MMIO),避免冲突
- 分配 MSI/MSI-X 中断向量
- 配置 PCIe 能力(PCIe Link、Power、MSI-X 等)
7. 设备与驱动匹配 → 调用你的 probe
内核拿着设备的:
- Vendor ID
- Device ID
- Class Code
- Subsystem ID
去匹配内核中所有pci_driver的id_table,匹配成功就调用pcie的probe函数, 加载驱动。
pcie设备的地址空间和BAR 寄存器
BAR = 放在 PCIe 设备配置空间里的「地址资源申请表」
枚举的过程本质就是: 主机读取PCI 配置空间(Type0 头对应 Endpoint,Type1 头对应 Switch / 桥),获取设备信息、类型和BAR 寄存器, 同时给BAR寄存器分配内存。
过程:
- 读
Vendor ID(判断设备是否存在) - 读
Header Type(区分 Type0 / Type1) - 读
Class Code / Device ID(识别设备类型) - 读
BAR(获知需要多大内存空间) - 给soc内部地址空间中给pcie bar寄存器分配地址,并把这个地址回写到BAR 寄存器中。 相当于完成地址映射。
- 若为 Type1 桥 → 分配总线号,递归扫下游
什么是pcie配置空间?
---》每个pcie设备欸有自己的配置空间, soc 范围pcie的配置空间获取pcie 设备的类型、BAR寄存器。
SOC 怎么获取到pcie 设备的配置空间 地址?
--》是soc的pcie 基地址+遍历pcie 拓扑接口获取的bus等信息。(每个设备的具体配置地址 = soc pcie的基地址 + 按 Bus/Dev/Fn 计算的偏移)
Pcie的配置空间有哪些格式?
---》
PCI 配置空间只有两种头格式:
Type 0 Header:给PCIe Endpoint(终端设备)用Type 1 Header:给PCIe Bridge(桥设备)用
配置空间和BAR 寄存器 布局:
AR 寄存器(Base Address Register)总结
BAR 是PCI/PCIe 终端设备(Type 0 配置头)配置空间中(偏移 0x10~0x24,最多 6 个)的专用寄存器,是设备与主机约定物理地址资源的硬件接口,包含地址属性(32/64 位、预取、内存 / I/O 类型)和主机分配的物理基地址。
pci_host_probe的宏观职责和代码的物理动作完美对应了起来:
pci_scan_root_bus_bridge(bridge)→ 对应你标注的“扫描root和ep”。这是发现阶段,内核通过读取配置空间,把整棵总线树和设备节点“摸”出来。pci_bus_assign_resources(bus)→ 对应你标注的“分配各个ep的bar 地址空间”。这是分配阶段,内核把实际物理地址(基地址)写入每个 EP 设备的 BAR 寄存器,让它们能真正被 CPU 访问到。
这两个函数就是内核初始化 PCIe 总线的两大核心支柱:先扫描,后分配。主机给 BAR 寄存器分配并回写物理地址后,就拥有了与 PCIe 设备通信的「硬件地址入口」:驱动通过 BAR 映射的地址访问设备寄存器,既可以下发控制配置,也能间接完成网络数据等大量数据的传输,这是 PCIe 设备工作的基础。
其中Type0 Header最多有6个BAR,而Type1 Header最多有两个BAR。这就意味着,对于Endpoint来说,最多可以拥有6个不同的地址空间。但是实际应用中基本上不会用到6个,一般是1-3个BAR
假设插了一块 博通switch 网卡:
- 枚举阶段:系统读到 VID=8086、DID=10BF、Class=0200 → 识别是 博通 网卡;
- 系统给 BAR0/BAR1 分配物理地址,回写 BAR 寄存器;
- 加载 博通专用驱动 → 驱动里写死了 “BAR0 是控制寄存器,BAR1 是 DMA 描述符”;
- 驱动通过 BAR0 配置网卡、通过 BAR1 设置数据包缓冲区,实现收发包
个人理解:
配置空间里面很多信息, 其中就有bar寄存器地址。 我们知道bar 寄存器地址后, soc 先向bar寄存器写0xFFFFFFFF,看看返回多少个0,确定需要分配多大ram 空间, soc 配置分配地址以后, 把ram 地址写入到bar 寄存器。 这样bar 地址就映射到了soc内部的ram区域, 可以通过这个ram 区域和pcie 通信。
完整流程可以这样看
RC 写 BAR = 0xFFFFFFFF ↓ EP 硬件把低位不可写位清零,返回掩码 ↓ RC 算出 BAR 大小 ↓ RC/OS 在系统物理地址空间分配一段对齐地址 ↓ RC 把物理基地址写回 BAR ↓ 使能 Command Register 的 Memory Space Enable ↓ CPU 访问该物理地址 → RC 转发成 PCIe Memory TLP → EP 内部寄存器 看一下RC 驱动代码 pci_host_probe(host)pci_host_probe的宏观职责和代码的物理动作完美对应了起来:
pci_scan_root_bus_bridge(bridge)→ 对应你标注的“扫描root和ep”。这是发现阶段,内核通过读取配置空间,把整棵总线树和设备节点“摸”出来。pci_bus_assign_resources(bus)→ 对应你标注的“分配各个ep的bar 地址空间”。这是分配阶段,内核把实际物理地址(基地址)写入每个 EP 设备的 BAR 寄存器,让它们能真正被 CPU 访问到。
这两个函数就是内核初始化 PCIe 总线的两大核心支柱:先扫描,后分配。