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设备(包括桥和端点)。枚举过程就是填充这两个结构体的过程。
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(设备树)。两者在枚举流程、资源获取和设备发现上存在显著差异。
4.4 ACPI平台枚举流程
在x86平台上,PCIe枚举的流程如下:
- BIOS/UEFI阶段: 固件在POST阶段完成所有PCIe设备的枚举和资源分配(包括BAR基地址、总线号分配)。
- 内核启动: Linux内核读取ACPI表,特别是
MCFG(内存映射配置空间基地址)和_SB_.PCI0等设备对象。 - 根总线创建:
pci_acpi_scan_root()为每个ACPI定义的PCI根总线创建struct pci_bus。 - 递归扫描: 调用
pci_scan_child_bus()遍历所有桥设备,发现下级总线上的端点。 - 资源确认: 内核读取配置空间中的BAR值(由BIOS分配),并建立资源树。
注意: 在ACPI平台上,内核通常不会重新分配BAR地址,而是直接使用BIOS已经配置好的值。如果BIOS分配不合理,可以通过
pci=realloc 内核参数强制重新分配。
4.5 Device Tree平台枚举流程
在嵌入式ARM/RISC-V平台上,流程有所不同:
- DTB传递: Bootloader(如U-Boot)将设备树二进制(DTB)传递给内核。
- 节点解析: 内核PCI主机桥驱动(如
pcie-designware、pcie-rcar)解析DT中的pci或pcie节点。 - 配置空间访问: 从DT中获取
reg属性(配置空间基地址)、bus-range(总线号范围)等关键信息。 - 根总线创建:
pci_host_bridge_of_scan()或驱动自定义函数创建根总线。 - 硬件扫描: 内核直接通过内存映射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() 完成,通常在枚举之后进行。
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来验证设备是否被正确枚举。