Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions uefi-test-runner/src/proto/pci/root_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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.iter_subsequent_busses_for(addr) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of just iterating, would it make sense to explictely check for the expected PCIe devices?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bus numbers are dynamically allocated and thus might vary.
But I've done something like you suggest in the follow-up MR here, based on device paths: 582714a

log::info!(" |- Bus: {child_bus:02x}");
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions uefi-test-runner/src/proto/scsi/pass_thru.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
109 changes: 98 additions & 11 deletions uefi/src/proto/pci/enumeration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -55,6 +56,89 @@ 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 push_bus(&self, bus: u8) -> bool {
!self.bus_anchors.contains_key(&bus)
}

pub(crate) fn push_device(&mut self, addr: PciIoAddress) {
self.devices.insert(addr);
}

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 iter_subsequent_busses_for(&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)

Expand Down Expand Up @@ -86,12 +170,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 {
Expand All @@ -106,8 +190,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(())
Expand All @@ -116,17 +203,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)?;
}
}

Expand All @@ -136,11 +223,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(())
}
32 changes: 1 addition & 31 deletions uefi/src/proto/pci/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
39 changes: 19 additions & 20 deletions uefi/src/proto/pci/root_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@ 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;
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;

Expand Down Expand Up @@ -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
Expand All @@ -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.push_bus(bus) {
let addr = PciIoAddress::new(bus, 0, 0);
enumeration::visit_bus(self, addr, &mut tree)?;
}
}
}
}

Ok(devices)
Ok(tree)
}
}

Expand Down
31 changes: 31 additions & 0 deletions xtask/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 correct recursion in the bus enumeration implementation.
cmd.args([
"-device",
"ioh3420,id=root_port1,bus=pcie.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that familiar with PCI. Could you please elaborate a little?

  • why adding a root port? Is this equivalent to adding a new PCI bus?
  • what is x3130-upstream and why are you using it here?
  • what is x3130-downstream and why are you using it here?
  • virtio-scsi-pci is just a random PCI device without serving a functionality here except for appearing on the PCIe bus?
  • why are you specifying chassis=9|10?

Copy link
Contributor Author

@seijikun seijikun Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much of this is qemu specific syntax:
ioh3420 reserves a port in pcie.0 - the pcie root port, also called pcie root complex. I imagine this like soldering a PCIe socket on your mainboard, where you can plug stuff in.
Into this socket, we then plug a x3130-upstream. That's a PCIe switch.
x3130-downstream is the other side of the swich, where you can plug child devices in.
I have no idea what chassis is, the way I understood it, it can be any number as long as they are unique. All bridges/switches have this obligatory parameter.
The scsi devices are just for testing, like you said.

Difference PCIe switches and PCIe bridges:
Both allocate a new subordinate bus. Both allow connecting devices to them, which they "pass through" to the port where they are connected upstream. The key difference is, that for you to pass 2 lanes through a bridge, it needs to be connected with 2 lanes to its upstream. Whereas PCIe switches can do switching. They can tunnel y downstream lanes through x upstream lanes - sacrificing speed if y > x (like with network switches). They are rather seldom, which is why I wanted to test it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image https://blogs.oracle.com/linux/a-study-of-the-linux-kernel-pci-subsystem-with-qemu

"-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",
Expand Down
Loading