ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从PCI0._BBN推导P2P0的Bus号:ACPI PCI总线枚举链路解析

从PCI0._BBN推导P2P0的Bus号:ACPI PCI总线枚举链路解析 1. 一句绕口令背后的 PCI 枚举链路如果你调试过 ACPI DSDT/SSDT一定见过类似\_SB_.PCI0、\_SB_.PCI0.P2P0这样的节点名。最近我在一个平台项目里就翻到一句话“为了得到节点 P2P0 的 Bus 号需要先得到节点 PCI0 的_BBN BaseBusNumber”。乍看像绕口令但这句话背后其实是 PCI 总线枚举里最基础、也最容易被人忽略的一个因果关系P2P 桥的后端总线号不是凭空产生的它必须从“根桥的根总线号”一级一级推下来。这个场景出现在很多地方也许是你在调 DSDT 里的 ACPI 方法想通过 Intel 的 PCI 配置空间访问一个桥设备也许是你在写一个内核补丁想从 ACPI 命名空间里解析出某个 PCIe Root Port 的 Bus 号又或者你只是用 ACPICA 工具翻 ACPI 表想搞清楚P2P0到底挂在第几条总线上。不管哪种情况只要你想用“P2P0”这个 ACPI 节点去对应真实 PCI 总线上的设备你就绕不开PCI0._BBN。这一篇我就用项目里实际调过的东西把_BBN、BaseBusNumber、PCI0、P2P0这条链路完整拆开先讲清楚节点之间是什么关系再讲_BBN的定义和作用然后给出一条可以在 Linux 下验证的推导路径最后把我在实践里踩过的几个坑和排查命令一起整理出来。内容不追求教科书式严谨所有例子都是可复现的。1.1 PCI0 和 P2P0 到底是谁在 ACPI 命名空间里PCI0通常是一个 PCI 宿主桥Host Bridge根设备HID 一般是PNP0A03或PNP0A08。它代表一个 PCI 总线根节点也就是一整条 PCI Segment 的起点。系统枚举 PCI 时首先从它这里拿到“根总线号”然后才能往下扫描。P2P0这个名字更像是一个“占位名”不同厂商叫法不一样有的叫RP01、PC01、P0P1、BR00。从语义上看它多半是一个 PCI Express Root Port 或者 PCI-to-PCI Bridge挂在 PCI0 这个根桥下面。ACPI 命名空间里的父子关系不一定等于 PCI 总线拓扑关系但在这个常见例子里可以粗略理解为\_SB_.PCI0 - PCI Root Bus根桥 \_SB_.PCI0.P2P0 - Root Port / PCI-PCI Bridge桥或端口 \_SB_.PCI0.P2P0.XXXX - 下游设备不过这只是“命名空间父子关系”真正决定 P2P0 是哪个物理设备的是_ADR。_ADR返回一个 32 位整数高 16 位是 Device 号低 16 位是 Function 号。比如0x00010000表示它在父总线上是 Device 1、Function 0。操作系统通过这个地址去扫描配置空间才能知道它到底是不是一个桥设备。很多人容易把“ACPI 节点名”和“Bus 号”混在一起以为 P2P0 的 Bus 号写在某个名字里。实际上 ACPI 名字只是固件给节点起的标签Bus 号是枚举过程中动态分配出来的。这也正是那句“要得到 P2P0 的 Bus 号必须先得到 PCI0 的 _BBN”的关键所在。1.2 为什么 P2P0 的 Bus 号不能直接读出来PCI 总线的扫描过程是递归的。操作系统先从根桥处获知“根总线号”然后扫描这条总线上的所有设备如果某个设备是 PCI-to-PCI Bridge就给它分配一个“次级总线号”Secondary Bus Number再对这个新总线做递归扫描。所以想要知道 P2P0 所在的总线号本质上要知道它作为桥设备的“次级总线”是多少。而这个值是在扫描配置空间时由操作系统写入 PCI 配置寄存器0x19的。在扫描完成之前它不是一个天花板上下来的固定值而是由以下因素决定根总线的起始号就是PCI0._BBN。P2P0 在父总线上占用的 Device/Function_ADR。枚举时已经分配过多少条总线。桥配置寄存器 Primary / Secondary / Subordinate 的当前值。如果一个系统里只有 PCI0 一个根桥_BBN往往是 0P2P0 是它的下游第一个桥那么枚举后 P2P0 的 Secondary Bus 很可能就是 1。但这不是数学公式而是一个“可能”的结果。如果系统有多个 Segment或者根总线号不是从 0 开始那么 P2P0 的 Bus 号就必须从_BBN开始推导。换句话说PCI0._BBN是整个推导链的锚点。锚点错了后面全是错的。2._BBN是什么BaseBusNumber 的核心作用2.1 ACPI 规范里的_BBNACPI 规范里对_BBN的定义很直接Base Bus Number一个整数表示该 PCI 根桥所管理的根总线号。对 PCI 设备_BBN最常见的表现形式是Device (PCI0) { Name (_HID, EisaId (PNP0A03)) Name (_SEG, 0) Name (_BBN, 0) Name (_CRS, ResourceTemplate () { WordBusNumber (ResourceProducer, MinFixed, MaxFixed, PosDecode, 0x0000, 0x0000, 0x00FF, 0x0000, 0x0100) }) }这里_BBN等于 0表示根总线是 Bus 0。_SEG等于 0表示 Segment 0。在多 PCI Domain 的环境下_BBN只能表示根总线在这一段内的本地编号真正的完整总线号需要_SEG_BBN联合起来看。有一点需要注意_BBN可以是Name直接写死也可以是Method动态返回。不管哪种形式操作系统只需要它在枚举根桥之前求值一次。Linux 内核在启动流程里会从 ACPI 根桥设备的 handle 上 evaluate_BBN把返回值作为这个 root bus 的 bus number。2.2_BBN和_CRS的分工很多 DSDT 里同时存在_BBN和_CRS容易让人困惑既然_CRS里已经有WordBusNumber为什么还要_BBN_BBN表达的是“当前根总线号是多少”而_CRS表达的是“这个根桥可使用的总线资源范围”。比如上面_CRS里的WordBusNumber范围是 0x00 到 0xFF长度 0x100那操作系统就知道这个根桥一共能管理 256 条总线。但_CRS里的 Min 值不一定等于_BBN因为根桥可以报告一个很大的资源窗口但实际配置的根总线段号是_BBN。实战里最常见的 BIOS 行为是_BBN 0_CRS的 BusNumber 窗口是 0x00-0xFF。_BBN 0x18_CRS的 BusNumber 窗口可能是 0x00-0xFF也可能从 0x18 开始。_BBN存在且非 0但是_CRS窗口起始不对导致操作系统枚举出来的总线和_BBN对不上。所以_BBN是“用来定位根总线”的_CRS是“用来规划可用总线范围”的。顺序上OS 总是先 eval_BBN再解析_CRS里的资源。如果_BBN没有就得靠_CRS里 BusNumber 窗口的最小值来猜。这两种行为在 ACPI 表不一致的机器上会产生完全不同的枚举结果。2.3_BBN与_ADR的关系_BBN回答的是“我在哪一条总线”的根层问题_ADR回答的是“我在父总线的哪个槽位”。P2P0 作为一个 PCI 设备它的_ADR告诉你它在父总线上的 Device/Function而它的父总线是 PCI0 的根总线这个根总线由_BBN标定。所以完整的区位信息是信息来源作用Segment / Domain_SEG区分不同 PCI DomainRoot Bus Number_BBN根桥所在总线号Parent Bus Number枚举结果P2P0 所在父总线通常等于根总线Device / Function_ADR在父总线上的槽位Secondary Bus Number桥配置空间 0x19桥下游子总线的 Bus 号一句话总结_BBN是根_ADR是位Bus 号是枚举后从根出发算出来的“距离”。3. 从 PCI0._BBN 到 P2P0 的 Bus 号完整推导链路3.1 第一步先拿到 PCI0 的_BBN要拿到_BBN最直接的方法是看 ACPI 表。在 Linux 下先把 DSDT 导出来sudo acpidump -o acpi.tbl acpixtract -a acpi.tbl iasl -d dsdt.dat grep -n _BBN\|PCI0\|P2P0 dsdt.dsl如果 DSDT 里 PCI0 的_BBN是Name (_BBN, 0)那就说明根总线号是 0。如果它是个 MethodMethod (_BBN, 0) { Return (0x0018) }那根总线号就是 24 号。拿到这个数以后就得到了整条链路的起点。在 Linux 内核里这个信息会被保存到struct acpi_pci_root中。比如root-bus-number就是_BBN对应的根总线号。调试时看 dmesg 能直接看到类似ACPI: PCI Root Bridge [PCI0] (domain 0000 [bus 00-ff])如果内核打印的bus 00-ff是从 0 开始的说明_BBN生效了。如果看到的是bus 18-ff说明_BBN不是 0后续所有桥的总线号都会基于 24 往上加。3.2 第二步根据_ADR定位 P2P0 的 BDF拿到根总线号后再来看 P2P0 的_ADR。假设 DSDT 里写的是Device (P2P0) { Name (_ADR, 0x00010000) // Device 1, Function 0 Name (_BBN, 0x1) // 有些固件会给桥设备也写 _BBN ... }注意P2P0 的_BBN和 PCI0 的_BBN含义完全不同。PCI0 的_BBN是根总线号P2P0 的_BBN通常表示“这个桥自己所在的总线号”但桥设备本身的_BBN不一定可靠而且很多桥设备根本不会提供_BBN。因此我还是以 PCI0 的_BBN作为第一手锚点。假设PCI0._BBN 0那 P2P0 的父总线大概率是 Bus 0它在 Bus 0 上的 BDF 是00:01.0Device 1, Function 0。如果你在 Linux 下用lspci -tv看到一条这样的路径-[0000:00]--00.0 Intel Host Bridge -01.0-[01]----00.0 Some Device这里01.0-[01]前面的01.0表示一个桥设备在 Bus 0 上的 BDF 是 01.0中括号里的[01]表示它下游的二级总线是 Bus 1也就是 P2P0 桥所管理的子总线的 Bus 号。3.3 第三步读桥配置空间确认 Secondary Bus如果只想知道 P2P0 下游总线的实际编号最快的验证方式是直接读 PCI 桥的配置空间。PCI-to-PCI Bridge 的配置空间里这几个寄存器非常关键0x18Primary Bus Number即桥所在的父总线号。0x19Secondary Bus Number即桥下游子总线的 Bus 号。0x1ASubordinate Bus Number即桥下游能到达的最大 Bus 号。用setpci就能直接读sudo setpci -s 00:01.0 0x18.l比如我曾在某块板子上读到0001ff00字节展开就是0x18: 00 // Primary Bus 0 0x19: 01 // Secondary Bus 1 0x1A: FF // Subordinate Bus 255这个结果说明 P2P0 桥设备本身在 Bus 0它下游的子总线从 Bus 1 开始。和PCI0._BBN 0完全对得上。如果PCI0._BBN是 0x18那么读出来的 Primary Bus 可能也是 0x18Secondary Bus 可能是 0x19、0x20 或者更大的值。这个值取决于枚举时 OS 分配的顺序不能想当然地认为是_BBN 1。这里我想强烈建议一句话任何时候推导 P2P0 的 Bus 号都要先读一遍PCI0._BBN然后读桥的0x18/0x19/0x1A配置空间两者互相印证。因为固件里写的逻辑不一定和 OS 枚举结果一致但配置空间里的值一定是最新的真实结果。4. 实操中必踩的坑_BBN缺失、异常和多级桥4.1_BBN缺失时OS 默认值不一定是 0不少老旧 BIOS 只在_CRS里写了 BusNumber 范围根本没写_BBN。这种情况下操作系统会退而求其次从_CRS的WordBusNumber资源里取 Min 值作为根总线号或者直接按 0 处理。问题来了如果_CRS里的 Min 是 0x18而某个 ACPI 方法里硬编码了P2P0的 Bus 号是 0x19那当前系统里如果实际枚举从 0 开始你的硬编码就全错了。我调过的某款主板上DSDT 里 PCI0 没有_BBN但_CRS把 BusNumber 窗口写成0x00-0xFF结果 OS 把根总线认成 0。可是固件内部一段 AML 代码却用0x01去访问 P2P0导致访问到了错误的设备。最终修法不是去调 AML而是把 PCI0 的_BBN补成 0保证口供一致。所以当你看到“为了得到节点 P2P0 的 Bus 号需要先得到节点 PCI0 的 _BBN”这种话第一反应不是去搜 P2P0 的_BBN而是去查 PCI0 的_BBN是否真的存在、是否被 OS 接受。4.2_BBN和_SEG的组合问题多 Segment 系统里_SEG不能忘。PCI Segment 在 Linux 里就是 Domainsysfs 里的设备路径会显示为0000:xx:yy.z前面的0000就是 domain/segment。如果只处理_BBN而忽略_SEG你可能在 segment 1 上找到了_BBN 0的根桥结果把 segment 1 的总线号和 segment 0 混在一起。典型的多根桥表是这样Device (PCI0) { Name (_SEG, 0) Name (_BBN, 0) } Device (PCI1) { Name (_SEG, 1) Name (_BBN, 0) }看起来两个_BBN都是 0但它们属于不同 Segment。Linux 内核里计算完整 PCI 域时会用_SEG表示 domain_BBN表示 bus number。你在推导 P2P0 的 Bus 号时一定要把 Segment 一起带上写成segment:bus:device.function。排查时可以这样看cat /sys/bus/pci/devices/0000:01:00.0/uevent重点看PCI_SLOT_NAME、PCI_BUS_NUM和PCI_DOMAIN三个字段domain 对应_SEGbus num 对应经过_BBN推导出来的总线号。4.3 多级 P2P 桥不能简单用_BBN 1我碰到过最典型的一个错误认知是以为 P2P0 的 Bus 号一定等于PCI0._BBN 1。如果 PCI0 下面只挂一个桥那确实通常是这样但只要有两个桥、三条总线枚举顺序就可能变成Bus 0: PCI0 根总线 Bus 1: P2P0 桥的下游总线 Bus 2: 另一个 P2P 桥的下游总线 Bus 3: 又一个桥挂在 Bus 2 下面还有一种情况桥设备被枚举时不一定按照命名顺序分配总线号。OS 可能先给 P2P0 分配 Bus 1给 P2P2 分配 Bus 2也可能因为资源冲突把 Bus 号整体往后挪到 0x10、0x11。因此_BBN只能保证你知道起点不能保证你知道终点。想要确认最终结果唯一可靠的办法是读桥配置空间。或者用lspci -tv看树形关系不要靠猜。4.4 排查命令速查表下面这套命令是我在 Linux 下调P2P0总线号时每次都会跑的贴在项目笔记里目的命令抓取 ACPI 表sudo acpidump -o acpi.tbl解压 DSDTacpixtract -a acpi.tbl iasl -d dsdt.dat查看_BBN/_SEGgrep -n -E _BBN查看 PCI 树lspci -tv查看指定设备细节lspci -s 00:01.0 -vvv读桥总线号寄存器sudo setpci -s 00:01.0 0x18.l查看 sysfs 连接关系ls -l /sys/bus/pci/devices/0000:00:01.0/看内核初始化日志dmesg如果lspci -tv显示 P2P0 下游总线是中括号[01]那setpci读出来的 0x19 也必须是 1两者不一致时优先信配置空间里的值再回头查 DSDT。5. 项目里我总结出的几条实操习惯这类问题看似是纯 ACPI 知识但真正落地时很多坑都来自“固件设计者预想的枚举结果”和“OS 实际枚举结果”之间的偏差。我在项目里习惯性遵守这么几条原则第一先看根再看桥最后才看叶子设备。碰到任何 P2P0 相关的问题先确认PCI0._BBN的值再setpci读一次桥的配置空间。没有这两个数据不要下结论。第二ACPI 表里的_BBN和_CRS不能只信一个。很多 OEM 机器上这两个字段并不完全一致。Linux 内核的跟踪日志会打印最终采用的总线号范围那才是对的。以dmesg里pci_bus 0000:00: root bus resource打印的信息为准。第三改 DSDT 后不要只在虚拟机上验证。我曾在 QEMU 里模拟 PCI0._BBN0 的拓扑完全正常加载到真机上就发现_SEG影响了 bus 分配。ACPI 表生成的随机错误往往只在特定固件下复现。第四验证_BBN最稳的方法是写一个小 AML 方法来返回值而不是靠肉眼读 DSDT。比如在 PCI0 里加一个临时 Method把_BBN存到 ACPI 调试设备或日志里。不过这个需要改表只适合在固件开发阶段做。最后分享一个小技巧如果你手上只有跑起来的 Linux 环境又不想重新编译内核可以直接看/proc/ioports或/sys/bus/pci里的信息但最推荐的还是lspci -vvv。它会把每个桥设备的Bus: primary00, secondary01, subordinateff打印得清清楚楚。你只要把它和setpci读到的 0x18/0x19/0x1A 对上P2P0 的 Bus 号就永远不会再搞错。
返回列表