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
61 changes: 29 additions & 32 deletions uefi-test-runner/src/proto/pci/root_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
use uefi::Handle;
use uefi::boot::{OpenProtocolAttributes, OpenProtocolParams, ScopedProtocol, image_handle};
use uefi::proto::ProtocolPointer;
use uefi::proto::pci::PciIoAddress;
use uefi::proto::pci::root_bridge::PciRootBridgeIo;

const RED_HAT_PCI_VENDOR_ID: u16 = 0x1AF4;
Expand All @@ -22,42 +21,40 @@ pub fn test() {
for pci_handle in pci_handles {
let mut pci_proto = get_open_protocol::<PciRootBridgeIo>(pci_handle);

for bus in 0..=255 {
for dev in 0..32 {
for fun in 0..8 {
let addr = PciIoAddress::new(bus, dev, fun);
let Ok(reg0) = pci_proto.pci().read_one::<u32>(addr.with_register(0)) else {
continue;
};
if reg0 == 0xFFFFFFFF {
continue; // not a valid device
}
let reg1 = pci_proto
.pci()
.read_one::<u32>(addr.with_register(2 * REG_SIZE))
.unwrap();

let vendor_id = (reg0 & 0xFFFF) as u16;
let device_id = (reg0 >> 16) as u16;
if vendor_id == RED_HAT_PCI_VENDOR_ID {
red_hat_dev_cnt += 1;
}
let devices = pci_proto.enumerate().unwrap();
for fqaddr in devices {
let addr = fqaddr.addr();
let Ok(reg0) = pci_proto.pci().read_one::<u32>(addr.with_register(0)) else {
continue;
};
if reg0 == 0xFFFFFFFF {
continue; // not a valid device
}
let reg1 = pci_proto
.pci()
.read_one::<u32>(addr.with_register(2 * REG_SIZE))
.unwrap();

let class_code = (reg1 >> 24) as u8;
let subclass_code = ((reg1 >> 16) & 0xFF) as u8;
if class_code == MASS_STORAGE_CTRL_CLASS_CODE {
mass_storage_ctrl_cnt += 1;
let vendor_id = (reg0 & 0xFFFF) as u16;
let device_id = (reg0 >> 16) as u16;
if vendor_id == RED_HAT_PCI_VENDOR_ID {
red_hat_dev_cnt += 1;
}

if subclass_code == SATA_CTRL_SUBCLASS_CODE {
sata_ctrl_cnt += 1;
}
}
let class_code = (reg1 >> 24) as u8;
let subclass_code = ((reg1 >> 16) & 0xFF) as u8;
if class_code == MASS_STORAGE_CTRL_CLASS_CODE {
mass_storage_ctrl_cnt += 1;

log::info!(
"PCI Device: [{bus}, {dev}, {fun}]: vendor={vendor_id:04X}, device={device_id:04X}, class={class_code:02X}, subclass={subclass_code:02X}"
);
if subclass_code == SATA_CTRL_SUBCLASS_CODE {
sata_ctrl_cnt += 1;
}
}

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}"
);
}
}

Expand Down
4 changes: 3 additions & 1 deletion uefi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
## Added
- Added `proto::ata::AtaRequestBuilder::read_pio()`.
- Added `proto::shell::Shell::{var(), set_var(), vars()}`
- Added `proto::pci::root_bridge::PciRootBridgeIo::configuration()`.
- Added `proto::pci::root_bridge::PciRootBridgeIo::enumerate()`.

## Changed

- Changed ordering of `proto::pci::PciIoAddress` to (bus -> dev -> fun -> reg -> ext_reg).

# uefi - v0.36.1 (2025-11-05)

Expand Down
146 changes: 146 additions & 0 deletions uefi/src/proto/pci/configuration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Pci root bus resource configuration descriptor parsing.

/// Represents the type of resource described by a QWORD Address Space Descriptor.
/// This corresponds to the `resource_type` field at offset 0x03 in the descriptor.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ResourceRangeType {
/// Memory Range (value = 0)
/// Indicates that the descriptor describes a memory-mapped address range.
/// Commonly used for MMIO regions decoded by the PCI root bridge.
Memory = 0,

/// I/O Range (value = 1)
/// Indicates that the descriptor describes a legacy I/O port range.
/// Used for devices that communicate via port-mapped I/O.
Io = 1,

/// Bus Number Range (value = 2)
/// Indicates that the descriptor describes a range of PCI bus numbers.
/// Used to define the bus hierarchy behind a PCI root bridge.
Bus = 2,

/// Unknown or vendor-specific resource type.
/// Captures any unrecognized value for forward compatibility.
Unknown(u8),
}
impl From<u8> for ResourceRangeType {
fn from(value: u8) -> Self {
match value {
0 => Self::Memory,
1 => Self::Io,
2 => Self::Bus,
other => Self::Unknown(other),
}
}
}

/// Represents a parsed QWORD Address Space Descriptor from UEFI.
/// This structure describes a decoded resource range for a PCI root bridge.
#[derive(Clone, Debug)]
pub struct QwordAddressSpaceDescriptor {
/// Type of resource: Memory, I/O, Bus, or Unknown.
pub resource_range_type: ResourceRangeType,
/// General flags that describe decode behavior (e.g., positive decode).
pub general_flags: u8,
/// Type-specific flags (e.g., cacheability for memory).
pub type_specific_flags: u8,
/// Granularity of the address space (typically 32 or 64).
/// Indicates whether the range is 32-bit or 64-bit.
pub granularity: u64,
/// Minimum address of the range (inclusive).
pub address_min: u64,
/// Maximum address of the range (inclusive).
pub address_max: u64,
/// Translation offset to convert host address to PCI address.
/// Usually zero unless the bridge remaps addresses.
pub translation_offset: u64,
/// Length of the address range (in bytes or bus numbers).
pub address_length: u64,
}

/// Parses a list of QWORD Address Space Descriptors from a raw memory region.
/// Stops when it encounters an End Tag descriptor (type 0x79).
#[cfg(feature = "alloc")]
pub(crate) fn parse(
base: *const core::ffi::c_void,
) -> alloc::vec::Vec<QwordAddressSpaceDescriptor> {
use alloc::slice;
use alloc::vec::Vec;
const PCI_RESTBL_QWORDADDRSPEC_TAG: u8 = 0x8a;
const PCI_RESTBL_END_TAG: u8 = 0x79;

let base: *const u8 = base.cast();

// Phase 1: determine total length
let mut offset = 0;
loop {
let tag = unsafe { core::ptr::read(base.add(offset)) };
offset += match tag {
PCI_RESTBL_QWORDADDRSPEC_TAG => 3 + 0x2B,
PCI_RESTBL_END_TAG => break,
_ => panic!("{tag}"), // Unknown tag - bailing
};
}

// Phase 2: parse descriptors from resource table
let mut bfr: &[u8] = unsafe { slice::from_raw_parts(base, offset) };
let mut descriptors = Vec::new();
while !bfr.is_empty() {
match bfr[0] {
PCI_RESTBL_QWORDADDRSPEC_TAG => {
let descriptor = QwordAddressSpaceDescriptor {
resource_range_type: ResourceRangeType::from(bfr[0x03]),
general_flags: bfr[0x04],
type_specific_flags: bfr[0x05],
granularity: u64::from_le_bytes(bfr[0x06..0x06 + 8].try_into().unwrap()),
address_min: u64::from_le_bytes(bfr[0x0E..0x0E + 8].try_into().unwrap()),
address_max: u64::from_le_bytes(bfr[0x16..0x16 + 8].try_into().unwrap()),
translation_offset: u64::from_le_bytes(bfr[0x1E..0x1E + 8].try_into().unwrap()),
address_length: u64::from_le_bytes(bfr[0x26..0x26 + 8].try_into().unwrap()),
};
descriptors.push(descriptor);

bfr = &bfr[3 + 0x2B..];
}
_ => break,
}
}

descriptors
}

#[cfg(test)]
mod tests {
use crate::proto::pci::configuration::ResourceRangeType;

#[test]
fn parse() {
// example acpi pci qword configuration table export from a qemu vm
const BFR: &[u8] = &[
138, 43, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 255, 111, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 138, 43, 0, 0, 0, 0, 32,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 255, 255, 15, 129, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 16, 1, 0, 0, 0, 0, 138, 43, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 192, 0, 0, 0, 255, 255, 15, 0, 192, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
16, 0, 0, 0, 0, 0, 138, 43, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 121, 0,
];
let configuration = super::parse(BFR.as_ptr().cast());
assert_eq!(configuration.len(), 4);
let (mut cnt_mem, mut cnt_io, mut cnt_bus) = (0, 0, 0);
for entry in &configuration {
match entry.resource_range_type {
ResourceRangeType::Memory => cnt_mem += 1,
ResourceRangeType::Io => cnt_io += 1,
ResourceRangeType::Bus => cnt_bus += 1,
_ => unreachable!(),
}
}
assert_eq!(cnt_mem, 2);
assert_eq!(cnt_io, 1);
assert_eq!(cnt_bus, 1);
}
}
145 changes: 145 additions & 0 deletions uefi/src/proto/pci/enumeration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

//! PCI Bus device function and bridge enumeration.

use core::mem;

use alloc::collections::btree_set::BTreeSet;

use super::root_bridge::PciRootBridgeIo;
use super::{FullPciIoAddress, PciIoAddress};

#[allow(unused)]
#[derive(Clone, Copy, Debug)]
struct PciRegister0 {
vendor_id: u16,
device_id: u16,
}

#[allow(unused)]
#[derive(Clone, Copy, Debug)]
struct PciRegister2 {
revision_id: u8,
prog_if: u8,
subclass: u8,
class: u8,
}

#[allow(unused)]
#[derive(Clone, Copy, Debug)]
struct PciRegister3 {
cache_line_size: u8,
latency_timer: u8,
header_type: u8,
bist: u8,
}

#[allow(unused)]
#[derive(Clone, Copy, Debug)]
struct PciHeader1Register6 {
secondary_latency_timer: u8,
subordinate_bus: u8,
secondary_bus: u8,
primary_bus: u8,
}

/// Read the 4byte pci register with the given `addr` and cast it into the given structured representation.
fn read_device_register_u32<T: Sized + Copy>(
proto: &mut PciRootBridgeIo,
addr: PciIoAddress,
) -> uefi::Result<T> {
unsafe {
let raw = proto.pci().read_one::<u32>(addr)?;
let reg: T = mem::transmute_copy(&raw);
Ok(reg)
}
}

// ##########################################################################################
// # Query Helpers (read from a device's configuration registers)

fn get_vendor_id(proto: &mut PciRootBridgeIo, addr: PciIoAddress) -> uefi::Result<u16> {
read_device_register_u32::<PciRegister0>(proto, addr.with_register(0)).map(|v| v.vendor_id)
}

fn get_classes(proto: &mut PciRootBridgeIo, addr: PciIoAddress) -> uefi::Result<(u8, u8)> {
let reg = read_device_register_u32::<PciRegister2>(proto, addr.with_register(2 * 4))?;
Ok((reg.class, reg.subclass))
}

fn get_header_type(proto: &mut PciRootBridgeIo, addr: PciIoAddress) -> uefi::Result<u8> {
read_device_register_u32::<PciRegister3>(proto, addr.with_register(3 * 4))
.map(|v| v.header_type)
}

fn get_secondary_bus_range(
proto: &mut PciRootBridgeIo,
addr: PciIoAddress,
) -> uefi::Result<(u8, u8)> {
let reg = read_device_register_u32::<PciHeader1Register6>(proto, addr.with_register(6 * 4))?;
Ok((reg.secondary_bus, reg.subordinate_bus))
}

// ##########################################################################################
// # Recursive visitor implementation

fn visit_function(
proto: &mut PciRootBridgeIo,
addr: PciIoAddress,
queue: &mut BTreeSet<FullPciIoAddress>,
) -> 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));
let (base_class, sub_class) = get_classes(proto, addr)?;
if base_class == 0x6 && sub_class == 0x4 && get_header_type(proto, addr)? == 0x01 {
// This is a PCI-to-PCI bridge controller. The current `addr` is the address with which it's
// mounted in the PCI tree we are currently traversing. Now we query its header, where
// the bridge tells us a range of addresses [secondary;subordinate], with which the other
// side of the bridge is mounted into the PCI tree.
let (secondary_bus_nr, subordinate_bus_nr) = get_secondary_bus_range(proto, addr)?;
if secondary_bus_nr == 0 || subordinate_bus_nr < secondary_bus_nr {
// If the secondary bus number is the root number, or if the range is invalid - this hardware
// is so horribly broken that we refrain from touching it. It might explode - or worse!
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)?;
}
}
Ok(())
}

fn visit_device(
proto: &mut PciRootBridgeIo,
addr: PciIoAddress,
queue: &mut BTreeSet<FullPciIoAddress>,
) -> uefi::Result<()> {
if get_vendor_id(proto, addr)? == 0xFFFF {
return Ok(()); // device doesn't exist
}
visit_function(proto, addr.with_function(0), queue)?;
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)?;
}
}

Ok(())
}

pub(crate) fn visit_bus(
proto: &mut PciRootBridgeIo,
addr: PciIoAddress,
queue: &mut BTreeSet<FullPciIoAddress>,
) -> 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)?;
}
Ok(())
}
Loading