uefi: Refactor PciRootBridgeIo::enumerate() with tree-topology information

- Refactored return type from standard BTreeSet to custom PciTree struct
- Removed special FullPciIoAddress type, since segment number is PciRoot dependent
- During enumeration, skip branches we have already seen
- During enumeration, collect tree topology information (which child bus linked from where)
- Add complicated pci structure in integration test vm
- Print child busses for every device entry in integration test
This commit is contained in:
Markus Ebner
2025-11-21 23:03:54 +01:00
parent 159f825784
commit 3d29ddaed1
6 changed files with 161 additions and 68 deletions
@@ -21,9 +21,8 @@ pub fn test() {
for pci_handle in pci_handles {
let mut pci_proto = get_open_protocol::<PciRootBridgeIo>(pci_handle);
let devices = pci_proto.enumerate().unwrap();
for fqaddr in devices {
let addr = fqaddr.addr();
let pci_tree = pci_proto.enumerate().unwrap();
for addr in pci_tree.iter().cloned() {
let Ok(reg0) = pci_proto.pci().read_one::<u32>(addr.with_register(0)) else {
continue;
};
@@ -53,8 +52,11 @@ pub fn test() {
let (bus, dev, fun) = (addr.bus, addr.dev, addr.fun);
log::info!(
"PCI Device: [{bus}, {dev}, {fun}]: vendor={vendor_id:04X}, device={device_id:04X}, class={class_code:02X}, subclass={subclass_code:02X}"
"PCI Device: [{bus:02x}, {dev:02x}, {fun:02x}]: vendor={vendor_id:04X}, device={device_id:04X}, class={class_code:02X}, subclass={subclass_code:02X}"
);
for child_bus in pci_tree.child_bus_of_iter(addr) {
log::info!(" |- Bus: {child_bus:02x}");
}
}
}
+2 -2
View File
@@ -16,9 +16,9 @@ fn test_allocating_api() {
// by default respectively. We manually configure an additional SCSI controller.
// Thus, we should see two controllers with support for EXT_SCSI_PASS_THRU on this platform
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
assert_eq!(scsi_ctrl_handles.len(), 2);
assert_eq!(scsi_ctrl_handles.len(), 4);
#[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
assert_eq!(scsi_ctrl_handles.len(), 1);
assert_eq!(scsi_ctrl_handles.len(), 3);
let mut found_drive = false;
for handle in scsi_ctrl_handles {
+102 -11
View File
@@ -4,10 +4,11 @@
use core::mem;
use alloc::collections::btree_set::BTreeSet;
use alloc::collections::btree_map::BTreeMap;
use alloc::collections::btree_set::{self, BTreeSet};
use super::PciIoAddress;
use super::root_bridge::PciRootBridgeIo;
use super::{FullPciIoAddress, PciIoAddress};
#[allow(unused)]
#[derive(Clone, Copy, Debug)]
@@ -55,6 +56,93 @@ fn read_device_register_u32<T: Sized + Copy>(
}
}
// ##########################################################################################
/// Struct representing the tree structure of PCI devices.
///
/// This allows iterating over all valid PCI device addresses in a tree, as well as querying
/// the tree topology.
#[derive(Debug)]
pub struct PciTree {
segment: u32,
devices: BTreeSet<PciIoAddress>,
bus_anchors: BTreeMap<u8 /* bus */, PciIoAddress>,
}
impl PciTree {
pub(crate) const fn new(segment: u32) -> Self {
Self {
segment,
devices: BTreeSet::new(),
bus_anchors: BTreeMap::new(),
}
}
pub(crate) fn should_visit_bus(&self, bus: u8) -> bool {
!self.bus_anchors.contains_key(&bus)
}
pub(crate) fn push_device(&mut self, addr: PciIoAddress) {
self.devices.insert(addr);
}
/// Pushes a new bridge into the topology.
///
/// Returns `false` if the bus is already in the topology and `true`
/// if the bridge was added to the topology.
pub(crate) fn push_bridge(&mut self, addr: PciIoAddress, child_bus: u8) -> bool {
match self.bus_anchors.contains_key(&child_bus) {
true => false,
false => {
self.bus_anchors.insert(child_bus, addr);
true
}
}
}
/// Iterate over all valid PCI device addresses in this tree structure.
pub fn iter(&self) -> btree_set::Iter<'_, PciIoAddress> {
self.devices.iter()
}
/// Get the segment number of this PCI tree.
#[must_use]
pub const fn segment_nr(&self) -> u32 {
self.segment
}
/// Query the address of the parent PCI bridge this `addr`'s bus is subordinate to.
#[must_use]
pub fn parent_for(&self, addr: PciIoAddress) -> Option<PciIoAddress> {
self.bus_anchors.get(&addr.bus).cloned()
}
/// Iterate over all subsequent busses below the given `addr`.
/// This yields 0 results if `addr` doesn't point to a PCI bridge.
pub fn child_bus_of_iter(&self, addr: PciIoAddress) -> impl Iterator<Item = u8> {
self.bus_anchors
.iter()
.filter(move |&(_, parent)| *parent == addr)
.map(|(bus, _)| bus)
.cloned()
}
}
impl IntoIterator for PciTree {
type Item = PciIoAddress;
type IntoIter = btree_set::IntoIter<PciIoAddress>;
fn into_iter(self) -> Self::IntoIter {
self.devices.into_iter()
}
}
impl<'a> IntoIterator for &'a PciTree {
type Item = &'a PciIoAddress;
type IntoIter = btree_set::Iter<'a, PciIoAddress>;
fn into_iter(self) -> Self::IntoIter {
self.devices.iter()
}
}
// ##########################################################################################
// # Query Helpers (read from a device's configuration registers)
@@ -86,12 +174,12 @@ fn get_secondary_bus_range(
fn visit_function(
proto: &mut PciRootBridgeIo,
addr: PciIoAddress,
queue: &mut BTreeSet<FullPciIoAddress>,
tree: &mut PciTree,
) -> uefi::Result<()> {
if get_vendor_id(proto, addr)? == 0xFFFF {
return Ok(()); // function doesn't exist - bail instantly
}
queue.insert(FullPciIoAddress::new(proto.segment_nr(), addr));
tree.push_device(addr);
let (base_class, sub_class) = get_classes(proto, addr)?;
let header_type = get_header_type(proto, addr)? & 0b01111111;
if base_class == 0x6 && sub_class == 0x4 && header_type == 0x01 {
@@ -106,8 +194,11 @@ fn visit_function(
return Ok(());
}
for bus in secondary_bus_nr..=subordinate_bus_nr {
// Recurse into the bus namespaces on the other side of the bridge
visit_bus(proto, PciIoAddress::new(bus, 0, 0), queue)?;
// Recurse into the bus namespaces on the other side of the bridge, if we haven't visited
// the subordinate bus through a more direct path already
if tree.push_bridge(addr, bus) {
visit_bus(proto, PciIoAddress::new(bus, 0, 0), tree)?;
}
}
}
Ok(())
@@ -116,17 +207,17 @@ fn visit_function(
fn visit_device(
proto: &mut PciRootBridgeIo,
addr: PciIoAddress,
queue: &mut BTreeSet<FullPciIoAddress>,
tree: &mut PciTree,
) -> uefi::Result<()> {
if get_vendor_id(proto, addr)? == 0xFFFF {
return Ok(()); // device doesn't exist
}
visit_function(proto, addr.with_function(0), queue)?;
visit_function(proto, addr.with_function(0), tree)?;
if get_header_type(proto, addr.with_function(0))? & 0x80 != 0 {
// This is a multi-function device - also try the remaining functions [1;7]
// These remaining functions can be sparsely populated - as long as function 0 exists.
for fun in 1..=7 {
visit_function(proto, addr.with_function(fun), queue)?;
visit_function(proto, addr.with_function(fun), tree)?;
}
}
@@ -136,11 +227,11 @@ fn visit_device(
pub(crate) fn visit_bus(
proto: &mut PciRootBridgeIo,
addr: PciIoAddress,
queue: &mut BTreeSet<FullPciIoAddress>,
tree: &mut PciTree,
) -> uefi::Result<()> {
// Given a valid bus entry point - simply try all possible devices addresses
for dev in 0..32 {
visit_device(proto, addr.with_device(dev), queue)?;
visit_device(proto, addr.with_device(dev), tree)?;
}
Ok(())
}
+1 -31
View File
@@ -8,7 +8,7 @@ use uefi_raw::protocol::pci::root_bridge::PciRootBridgeIoProtocolWidth;
pub mod configuration;
#[cfg(feature = "alloc")]
mod enumeration;
pub mod enumeration;
pub mod root_bridge;
/// IO Address for PCI/register IO operations
@@ -124,36 +124,6 @@ impl Ord for PciIoAddress {
}
}
// --------------------------------------------------------------------------------------------
/// Fully qualified pci address. This address is valid across root bridges.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FullPciIoAddress {
/// PCI segment number
segment: u32,
/// Subsequent PCI address
addr: PciIoAddress,
}
impl FullPciIoAddress {
/// Construct a new fully qualified pci address.
#[must_use]
pub const fn new(segment: u32, addr: PciIoAddress) -> Self {
Self { segment, addr }
}
/// Get the segment number this address belongs to.
#[must_use]
pub const fn segment(&self) -> u32 {
self.segment
}
/// Get the internal RootBridge-specific portion of the address.
#[must_use]
pub const fn addr(&self) -> PciIoAddress {
self.addr
}
}
// ############################################################################################
/// Trait implemented by all data types that can natively be read from a PCI device.
+19 -20
View File
@@ -7,8 +7,6 @@ use crate::StatusExt;
#[cfg(feature = "alloc")]
use crate::proto::pci::configuration::QwordAddressSpaceDescriptor;
#[cfg(feature = "alloc")]
use alloc::collections::btree_set::BTreeSet;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[cfg(feature = "alloc")]
use core::ffi::c_void;
@@ -16,8 +14,6 @@ use core::ptr;
use uefi_macros::unsafe_protocol;
use uefi_raw::protocol::pci::root_bridge::{PciRootBridgeIoAccess, PciRootBridgeIoProtocol};
#[cfg(doc)]
use super::FullPciIoAddress;
#[cfg(doc)]
use crate::Status;
@@ -84,10 +80,10 @@ impl PciRootBridgeIo {
// ###################################################
// # Convenience functionality
/// Recursively enumerate all devices, device functions and pci-to-pci bridges on this root bridge.
/// Recursively enumerate all devices, device functions and pci(e)-to-pci(e) bridges, starting from this pci root.
///
/// The returned addresses might overlap with the addresses returned by another [`PciRootBridgeIo`] instance.
/// Make sure to perform some form of cross-[`PciRootBridgeIo`] deduplication on the returned [`FullPciIoAddress`]es.
/// Make sure to perform some form of cross-[`PciRootBridgeIo`] deduplication on the discovered addresses.
/// **WARNING:** Only use the returned addresses with the respective [`PciRootBridgeIo`] instance that returned them.
///
/// # Returns
@@ -96,24 +92,27 @@ impl PciRootBridgeIo {
/// # Errors
/// This can basically fail with all the IO errors found in [`PciIoAccessPci`] methods.
#[cfg(feature = "alloc")]
pub fn enumerate(&mut self) -> crate::Result<BTreeSet<super::FullPciIoAddress>> {
pub fn enumerate(&mut self) -> crate::Result<super::enumeration::PciTree> {
use super::enumeration::{self, PciTree};
use crate::proto::pci::configuration::ResourceRangeType;
use crate::proto::pci::enumeration;
let mut devices = BTreeSet::new();
// In the descriptors, the entry with range_type bus specifies the bus numbers that were
// allocated to devices below this root bridge. The first bus number in this range is
// the starting point. All subsequent numbers are reached via PCI bridge recursion during enumeration.
if let Some(descriptor) = self
.configuration()?
.iter()
.find(|d| d.resource_range_type == ResourceRangeType::Bus)
{
let addr = PciIoAddress::new(descriptor.address_min as u8, 0, 0);
enumeration::visit_bus(self, addr, &mut devices)?;
let mut tree = PciTree::new(self.segment_nr());
for descriptor in self.configuration()? {
// In the descriptors we can query for the current root bridge, Bus entries contain ranges of valid
// bus addresses. These are all bus addresses found below ourselves. These are not only the busses
// linked to **directly** from ourselves, but also recursively. Thus we use PciTree::push_bus() to
// determine whether we have already visited a given bus number.
if descriptor.resource_range_type == ResourceRangeType::Bus {
for bus in (descriptor.address_min as u8)..=(descriptor.address_max as u8) {
if tree.should_visit_bus(bus) {
let addr = PciIoAddress::new(bus, 0, 0);
enumeration::visit_bus(self, addr, &mut tree)?;
}
}
}
}
Ok(devices)
Ok(tree)
}
}
+31
View File
@@ -506,6 +506,37 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> {
None
};
// Make PCI tree a little more complicated so the PCI enumeration integration
// test is more interesting. This specific architecture was taken from:
// https://blogs.oracle.com/linux/a-study-of-the-linux-kernel-pci-subsystem-with-qemu
// It employs a complex PCIe switch (x3130) setup, which makes it interesting for testing.
// The bus architecture this generates is found below `0000:00:03.0` in below diagram.
// (actual bus numbers and surrounding devices don't match this diagram)
// -[0000:00]-+-00.0
// +-01.0
// +-02.0
// +-03.0-[01-04]----00.0-[02-04]--+-00.0-[03]----00.0
// | \-01.0-[04]----00.0
// +-1f.0
// +-1f.2
// \-1f.3
// In the diagram, `0000:03:00.0` and `0000:04:00.0` are just dummy SCSI devices
// connected below the PCIe switch, to test correct recursion in the bus enumeration implementation.
cmd.args([
"-device",
"ioh3420,id=root_port1,bus=pcie.0",
"-device",
"x3130-upstream,id=upstream1,bus=root_port1",
"-device",
"xio3130-downstream,id=downstream1,bus=upstream1,chassis=9",
"-device",
"virtio-scsi-pci,bus=downstream1",
"-device",
"xio3130-downstream,id=downstream2,bus=upstream1,chassis=10",
"-device",
"virtio-scsi-pci,bus=downstream2",
]);
// Pass CA certificate database to the edk2 firmware, for TLS support.
cmd.args([
"-fw_cfg",