4. 设备枚举与发现

PCIe设备的枚举与发现是驱动开发的基础。在Linux内核中,PCIe子系统的初始化过程负责扫描总线、识别设备、分配资源(如BAR地址)并建立设备拓扑。本章将深入分析内核如何通过 pci_scan_bus 系列函数完成枚举,并探讨在x86(ACPI)与嵌入式(Device Tree)平台上的差异。

4.1 PCIe枚举的核心流程

PCIe枚举的本质是深度优先遍历总线树,读取每个设备的配置空间,识别设备类型(桥或端点),并为桥设备递归扫描其下级总线。整个过程由内核PCI核心在启动时或热插拔时触发。

关键数据结构: struct pci_bus 代表一条PCI总线,struct pci_dev 代表一个PCIe设备(包括桥和端点)。枚举过程就是填充这两个结构体的过程。
PCIe枚举核心流程图 PCIe枚举核心流程 pci_scan_bus() 读取配置空间 Vendor/Device ID 判断 Header Type Type 1: PCI-PCI Bridge Type 0: Endpoint 递归 pci_scan_bus() 分配 BAR / 注册设备 枚举完成后,所有设备被加入 pci_devices 链表,等待驱动匹配。

4.2 pci_scan_bus 函数族详解

Linux内核提供了一套以 pci_scan_bus 为核心的函数族,用于不同场景下的枚举。下表总结了最常用的几个函数及其用途:

函数名 参数 典型调用场景 说明
pci_scan_bus(int bus, struct pci_ops *ops, void *sysdata) 总线号、操作函数集、系统数据 传统x86 BIOS枚举 直接扫描指定总线,不依赖ACPI
pci_scan_single_device(struct pci_bus *bus, int devfn) 总线指针、设备功能号 热插拔、指定设备扫描 只扫描单个设备/功能
pci_scan_child_bus(struct pci_bus *bus) 父总线指针 递归扫描下级总线 由桥设备发现后自动调用
pci_scan_root_bus(struct device *parent, int bus, struct pci_ops *ops, void *sysdata, struct list_head *resources) 父设备、总线号、操作函数、系统数据、资源列表 嵌入式DT/ACPI初始化 创建根总线并扫描,支持资源预分配
开发者提示: 在编写PCIe驱动时,通常不需要直接调用 pci_scan_bus。内核在启动阶段已经完成了枚举。驱动只需通过 pci_get_device() 或 pci_get_slot() 查找已注册的设备即可。

4.3 ACPI与Device Tree的枚举差异

PCIe枚举的触发方式与平台固件接口密切相关。x86平台依赖ACPI(高级配置与电源管理接口),而嵌入式ARM/ RISC-V平台则使用Device Tree(设备树)。两者在枚举流程、资源获取和设备发现上存在显著差异。

ACPI vs Device Tree 枚举对比图 ACPI vs Device Tree 枚举对比 ACPI (x86/IA64) BIOS/UEFI 枚举完成 ACPI 表提供 MCFG/资源 pci_acpi_scan_root() pci_scan_child_bus() Device Tree (ARM/RISC-V) Bootloader 传递 DTB 解析 DT 节点 pci/pcie pci_host_bridge_of_scan() pci_scan_child_bus() ACPI: 固件预枚举,内核读取表;DT: 内核解析节点,直接操作配置空间 两者最终都调用 pci_scan_child_bus() 完成递归扫描

4.4 ACPI平台枚举流程

在x86平台上,PCIe枚举的流程如下:

  1. BIOS/UEFI阶段: 固件在POST阶段完成所有PCIe设备的枚举和资源分配(包括BAR基地址、总线号分配)。
  2. 内核启动: Linux内核读取ACPI表,特别是 MCFG(内存映射配置空间基地址)和 _SB_.PCI0 等设备对象。
  3. 根总线创建: pci_acpi_scan_root() 为每个ACPI定义的PCI根总线创建 struct pci_bus。
  4. 递归扫描: 调用 pci_scan_child_bus() 遍历所有桥设备,发现下级总线上的端点。
  5. 资源确认: 内核读取配置空间中的BAR值(由BIOS分配),并建立资源树。
注意: 在ACPI平台上,内核通常不会重新分配BAR地址,而是直接使用BIOS已经配置好的值。如果BIOS分配不合理,可以通过 pci=realloc 内核参数强制重新分配。

4.5 Device Tree平台枚举流程

在嵌入式ARM/RISC-V平台上,流程有所不同:

  1. DTB传递: Bootloader(如U-Boot)将设备树二进制(DTB)传递给内核。
  2. 节点解析: 内核PCI主机桥驱动(如 pcie-designware、pcie-rcar)解析DT中的 pci 或 pcie 节点。
  3. 配置空间访问: 从DT中获取 reg 属性(配置空间基地址)、bus-range(总线号范围)等关键信息。
  4. 根总线创建: pci_host_bridge_of_scan() 或驱动自定义函数创建根总线。
  5. 硬件扫描: 内核直接通过内存映射I/O(MMIO)访问配置空间,执行 pci_scan_child_bus() 发现设备。

4.6 枚举过程中的关键数据结构

数据结构 作用 枚举阶段填充内容
struct pci_bus 代表一条PCI总线 总线号、父总线、子总线列表、设备列表、资源窗口
struct pci_dev 代表一个PCIe设备 Vendor/Device ID、Class Code、IRQ、BAR地址、总线/设备/功能号
struct pci_ops 配置空间读写操作函数集 由平台提供(ACPI使用 pci_mmcfg,DT使用 pci_generic_ops)
struct pci_host_bridge PCI主机桥抽象 根总线指针、资源列表、域编号、ACPI/DT句柄

4.7 枚举完成后的设备注册

当 pci_scan_child_bus() 发现一个有效的端点设备时,内核会执行以下操作:

  • 分配 struct pci_dev: 为设备分配内存,填充配置空间信息。
  • 读取BAR: 读取所有6个(或2个,对于桥设备)BAR寄存器的值,记录基地址和大小。
  • 分配IRQ: 读取中断引脚寄存器,分配中断号(ACPI使用 _PRT,DT使用 interrupt-map)。
  • 加入链表: 将设备加入 pci_devices 全局链表和 bus->devices 链表。
  • 触发通知: 调用 pci_bus_add_device(),通知驱动核心有新设备加入,触发驱动匹配。
关键代码路径: pci_scan_device() → pci_setup_device() → pci_read_bases() → pci_device_add() → pci_bus_add_device()

4.8 枚举与资源分配的关系

枚举过程与资源分配(BAR地址分配)紧密耦合。在ACPI平台上,资源由BIOS预分配,内核只需读取;在DT平台上,资源分配由内核的 pci_bus_assign_resources() 完成,通常在枚举之后进行。

枚举与资源分配时序图 枚举与资源分配时序 扫描设备 读取BAR 分配资源 注册设备 平台差异说明 • ACPI: 阶段1→2→4,资源分配由BIOS在阶段0完成 • Device Tree: 阶段1→2→3→4,内核在阶段3调用 pci_bus_assign_resources() • 热插拔: 阶段1→2→3→4,由 pciehp 驱动触发完整流程

4.9 常见问题与调试方法

枚举失败常见原因:
  • 配置空间不可访问: 检查MMIO基地址是否正确映射,或IO端口访问是否被禁用。
  • 总线号冲突: 多个根总线使用相同的总线号范围,导致设备无法被发现。
  • DT节点错误: bus-range 属性设置过小,导致桥设备无法分配下级总线号。
  • ACPI表缺失: 缺少 MCFG 表或 _SB_.PCI0 对象,内核无法找到配置空间。

调试命令:

# 查看所有PCIe设备
lspci -vvv

# 查看内核枚举日志
dmesg | grep -i pci

# 查看设备树中的PCI节点
ls /proc/device-tree/pci*/reg

# 查看ACPI PCI资源
cat /sys/bus/pci/devices/0000:00:00.0/resource
调试技巧: 在驱动初始化时,可以通过 pci_set_power_state() 唤醒设备,然后使用 pci_read_config_word() 读取Vendor ID来验证设备是否被正确枚举。