ASAravind Sivakumaran
Theme

Theme and appearance

Appearance
Palette

Pick any colour. The displayed accent adjusts for readable contrast in light or dark mode.

← PCIe guide

PCIe / BOOT DISCOVERY

How does Linux find a PCIe device at boot?

Picture a new caretaker arriving at a block of flats at night. The landlord leaves a binder: where the front desk is, which floors belong to this building, which street addresses its post room handles. The binder does not list the tenants. So the caretaker walks every corridor and knocks on every door. Some doors open onto stairs to more corridors; the caretaker follows them too. At the end every tenant has a mailbox number and a doorbell that rings in the right place.

That is PCIe discovery. The firmware (UEFI or BIOS on a PC, a bootloader's Device Tree on most Arm boards) is the binder: it describes the host bridge, the place where the CPU's world meets PCIe, but not the cards behind it. Linux then scans: it reads the ID of every possible device, follows every bridge, and builds the tree you see in lspci. The same scan code runs on x86 and Arm; what differs is how Linux finds the host bridge, and how much the firmware already set up.

Two ways through this page:

What claims does this page make? 4 points
  • Linux behaviour: read in the Linux v7.2 source, named function by function in each lab and in Sources.
  • Public specification: ACPI 6.6 for _HID, _CID, _CRS, _OSC, _PRT, _BBN and _SEG. The MCFG table and the PCIe and CEM specifications are PCI-SIG documents for members only; they are cited by section, never quoted.
  • Measured: the boot logs and the driver's output in Three real boots and A driver were printed by mainline Linux 7.2.0 on QEMU 8.2 (q35 for x86; virt for Arm, with a Device Tree and with UEFI and ACPI).
  • Teaching model: the labs run on a made-up fabric (vendor ID 1ee7, from the earlier chapters) laid out like QEMU's machines. Its log lines are generated in Linux's formats; they were not printed by a kernel. Lab 04's SoC is made up.

The short version: five steps, two machines

A PC hands Linux ACPI tables. An Arm board usually hands it a Device Tree (a DTB, compiled from a .dts text file). Arm servers use ACPI too; the full version covers them. Read across a row to compare. The two columns describe the QEMU machines this chapter boots for real, q35 and virt; other firmware and other host bridge drivers make different choices.

Three words first. Every PCI function has configuration space: a small block of registers (256 bytes, or 4 KiB on PCIe) that says who it is and how it is set up. ECAM maps every function's block into memory, so a read from one address reaches one register. A BAR (Base Address Register) is one of those registers: it says where in memory the device's own registers are.

StepPC (x86, ACPI)Arm board (Device Tree)
1. Firmware describes the machineUEFI or BIOS leaves ACPI tables in memory: the MCFG says where configuration space is, the DSDT describes the devices.The bootloader (U-Boot, or UEFI) passes the DTB's address to the kernel.
2. Linux finds the host bridgeA DSDT device whose ID is PNP0A08 (PCI Express) or PNP0A03 (PCI).A node whose compatible string a host bridge driver knows, such as "pci-host-ecam-generic".
3. Linux learns how to reach configuration spaceFrom the MCFG's ECAM base, plus the old I/O ports 0xcf8/0xcfc.From the node's reg; or the controller's driver must power it, train the link and program it first (lab 04).
4. Scan: knock on every doorThe same code on both, drivers/pci/probe.c: read every device's Vendor ID, follow every bridge (lab 01).
5. Addresses and interruptsOn q35 firmware already numbered the buses and placed every BAR; Linux checks them and keeps what is valid. Interrupt wiring comes from _PRT.On virt nothing is set up, and the generic host driver numbers the buses and places every BAR inside the node's ranges. Interrupt wiring comes from interrupt-map.

After step 5 the two machines are the same: each function is a struct pci_dev, and a driver whose ID table lists the function's vendor and device ID is probed (its initialization runs; it can still fail). Probing is also when Linux turns memory decoding on for the card and the bridges above it, if firmware has not already: a BAR with an address decodes nothing until then.

How Linux reaches the ACPI tables: UEFI points to the RSDP, the RSDP to the XSDT (or the older RSDT), and that lists the FADT, MCFG, SSDT and other tables; the FADT points to the DSDT. UEFIconfig table RSDPthe pointer XSDT / RSDTlist of tables FADTfixed data DSDTPCI0 lives here MCFGECAM regions SSDT, APIC …more tables
On a PC: UEFI's configuration table (or, with an old BIOS like q35's SeaBIOS, a signature Linux searches for in low memory) points to the RSDP (Root System Description Pointer), which points to the XSDT (Extended System Description Table), the list of every other table; an ACPI 1.0 RSDP like SeaBIOS's points to the older 32-bit RSDT instead. The FADT (Fixed ACPI Description Table) points to the DSDT (Differentiated System Description Table), whose AML bytecode Linux's ACPI interpreter loads to build the namespace, where \_SB.PCI0, the host bridge, lives. The MCFG is plain data: where configuration space is. None of them lists the cards: the scan finds those.

Three things share the word "bridge" and are easy to mix up. The host bridge (PCI0 in ACPI, pcie@10000000 in the Device Tree) is where the CPU's world meets PCI; it has no bus address of its own. The root port, 00:01.1 in the labs (00:00.0 on lab 04's DesignWare board), is a PCI function below it, found by the scan like any card. And many PCs also show a function at 00:00.0 whose class says "Host bridge": that is a PCI function the chipset exposes, not the ACPI object.

See it on your own machine

Every command here only reads. Output is from the QEMU boots on this page; yours will differ in numbers, not in shape.

sudo dmesg | grep -iE 'ecam|root bridge|host bridge'
Where Linux found configuration space and the host bridge. A PC prints PCI: ECAM [mem 0xb0000000-0xbfffffff] ... and ACPI: PCI Root Bridge [PCI0] ...; an Arm board pci-host-generic 4010000000.pcie: ECAM at [mem 0x4010000000-0x401fffffff] for [bus 00-ff].
ls /sys/firmware/acpi/tables or ls /sys/firmware/devicetree/base
Which binder this machine got. The Arm virt machine with UEFI listed APIC DBG2 DSDT FACP GTDT IORT MCFG PPTT SPCR; with a Device Tree, the first directory does not exist and the second holds pcie@10000000.
lspci -D, then readlink /sys/bus/pci/devices/address
Pick an address from the first command's list (on your machine 03:00.0 may be something else, or nothing). The second shows the path from the host bridge down to that function, one bridge per step. On the Arm board, for the card at 0000:03:00.0, it starts at the platform device the DT node became: ../../../devices/platform/4010000000.pcie/pci0000:00/0000:00:02.0/0000:01:00.0/0000:02:00.0/0000:03:00.0.
ls -l /sys/bus/pci/devices/*/firmware_node
On ACPI machines, the ACPI object that describes each function, if firmware described it: on q35, 0000:00:02.0 → .../PNP0A08:00/device:02 (a counter, not the slot). Nothing on a DT board, whose tree has no node per card.
lspci -tv
The tree the scan built: domain, buses and bridges.

What each thing you see proves, and what it does not

A card in lspci is not yet a working card. Each step of boot leaves its own evidence, and each piece answers one question only:

You seeIt tells youIt does not tell you
A host bridge in ACPI or the Device Treewhere one PCI tree starts, its buses and its address windowswhich cards are plugged in
An MCFG entry, or reg in the nodewhere configuration space isanything about a particular device
The function in lspcisomething answered a configuration read at that addressthat its BARs were placed, or that a driver works
No "can't claim", "can't assign" or "failed to assign" for it in dmesgLinux did not log a failureanything positive: a missing error proves nothing, and a log can be filtered or rotated
lspci -vv: "Region 0: Memory at ..." for the card, and a "Memory behind bridge" window on every bridge above it that contains the whole region (/proc/iomem nests them the same way)the BAR has an address that every bridge routes down to the card (lab 02 prints each check)that the card answers: decoding may be off, and the card may be in reset. Nor which memory the card itself may write: that is the IOMMU's job
lspci -vv: "Control: ... Mem+" on the card and on every bridge above itthe card and the bridges will accept memory accesses to the addresses in their windows, if the BAR is placed (row above)that the card answers: the link may be down or the card in reset or a low-power state. Its registers holding sane values shows only in the driver's first read of an ID or status register
_OSC: OS now controls [... AER ...]Linux, not firmware, handles AER, PME and hotpluganything about security; it authenticates nothing and grants no DMA
A driver link in the function's sysfs directorya driver's probe succeededthat the device's first real job worked
Its count rising in /proc/interruptsinterrupts arrivethat a particular transfer finished with the right data

Check yourself

Does the MCFG list your network card?

No. It lists regions of configuration space (one per segment and bus range). Linux finds the card by reading configuration space.

Can 03:00.0 exist with no ACPI object of its own?

Yes. PCI describes itself through configuration space; ACPI adds only what the platform must say. On the Arm board no card has a Device Tree node either.

Would the card use the ECAM address of its BAR register for DMA?

No. That address is where the CPU reads the register. The four addresses below each have a different job.

The card is in lspci but its driver never starts. Where do you look first?

dmesg for that address: "can't claim" means firmware's address lies in no bridge window above the card, and "can't assign" means no window had room (lab 02 and lab 03's What ifs); "probe with driver ... failed with error" means the driver ran and gave up. A driver that says "not mine" (-ENODEV) logs nothing by default, and one that defers (waiting for something else) is only reported later, if it never gets what it waits for (drivers/base/dd.c). Reinstalling the driver fixes none of these.

Firmware describes the host bridge to Linux's ACPI or Device Tree code, which starts the host bridge driver. The PCI core scans from there through the root port and the switch to the card, whose driver then binds. The card's legacy interrupt goes up to the root port and on to the interrupt controller. FirmwareACPI tables or DTB Linux: ACPI / OFfinds the host bridge Host bridgeECAM, windows Switch01:00.0, 02:00.0 Root port00:01.1 PCI core scandrivers/pci/probe.c Card 03:00.0IDs, BAR0, INTA Driverbound by ID InterruptsIO-APIC or GIC

The labs

Five boots of one made-up fabric: two host bridge functions on bus 00, the root port 00:01.1 (00:00.0, alone on bus 00, in lab 04), a switch and the card 03:00.0, with firmware laid out like QEMU's machines. The diagram's labels follow the machine each lab boots. It is a model in your browser, not a capture from the author's N100 or a test of your computer. A "What if" runs a whole boot on its own copy with one thing changed. Predict each result before you take the step.

Lab

Also true:

    What firmware handed over

      The tree so far

        Kernel log, as the model prints it

          Configuration reads, one by one

          Each Vendor ID read that found a device, and the path it took: port 0xcf8 with the dword written to it, or the ECAM address. Reads that found nobody are counted, not listed. After placement it also lists each window check.

            The full version

            The order things happen on a PC

            Linux starts subsystems in initcall levels, each finishing before the next. PCI discovery on x86 spans four of them:

            LevelFunction (file)What it doesWhat q35 printed
            archpci_arch_init() (arch/x86/pci/init.c)Probes configuration mechanism #1 (ports 0xcf8/0xcfc), then pci_mmcfg_early_init() parses the MCFG. For BIOSes dated before 2016 it also checks that the ECAM range is reserved in the E820 map; for newer ones it checks nothing yet.PCI: ECAM [mem 0xb0000000-0xbfffffff] (base 0xb0000000) for domain 0000 [bus 00-ff], reserved as E820 entry (SeaBIOS is dated 2014), PCI: Using configuration type 1 for base access
            subsysacpi_init() (drivers/acpi/bus.c)Only if early boot turned ECAM down, pci_mmcfg_late_init() tries again, now accepting a range that ACPI motherboard resources (PNP0C01/PNP0C02) or the EFI memory map reserve. Then acpi_scan_init() walks the namespace; each host bridge object reaches acpi_pci_root_add(), which scans the buses behind it.ACPI: PCI Root Bridge [PCI0] (domain 0000 [bus 00-ff]), then the scan's lines
            subsyspci_subsys_init() (arch/x86/pci/legacy.c)Sets up interrupt routing through ACPI. Only if that is impossible (booted with acpi=noirq, say) does it scan bus 0 the old way; it also probes for host bridges the BIOS reported but ACPI did not.PCI: Using ACPI for IRQ routing
            fspcibios_assign_resources() (arch/x86/pci/i386.c)Places whatever firmware left unplaced.nothing here: firmware placed it all
            device, or module loaddriversBind to the functions the scan found.the driver's own lines

            On a Device Tree board the order is simpler: of_platform_default_populate_init() (at arch_initcall_sync) turns every node into a platform device, and the host bridge driver's probe, whenever its driver registers, does steps 3 to 5 in one call to pci_host_probe().

            Configuration space: two doors

            Each function has 4 KiB of configuration space. x86 has two ways in. Mechanism #1 writes an address to I/O port 0xcf8 (bit 31 enable, then bus, device, function and a dword offset) and reads the data from 0xcfc; it reaches only the first 256 bytes (AMD's "IO ECS" extension aside). ECAM maps all of it into memory, 4 KiB per function, 1 MiB per bus. Linux's raw_pci_read() uses the ports for domain 0 below offset 256 and ECAM for everything else; Arm has no I/O ports, so it is ECAM (or a controller's own scheme) for everything.

            Where is one register? All numbers in hexadecimal.

            Four addresses, four jobs

            The same card on the PC has four numbers that all look like addresses. Mixing them up is the most common confusion in this whole topic:

            Address ofInOn the PCUsed by
            Where its BAR0 register isconfiguration space, through ECAM0xb0300010the CPU, to read or size the BAR
            What BAR0 holdsthe card's register window0xfe800000firmware or Linux, by writing BAR0
            One register inside itBAR0 + 0x20, memory-mapped I/O0xfe800020the driver, with readl() or writel()
            Where the card writes dataa DMA address (IOVA) the driver handed it0x4000the IOMMU translates it to RAM (Before DMA, One NIC)

            If Linux moved BAR0 (as it does on the Arm board), the second and third rows would change and the first would not: the register's location is fixed by the card's bus, device and function.

            The MCFG, byte by byte

            The MCFG is defined by the PCI Firmware Specification (r3.3 §4.1.2, PCI-SIG members only). Linux parses it in pci_parse_mcfg() (arch/x86/pci/mmconfig-shared.c) on x86 and pci_mcfg_parse() (drivers/acpi/pci_mcfg.c) on arm64. On q35 it is 60 bytes: the standard 36-byte ACPI header, 8 reserved bytes, and one 16-byte entry per ECAM region.

            OffsetSizeFieldq35
            0x0036ACPI header: signature MCFG, length, revision, checksum, OEM IDslength 60
            0x248reserved0
            0x2c8base address of the ECAM region0xb0000000
            0x342PCI segment group (the domain)0
            0x361start bus0x00
            0x371end bus0xff
            0x384reserved0

            The base address is where bus 0 would be, even when the start bus is not 0: Linux computes the region as base + (start << 20). A host bridge added later (hotplug) has no MCFG entry; its _CBA method gives its ECAM base instead.

            The host bridge in ACPI

            The DSDT's host bridge, as firmware might write it in ASL (it compiles with iasl; q35's real one also has _OSC, more windows and link devices for interrupts):

            Device (PCI0)
            {
                Name (_HID, EisaId ("PNP0A08"))  // PCI Express host bridge
                Name (_CID, EisaId ("PNP0A03"))  // ... and also a PCI host bridge
                Name (_SEG, Zero)  // segment (domain) 0000
                Name (_BBN, Zero)  // first bus number
                Name (_CRS, ResourceTemplate ()
                {
                    WordBusNumber (ResourceProducer, MinFixed, MaxFixed, PosDecode,
                        0x0000, 0x0000, 0x00FF, 0x0000, 0x0100)
                    IO (Decode16, 0x0CF8, 0x0CF8, 0x01, 0x08)
                    WordIO (ResourceProducer, MinFixed, MaxFixed, PosDecode, EntireRange,
                        0x0000, 0x0000, 0x0CF7, 0x0000, 0x0CF8)
                    WordIO (ResourceProducer, MinFixed, MaxFixed, PosDecode, EntireRange,
                        0x0000, 0x0D00, 0xFFFF, 0x0000, 0xF300)
                    DWordMemory (ResourceProducer, PosDecode, MinFixed, MaxFixed, NonCacheable, ReadWrite,
                        0x00000000, 0xC0000000, 0xFEBFFFFF, 0x00000000, 0x3EC00000)
                })
                Name (_PRT, Package ()
                {
                    Package () { 0x0001FFFF, Zero, Zero, 16 },  // device 01, INTA: GSI 16
                    Package () { 0x0001FFFF, One, Zero, 17 },  // device 01, INTB: GSI 17
                })
            }

            The host bridge in a Device Tree

            QEMU's virt machine's real node. The binding is Documentation/devicetree/bindings/pci/host-generic-pci.yaml; ranges and interrupt-map follow the Open Firmware PCI bus binding (IEEE 1275).

            pcie@10000000 {
                compatible = "pci-host-ecam-generic";
                device_type = "pci";
                reg = <0x40 0x10000000 0x00 0x10000000>;
                bus-range = <0x00 0xff>;
                linux,pci-domain = <0x00>;
                #address-cells = <0x03>;
                #size-cells = <0x02>;
                ranges = <0x1000000 0x00 0x00 0x00 0x3eff0000 0x00 0x10000
                          0x2000000 0x00 0x10000000 0x00 0x10000000 0x00 0x2eff0000
                          0x3000000 0x80 0x00 0x80 0x00 0x80 0x00>;
                #interrupt-cells = <0x01>;
                interrupt-map-mask = <0x1800 0x00 0x00 0x07>;
                interrupt-map = <0x00 0x00 0x00 0x01 0x8002 0x00 0x00 0x00 0x03 0x04
                                 0x00 0x00 0x00 0x02 0x8002 0x00 0x00 0x00 0x04 0x04
                                 ...
                                 0x800 0x00 0x00 0x01 0x8002 0x00 0x00 0x00 0x04 0x04
                                 ...>;
                msi-map = <0x00 0x8003 0x00 0x10000>;
                dma-coherent;
            };

            Press a part of each line to read what it means:

            reg = < >;

            ranges = < >

            interrupt-map-mask = < >

            interrupt-map = <… …>

            Nothing selected yet.

            Linux reads the node in pci_host_common_probe(): allocating the host bridge parses ranges and bus-range (devm_of_pci_get_host_bridge_resources()); then pci_host_common_init() maps reg as ECAM (pci_ecam_create()) and sets PCI_REASSIGN_ALL_BUS unless /chosen has linux,pci-probe-only. That is why every bridge in virt's DT log is "reconfiguring": nothing set it up, and this driver asks Linux to renumber every bus anyway. The same property in the host bridge's own node keeps firmware's BARs and windows but not its bus numbers.

            When the controller needs a driver of its own

            A generic ECAM host is ready the moment Linux maps reg. Most SoC controllers are not. Rockchip, NXP, Qualcomm, TI and others build theirs around Synopsys' DesignWare core, and their driver (matched by its own compatible, such as "rockchip,rk3568-pcie") first powers the slot (vpcie3v3-supply), starts the PHY, releases resets, turns on clocks and programs the iATU, the address translation unit, from ranges. Then the clock starts, in milliseconds (Rockchip's driver, Linux v7.2):

            AtWhat happensWhere
            0Assert PERST# (the card's reset), enable link trainingrockchip_pcie_start_link()
            0 → 100Wait with power stable, then release PERST#PCIE_T_PVPERL_MS = 100
            100 → up to 1000Check for link up, up to 10 times, 90 ms apart; Detect means an empty slot (carry on), anything else past Polling fails the probedw_pcie_wait_for_link()
            + 100Above 5 GT/s, wait 100 ms more before the first configuration readPCIE_RESET_CONFIG_WAIT_MS
            thenScanpci_host_probe()

            Lab 04 steps through it. An outbound translation unit is the controller's design, not something Arm requires: Cadence, Renesas R-Car, Broadcom and others have their own, while QEMU's virt and many servers have plain ECAM and need none.

            Arm servers: ACPI on arm64

            Arm servers (built to the SBSA and Arm SystemReady rules) boot with UEFI and ACPI, through the same MCFG and PNP0A08 objects as a PC. arm64 Linux uses ACPI when the Device Tree it got is empty apart from /chosen (what the UEFI stub passes when firmware supplies ACPI tables) or when booted with acpi=on. Three differences from x86 matter:

            Three ways to boot, side by side

            PC (q35, ACPI)Arm board (virt, DT)Arm server (virt, ACPI)
            Host bridge described byDSDT, PNP0A08node, compatibleDSDT, PNP0A08
            ECAM found inMCFG (or _CBA)regMCFG (or _CBA)
            First 256 bytes read throughports 0xcf8/0xcfcECAM, or the controller's iATUECAM
            Windows from_CRSranges_CRS
            Bus numbersfirmware's, kept while validLinux's (unless linux,pci-probe-only is in /chosen)firmware's, kept while valid
            BARsfirmware's, kept if they fit every window above them; else assigned againLinux's here: every BAR assigned (unless linux,pci-probe-only asks it to keep them)Linux's (unless _DSM 5 says preserve)
            INTx routing_PRTinterrupt-map_PRT
            IOMMU described byDMAR (Intel) or IVRS (AMD) tableiommu-map in the nodeIORT table
            MSI targetlocal APICGIC ITS or v2m, via msi-mapGIC ITS, via IORT
            Host bridge codedrivers/acpi/pci_root.c, arch/x86/pci/acpi.cpci-host-generic.c or a SoC driverdrivers/acpi/pci_root.c, drivers/pci/pci-acpi.c

            Three real boots

            The same fabric (a root port, a two-port switch and an Intel 82574L network card, QEMU's e1000e), booted with mainline Linux 7.2.0 on QEMU 8.2. Look for who sets up the bridges, and whether the card's BAR 0 moves.

            PC: q35, SeaBIOS and ACPI

            Firmware numbered the buses (PCI bridge to [bus 01-03] on the first read) and placed the BAR. Linux checked both and kept them: there is no "assigned" line.

            PCI: ECAM [mem 0xb0000000-0xbfffffff] (base 0xb0000000) for domain 0000 [bus 00-ff]
            PCI: ECAM [mem 0xb0000000-0xbfffffff] reserved as E820 entry
            PCI: Using configuration type 1 for base access
            ACPI: PCI Root Bridge [PCI0] (domain 0000 [bus 00-ff])
            acpi PNP0A08:00: _OSC: OS now controls [SHPCHotplug PME AER PCIeCapability]
            PCI host bridge to bus 0000:00
            pci_bus 0000:00: root bus resource [io  0x0000-0x0cf7 window]
            pci_bus 0000:00: root bus resource [io  0x0d00-0xffff window]
            pci_bus 0000:00: root bus resource [mem 0xc0000000-0xfebfffff window]
            pci_bus 0000:00: root bus resource [bus 00-ff]
              …
            pci 0000:00:02.0: [1b36:000c] type 01 class 0x060400 PCIe Root Port
            pci 0000:00:02.0: PCI bridge to [bus 01-03]
            pci 0000:00:02.0:   bridge window [mem 0xfe800000-0xfe9fffff]
            pci 0000:01:00.0: [104c:8232] type 01 class 0x060400 PCIe Switch Upstream Port
            pci 0000:01:00.0: PCI bridge to [bus 02-03]
            pci 0000:02:00.0: [104c:8233] type 01 class 0x060400 PCIe Switch Downstream Port
            pci 0000:02:00.0: PCI bridge to [bus 03]
            pci 0000:03:00.0: [8086:10d3] type 00 class 0x020000 PCIe Endpoint
            pci 0000:03:00.0: BAR 0 [mem 0xfe880000-0xfe89ffff]

            Arm: virt with a Device Tree

            In virt no firmware set anything up: every bridge reads bus 00, Linux renumbers it, and every window and BAR is "assigned" from the ranges window.

            Machine model: linux,dummy-virt
            ACPI: Interpreter disabled.
            pci-host-generic 4010000000.pcie: host bridge /pcie@10000000 ranges:
            pci-host-generic 4010000000.pcie:       IO 0x003eff0000..0x003effffff -> 0x0000000000
            pci-host-generic 4010000000.pcie:      MEM 0x0010000000..0x003efeffff -> 0x0010000000
            pci-host-generic 4010000000.pcie:      MEM 0x8000000000..0xffffffffff -> 0x8000000000
            pci-host-generic 4010000000.pcie: ECAM at [mem 0x4010000000-0x401fffffff] for [bus 00-ff]
            pci-host-generic 4010000000.pcie: PCI host bridge to bus 0000:00
            pci 0000:00:01.0: [1b36:000c] type 01 class 0x060400 PCIe Root Port
            pci 0000:00:01.0: PCI bridge to [bus 00]
            pci 0000:00:01.0: bridge configuration invalid ([bus 00-00]), reconfiguring
              …
            pci 0000:03:00.0: [8086:10d3] type 00 class 0x020000 PCIe Endpoint
            pci 0000:03:00.0: BAR 0 [mem 0x00000000-0x0001ffff]
              …
            pci 0000:00:01.0: bridge window [mem 0x10000000-0x103fffff]: assigned
            pci 0000:01:00.0: bridge window [mem 0x10000000-0x102fffff]: assigned
            pci 0000:02:00.0: bridge window [mem 0x10000000-0x102fffff]: assigned
            pci 0000:03:00.0: BAR 0 [mem 0x10080000-0x1009ffff]: assigned

            Arm: virt with UEFI and ACPI

            UEFI numbered the buses and Linux keeps them, but it moves BAR 0 from 0x10020000 to 0x10080000: no _DSM asked it to preserve them.

            ACPI: MCFG table detected, 1 entries
            ACPI: PCI Root Bridge [PCI0] (domain 0000 [bus 00-ff])
            acpi PNP0A08:00: _OSC: OS now controls [PCIeHotplug SHPCHotplug PME AER PCIeCapability]
            acpi PNP0A08:00: ECAM area [mem 0x4010000000-0x401fffffff] reserved by PNP0C02:00
            acpi PNP0A08:00: ECAM at [mem 0x4010000000-0x401fffffff] for [bus 00-ff]
            PCI host bridge to bus 0000:00
              …
            pci 0000:00:01.0: PCI bridge to [bus 01-03]
            pci 0000:01:00.0: PCI bridge to [bus 02-03]
            pci 0000:02:00.0: PCI bridge to [bus 03]
            pci 0000:03:00.0: BAR 0 [mem 0x10020000-0x1003ffff]
              …
            pci 0000:03:00.0: BAR 0 [mem 0x10080000-0x1009ffff]: assigned

            Two more runs on q35: booted with pci=nommconf, Linux printed _OSC: not requesting OS control; OS requires [ExtendedConfig ASPM ClockPM MSI] and never enabled AER. With a card at 00:05.1 and nothing at 00:05.0, Linux never listed it: without function 0 a slot is empty.

            Claims we checked

            Statements often made about this boot path, against the source:

            ClaimWhat the source says
            Linux matches x86 host bridges by PNP0A03 or PNP0A08.Both work, but the handler lists only PNP0A03; a PNP0A08 bridge matches through its _CID (root_device_ids in drivers/acpi/pci_root.c).
            pci_acpi_scan_root() parses _PRT for interrupts.The scan does not touch _PRT. acpi_pci_irq_enable() (drivers/acpi/pci_irq.c) reads it for each device a driver takes: on x86 when the driver calls pci_enable_device(), on arm64 just before its probe (pcibios_alloc_irq()).
            Once the MCFG is parsed, x86 reads configuration space through ECAM.Only above offset 255 or outside domain 0. The rest still goes through ports 0xcf8/0xcfc when they work (raw_pci_read(), arch/x86/pci/common.c).
            Arm boots PCIe from a Device Tree.Boards usually do; servers use ACPI with the same MCFG and host bridge objects as a PC (drivers/pci/pci-acpi.c).
            On Arm the driver must program the iATU.Only controllers with their own outbound translation (DesignWare's iATU, Cadence, R-Car and others) need it programmed. A generic ECAM host needs no address setup at all (pci-host-generic.c).
            A DesignWare node uses compatible = "synopsys,designware-pcie".No such string exists. The core's is "snps,dw-pcie", and boards use their SoC's, such as "rockchip,rk3568-pcie". "brcm,bcm2711-pcie" (Raspberry Pi 4) is Broadcom's own controller.
            A PC's firmware sets up the bridges and BARs and Linux only reads them; on Arm Linux builds it all.No. On q35 Linux checks each BAR against the windows above it and assigns again one that does not fit (pcibios_allocate_resources(), pci_claim_resource()), and reconfigures a bridge whose bus numbers are invalid. Who numbers and places is each host driver's policy: the generic DT host renumbers every bus (PCI_REASSIGN_ALL_BUS) unless /chosen has linux,pci-probe-only; arm64 ACPI keeps the buses and reassigns the BARs unless _DSM function 5 says preserve. The three boots above show three policies, not one rule: the first half of the claim holds for q35 only, the second for virt's Device Tree only.

            A driver that prints what boot found

            pci_found is a whole Linux PCI driver in about 100 lines. It binds to QEMU's teaching device edu (ID 1234:11e8, which no other driver claims) and prints what discovery did for it: the IDs and configuration-space size the scan read, the firmware node that described its host bridge, the host bridge window its BAR landed in, and where its interrupt pin was routed. It follows the kernel's own style (checkpatch.pl --strict reports nothing) and builds with W=1 against Linux 7.2 for x86 and arm64. Download pci_found.c.

            // SPDX-License-Identifier: GPL-2.0
            /*
             * pci_found: a minimal PCI driver that prints what boot discovery found
             * before it ran: the function's IDs and config space size (read during the
             * scan), the firmware node that described its host bridge (ACPI or Device
             * Tree), the host bridge window its BAR was placed in, and the interrupt
             * its pin was routed to.
             *
             * It binds only to QEMU's teaching device (1234:11e8, "-device edu"),
             * which no other driver claims. Build it against the headers of the kernel
             * you run, then load it:
             *   echo 'obj-m := pci_found.o' > Kbuild
             *   make -C /lib/modules/$(uname -r)/build M=$PWD modules
             *   sudo insmod pci_found.ko && sudo dmesg | grep pci_found
             */
            #include <linux/acpi.h>
            #include <linux/module.h>
            #include <linux/of.h>
            #include <linux/pci.h>
            #include <linux/property.h>
            
            #define PCI_VENDOR_ID_QEMU_EDU 0x1234 /* QEMU's "edu" teaching device */
            #define PCI_DEVICE_ID_QEMU_EDU 0x11e8
            
            /* Who described the host bridge: the first ancestor with a firmware node. */
            static void pci_found_firmware(struct pci_dev *pdev)
            {
            	struct device *dev = &pci_find_host_bridge(pdev->bus)->dev;
            
            	for (; dev; dev = dev->parent) {
            		struct fwnode_handle *fwnode = dev_fwnode(dev);
            
            		if (is_acpi_node(fwnode)) {
            			pci_info(pdev, "host bridge described by ACPI: %pfwf\n", fwnode);
            			return;
            		}
            		if (is_of_node(fwnode)) {
            			pci_info(pdev, "host bridge described by Device Tree: %pfwf\n", fwnode);
            			return;
            		}
            	}
            	pci_info(pdev, "host bridge has no firmware node\n");
            }
            
            /* Each BAR, and the host bridge window (from _CRS or ranges) that holds it. */
            static void pci_found_bars(struct pci_dev *pdev)
            {
            	struct pci_bus *root = pdev->bus;
            	struct resource *res, *window;
            	unsigned int bar;
            
            	while (!pci_is_root_bus(root))
            		root = root->parent;
            
            	for (bar = 0; bar < PCI_STD_NUM_BARS; bar++) {
            		res = pci_resource_n(pdev, bar);
            		if (!resource_size(res))
            			continue;
            		pci_bus_for_each_resource(root, window) {
            			if (window && resource_contains(window, res)) {
            				pci_info(pdev, "BAR %u %pR is in host bridge window %pR\n",
            					 bar, res, window);
            				break;
            			}
            		}
            	}
            }
            
            static int pci_found_probe(struct pci_dev *pdev, const struct pci_device_id *id)
            {
            	int ret;
            
            	pci_info(pdev, "found during the scan: [%04x:%04x] class %06x, %d-byte config space\n",
            		 pdev->vendor, pdev->device, pdev->class, pdev->cfg_size);
            	pci_found_firmware(pdev);
            
            	/* x86 looks INTA up in _PRT here; arm64 did, from _PRT or interrupt-map, before probe. */
            	ret = pcim_enable_device(pdev);
            	if (ret)
            		return ret;
            
            	pci_found_bars(pdev);
            	if (pdev->pin)
            		pci_info(pdev, "INT%c routed to IRQ %d\n", 'A' + pdev->pin - 1, pdev->irq);
            	return 0;
            }
            
            static const struct pci_device_id pci_found_ids[] = {
            	{ PCI_DEVICE(PCI_VENDOR_ID_QEMU_EDU, PCI_DEVICE_ID_QEMU_EDU) },
            	{ }
            };
            MODULE_DEVICE_TABLE(pci, pci_found_ids);
            
            static struct pci_driver pci_found_driver = {
            	.name = "pci_found",
            	.id_table = pci_found_ids,
            	.probe = pci_found_probe,
            };
            module_pci_driver(pci_found_driver);
            
            MODULE_DESCRIPTION("Print what PCI boot discovery found for one device");
            MODULE_LICENSE("GPL");
            

            Read it from the bottom up:

            Build it and load it in a QEMU guest (or on a machine with that device):

            echo 'obj-m := pci_found.o' > Kbuild
            make -C /lib/modules/$(uname -r)/build M=$PWD modules
            sudo insmod pci_found.ko && sudo dmesg | grep pci_found
            
            # The fabric used on this page, behind a root port and a switch:
            qemu-system-x86_64 -M q35 ... \
              -device pcie-root-port,id=rp1,chassis=1,slot=1 \
              -device x3130-upstream,id=up,bus=rp1 \
              -device xio3130-downstream,id=dn,bus=up,chassis=2,slot=0 \
              -device edu,bus=dn

            What it printed on the three machines:

            PC, q35 with ACPI:
            pci_found 0000:03:00.0: found during the scan: [1234:11e8] class 00ff00, 256-byte config space
            pci_found 0000:03:00.0: host bridge described by ACPI: \_SB.PCI0
            pci_found 0000:03:00.0: BAR 0 [mem 0xfe800000-0xfe8fffff] is in host bridge window [mem 0xc0000000-0xfebfffff window]
            pci_found 0000:03:00.0: INTA routed to IRQ 23
            
            Arm, virt with a Device Tree:
            pci_found 0000:03:00.0: found during the scan: [1234:11e8] class 00ff00, 256-byte config space
            pci_found 0000:03:00.0: host bridge described by Device Tree: /pcie@10000000
            pci_found 0000:03:00.0: enabling device (0000 -> 0002)
            pci_found 0000:03:00.0: BAR 0 [mem 0x10000000-0x100fffff] is in host bridge window [mem 0x10000000-0x3efeffff]
            pci_found 0000:03:00.0: INTA routed to IRQ 15
            
            Arm, virt with UEFI and ACPI:
            pci_found 0000:03:00.0: found during the scan: [1234:11e8] class 00ff00, 256-byte config space
            pci_found 0000:03:00.0: host bridge described by ACPI: \_SB.PCI0
            pci_found 0000:03:00.0: enabling device (0000 -> 0002)
            pci_found 0000:03:00.0: BAR 0 [mem 0x10000000-0x100fffff] is in host bridge window [mem 0x10000000-0x3efeffff window]
            pci_found 0000:03:00.0: INTA routed to IRQ 47

            The IRQ numbers are Linux's own numbers for the interrupt, not the GSI or the GIC's; /proc/interrupts shows both. The same BAR sits at 0xfe800000 on the PC, where firmware put it, and at 0x10000000 on both Arm machines, the start of the window, where Linux put it.

            Where next

            The card is found, placed and bound. What it may do next is its own story: Reading lspci reads the same card field by field; Before DMA turns on the one bit that lets it write to memory; IOMMU groups & ACS shows which devices the IOMMU can tell apart; and One NIC, three stops follows it from the bus through the IOMMU into sleep and wake.

            Sources