diff --git a/Cargo.lock b/Cargo.lock index 9efce8a55..185008917 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,14 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "acpi_tables" +version = "0.2.1" +source = "git+https://github.com/oxidecomputer/acpi_tables.git#a4e63474d63ec81dbaacebebf41aefac743dc045" +dependencies = [ + "zerocopy 0.8.27", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -1827,7 +1835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5349,6 +5357,7 @@ dependencies = [ name = "propolis" version = "0.1.0" dependencies = [ + "acpi_tables", "anyhow", "async-trait", "bhyve_api 0.0.0", @@ -6152,7 +6161,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7320,7 +7329,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.1.2", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7329,7 +7338,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 55659ade2..cbb3f2b20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ crucible = { git = "https://github.com/oxidecomputer/crucible", rev = "7103cd3a3 crucible-client-types = { git = "https://github.com/oxidecomputer/crucible", rev = "7103cd3a3d7b0112d2949dd135db06fef0c156bb" } # External dependencies +acpi_tables = "0.2.0" anyhow = "1.0" async-trait = "0.1.88" atty = "0.2.14" @@ -180,6 +181,8 @@ usdt = { version = "0.5", default-features = false } uuid = "1.3.2" zerocopy = "0.8.25" +[patch.crates-io] +acpi_tables = { git = 'https://github.com/oxidecomputer/acpi_tables.git' } # # It's common during development to use a local copy of various complex diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index ee7ebdac6..738478ba2 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -111,6 +111,12 @@ pub enum MachineInitError { /// Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; +/// End address of the 32-bit PCI MMIO window. +// XXX(acpi): Value inherited from the original EDK2 static tables. It should +// match the actual memory regions registered in the instance. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/PlatformPei/Platform.c#L180-L192 +const PCI_MMIO32_END: usize = 0xfeef_ffff; + fn get_spec_guest_ram_limits(spec: &Spec) -> (usize, usize) { let memsize = spec.board.memory_mb as usize * MB; let lowmem = memsize.min(3 * GB); @@ -393,16 +399,25 @@ impl MachineInitializer<'_> { continue; } - let (irq, port) = match desc.num { - SerialPortNumber::Com1 => (ibmpc::IRQ_COM1, ibmpc::PORT_COM1), - SerialPortNumber::Com2 => (ibmpc::IRQ_COM2, ibmpc::PORT_COM2), - SerialPortNumber::Com3 => (ibmpc::IRQ_COM3, ibmpc::PORT_COM3), - SerialPortNumber::Com4 => (ibmpc::IRQ_COM4, ibmpc::PORT_COM4), + let (irq, port, uart_name) = match desc.num { + SerialPortNumber::Com1 => { + (ibmpc::IRQ_COM1, ibmpc::PORT_COM1, "COM1") + } + SerialPortNumber::Com2 => { + (ibmpc::IRQ_COM2, ibmpc::PORT_COM2, "COM2") + } + SerialPortNumber::Com3 => { + (ibmpc::IRQ_COM3, ibmpc::PORT_COM3, "COM3") + } + SerialPortNumber::Com4 => { + (ibmpc::IRQ_COM4, ibmpc::PORT_COM4, "COM4") + } }; - let dev = LpcUart::new(chipset.irq_pin(irq).unwrap()); + let dev = + LpcUart::new(uart_name, irq, chipset.irq_pin(irq).unwrap()); dev.set_autodiscard(true); - LpcUart::attach(&dev, &self.machine.bus_pio, port); + dev.attach(&self.machine.bus_pio, port); self.devices.insert(name.to_owned(), dev.clone()); if desc.num == SerialPortNumber::Com1 { assert!(com1.is_none()); @@ -899,9 +914,13 @@ impl MachineInitializer<'_> { // Set up an LPC uart for ASIC management comms from the guest. // // NOTE: SoftNpu squats on com4. - let uart = LpcUart::new(chipset.irq_pin(ibmpc::IRQ_COM4).unwrap()); + let uart = LpcUart::new( + "COM4", + ibmpc::IRQ_COM4, + chipset.irq_pin(ibmpc::IRQ_COM4).unwrap(), + ); uart.set_autodiscard(true); - LpcUart::attach(&uart, &self.machine.bus_pio, ibmpc::PORT_COM4); + uart.attach(&self.machine.bus_pio, ibmpc::PORT_COM4); self.devices .insert(SpecKey::Name("softnpu-uart".to_string()), uart.clone()); @@ -1224,8 +1243,38 @@ impl MachineInitializer<'_> { Ok(Some(order.finish())) } + fn generate_acpi_tables( + &self, + cpus: u8, + ) -> Result { + let (lowmem, _) = get_spec_guest_ram_limits(self.spec); + let generators: Vec<_> = self + .devices + .values() + .filter_map(|dev| dev.as_dsdt_generator()) + .collect(); + + let config = &fwcfg::formats::AcpiConfig { + num_cpus: cpus, + pci_window_32: fwcfg::formats::PciWindow { + base: lowmem as u64, + end: PCI_MMIO32_END as u64, + }, + // XXX(acpi): Value inherited from the original EDK2 static tables, + // where the 64-bit PCI MMIO region was never set. It + // should match the actual memory regions registered in + // the instance. + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L284-L286 + pci_window_64: fwcfg::formats::PciWindow { base: 0, end: 0 }, + dsdt_generators: &generators, + }; + let acpi_tables = fwcfg::formats::AcpiTablesBuilder::new(config); + + Ok(acpi_tables.finish()) + } + /// Initialize qemu `fw_cfg` device, and populate it with data including CPU - /// count, SMBIOS tables, and attached RAM-FB device. + /// count, SMBIOS and ACPI tables, and attached RAM-FB device. /// /// Should not be called before [`Self::initialize_rom()`]. pub fn initialize_fwcfg( @@ -1270,6 +1319,19 @@ impl MachineInitializer<'_> { .insert_named("etc/e820", e820_entry) .map_err(|e| MachineInitError::FwcfgInsertFailed("e820", e))?; + let acpi_entries = self.generate_acpi_tables(cpus)?; + fwcfg.insert_named("etc/acpi/tables", acpi_entries.tables).map_err( + |e| MachineInitError::FwcfgInsertFailed("acpi/tables", e), + )?; + fwcfg + .insert_named("etc/acpi/rsdp", acpi_entries.rsdp) + .map_err(|e| MachineInitError::FwcfgInsertFailed("acpi/rsdp", e))?; + fwcfg + .insert_named("etc/table-loader", acpi_entries.table_loader) + .map_err(|e| { + MachineInitError::FwcfgInsertFailed("table-loader", e) + })?; + let ramfb = ramfb::RamFb::create( self.log.new(slog::o!("component" => "ramfb")), ); diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index 507284faf..c0e7466f5 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -43,6 +43,12 @@ const PAGE_OFFSET: u64 = 0xfff; // Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; +/// End address of the 32-bit PCI MMIO window. +// XXX(acpi): Value inherited from the original EDK2 static tables. It should +// match the actual memory regions registered in the instance. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/PlatformPei/Platform.c#L180-L192 +const PCI_MMIO32_END: usize = 0xfeef_ffff; + const MIN_RT_THREADS: usize = 8; const BASE_RT_THREADS: usize = 4; @@ -1032,6 +1038,36 @@ fn generate_bootorder( Ok(Some(order.finish())) } +fn generate_acpi_tables( + cpus: u8, + lowmem: usize, + inventory: &Inventory, +) -> anyhow::Result { + let generators: Vec<_> = inventory + .devs + .values() + .filter_map(|dev| dev.as_dsdt_generator()) + .collect(); + + let config = &fwcfg::formats::AcpiConfig { + num_cpus: cpus, + pci_window_32: fwcfg::formats::PciWindow { + base: lowmem as u64, + end: PCI_MMIO32_END as u64, + }, + // XXX(acpi): Value inherited from the original EDK2 static tables, + // where the 64-bit PCI MMIO region was never set. It + // should match the actual memory regions registered in + // the instance. + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L284-L286 + pci_window_64: fwcfg::formats::PciWindow { base: 0, end: 0 }, + dsdt_generators: &generators, + }; + let acpi_tables = fwcfg::formats::AcpiTablesBuilder::new(config); + + Ok(acpi_tables.finish()) +} + fn setup_instance( config: config::Config, from_restore: bool, @@ -1139,10 +1175,26 @@ fn setup_instance( guard.inventory.register(&hpet); // UARTs - let com1 = LpcUart::new(chipset_lpc.irq_pin(ibmpc::IRQ_COM1).unwrap()); - let com2 = LpcUart::new(chipset_lpc.irq_pin(ibmpc::IRQ_COM2).unwrap()); - let com3 = LpcUart::new(chipset_lpc.irq_pin(ibmpc::IRQ_COM3).unwrap()); - let com4 = LpcUart::new(chipset_lpc.irq_pin(ibmpc::IRQ_COM4).unwrap()); + let com1 = LpcUart::new( + "COM1", + ibmpc::IRQ_COM1, + chipset_lpc.irq_pin(ibmpc::IRQ_COM1).unwrap(), + ); + let com2 = LpcUart::new( + "COM2", + ibmpc::IRQ_COM2, + chipset_lpc.irq_pin(ibmpc::IRQ_COM2).unwrap(), + ); + let com3 = LpcUart::new( + "COM3", + ibmpc::IRQ_COM3, + chipset_lpc.irq_pin(ibmpc::IRQ_COM3).unwrap(), + ); + let com4 = LpcUart::new( + "COM4", + ibmpc::IRQ_COM4, + chipset_lpc.irq_pin(ibmpc::IRQ_COM4).unwrap(), + ); com1_sock.spawn( Arc::clone(&com1) as Arc, @@ -1156,10 +1208,10 @@ fn setup_instance( com4.set_autodiscard(true); let pio = &machine.bus_pio; - LpcUart::attach(&com1, pio, ibmpc::PORT_COM1); - LpcUart::attach(&com2, pio, ibmpc::PORT_COM2); - LpcUart::attach(&com3, pio, ibmpc::PORT_COM3); - LpcUart::attach(&com4, pio, ibmpc::PORT_COM4); + com1.attach(pio, ibmpc::PORT_COM1); + com2.attach(pio, ibmpc::PORT_COM2); + com3.attach(pio, ibmpc::PORT_COM3); + com4.attach(pio, ibmpc::PORT_COM4); guard.inventory.register_instance(&com1, "com1"); guard.inventory.register_instance(&com2, "com2"); guard.inventory.register_instance(&com3, "com3"); @@ -1375,6 +1427,18 @@ fn setup_instance( let e820_entry = generate_e820(machine, log).expect("can build E820 table"); fwcfg.insert_named("etc/e820", e820_entry).unwrap(); + let acpi_entries = generate_acpi_tables(cpus, lowmem, &guard.inventory) + .expect("failed to build ACPI tables"); + fwcfg + .insert_named("etc/acpi/tables", acpi_entries.tables) + .context("failed to insert ACPI tables")?; + fwcfg + .insert_named("etc/acpi/rsdp", acpi_entries.rsdp) + .context("failed to insert ACPI RSDP")?; + fwcfg + .insert_named("etc/table-loader", acpi_entries.table_loader) + .context("failed to insert ACPI table-loader")?; + fwcfg.attach(pio, &machine.acc_mem); guard.inventory.register(&fwcfg); diff --git a/lib/propolis/Cargo.toml b/lib/propolis/Cargo.toml index a3e243155..a8fe57e32 100644 --- a/lib/propolis/Cargo.toml +++ b/lib/propolis/Cargo.toml @@ -22,6 +22,7 @@ tokio = { workspace = true, features = ["full"] } futures.workspace = true paste.workspace = true pin-project-lite.workspace = true +acpi_tables.workspace = true anyhow.workspace = true rgb_frame.workspace = true rfb.workspace = true diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs new file mode 100644 index 000000000..53b00ebf6 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -0,0 +1,1193 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a DSDT and SSDT ACPI tables for an instance. +//! +//! The [`Dsdt`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. +//! +//! The AML code can also be generated by the objects being represented in the +//! DSDT table, which is often a better practice as it keeps internal +//! configuration and AML representation close to each other. +//! +//! Structs that implement the [`DsdtGenerator`] trait can be passed to +//! [`DsdtConfig`] and their AML code will be added to the scope they selected. + +// XXX(acpi): Most of the DSDT and SSDT tables are generated here to keep them +// consistent with the original EDK2 static tables. In the future +// they could be created by the devices they represent. For example, +// the _SB scope can be created by the I440FxHostBridge struct, the +// LPC by Piix3Lpc etc., but they currently lack all the information +// necessary to generate the tables. This pattern is already used +// for some devices when possible. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl + +use super::{IO_APIC_ADDR, LOCAL_APIC_ADDR, PCI_LINK_IRQS, SCI_IRQ}; +use acpi_tables::{aml, sdt::Sdt, Aml, AmlSink}; + +/// The ACPI scope in which DsdtGenerators are placed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DsdtScope { + SystemBus, // \_SB scope. + PciRoot, // \_SB.PCI0 scope. + Lpc, // \_SB.PCI0.LPC scope. +} + +/// An implementer of DsdtGenerator is able to generate AML code to be loaded +/// into the DSDT ACPI table. +pub trait DsdtGenerator: Aml { + /// Returns the scope of the DSDT table in which the generated AML code + /// should be placed. + fn dsdt_scope(&self) -> DsdtScope; +} + +/// Wraps a list of DsdtGenerators to help generate their AML code in places +/// where a `&dyn Aml` is needed. +struct DsdtGeneratorAml<'a> { + generators: &'a [&'a dyn DsdtGenerator], + scope: DsdtScope, +} + +impl<'a> DsdtGeneratorAml<'a> { + fn new(generators: &'a [&'a dyn DsdtGenerator], scope: DsdtScope) -> Self { + Self { generators, scope } + } +} + +impl<'a> Aml for DsdtGeneratorAml<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + self.generators + .iter() + .filter(|&&g| g.dsdt_scope() == self.scope) + .for_each(|&g| g.to_aml_bytes(sink)); + } +} + +/// Values for the PM1a_CNT.SLP_TYP register to enter different sleep states. +/// +/// +const PM1A_CNT_SLP_TYP_S0: u8 = 5; +const PM1A_CNT_SLP_TYP_S3: u8 = 1; +const PM1A_CNT_SLP_TYP_S4: u8 = 2; +const PM1A_CNT_SLP_TYP_S5: u8 = 0; + +pub struct DsdtConfig<'a> { + pub generators: &'a [&'a dyn DsdtGenerator], +} + +/// The DSDT table is part of the fixed ACPI tables and is used to describe +/// system resources. +/// +/// +pub struct Dsdt<'a> { + config: DsdtConfig<'a>, +} + +impl<'a> Dsdt<'a> { + pub fn new(config: DsdtConfig<'a>) -> Self { + Self { config } + } +} + +impl<'a> Aml for Dsdt<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut dsdt = Vec::new(); + + // XXX(acpi): This is an artifact inserted into the AML code to keep + // the DSDT table exactly the same as the static EDK2 tables + // used previously. It's not functionally necessary. + aml::If::new( + &aml::ZERO, + vec![&aml::External::new( + "\\_SB_.PCI0._CRS.FWDT".into(), + aml::ExternalObjectType::OperationRegion, + None, + )], + ) + .to_aml_bytes(&mut dsdt); + + // Sleep states. + SleepState::new("_S0_", PM1A_CNT_SLP_TYP_S0).to_aml_bytes(&mut dsdt); + SleepState::new("_S5_", PM1A_CNT_SLP_TYP_S5).to_aml_bytes(&mut dsdt); + + // System bus namespace (\_SB). + aml::Scope::new( + "_SB_".into(), + vec![ + &PciRootBridge { generators: self.config.generators }, + &DsdtGeneratorAml::new( + self.config.generators, + DsdtScope::SystemBus, + ), + ], + ) + .to_aml_bytes(&mut dsdt); + + // DSDT table. + // XXX(acpi): OEM ID, table ID, and revision are kept the same as the + // original static EDK2 tables for consistency. They could + // be set to Propolis-specific values in the future. + let mut sdt = Sdt::new(*b"DSDT", 36, 1, *b"INTEL ", *b"OVMF ", 0x4); + sdt.append_slice(dsdt.as_slice()); + sdt.to_aml_bytes(sink); + } +} + +/// Describe a sleep state. +struct SleepState<'a> { + state: &'a str, + pm1a_cnt: u8, +} + +impl<'a> SleepState<'a> { + fn new(state: &'a str, pm1a_cnt: u8) -> Self { + Self { state, pm1a_cnt } + } +} + +impl<'a> Aml for SleepState<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Name::new( + self.state.into(), + &aml::Package::new(vec![ + &self.pm1a_cnt, // PM1a_CNT.SLP_TYP value to enter this sleep state. + &aml::ZERO, // PM1b_CNT.SLP_TYP value. PM1b is not currently used in Propolis. + &aml::ZERO, // Reserved. + &aml::ZERO, // Reserved. + ]), + ) + .to_aml_bytes(sink); + } +} + +/// PCI root bridge namespace (\_SB.PCI0). +struct PciRootBridge<'a> { + generators: &'a [&'a dyn DsdtGenerator], +} + +impl<'a> Aml for PciRootBridge<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "PCI0".into(), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0A03")), + &aml::Name::new("_ADR".into(), &aml::ZERO), + &aml::Name::new("_BBN".into(), &aml::ZERO), + &aml::Name::new("_UID".into(), &aml::ZERO), + &PciRootBridgeCrs {}, + &PciRootBridgePrt {}, + &PciRootBridgeLpc { generators: self.generators }, + &DsdtGeneratorAml::new(self.generators, DsdtScope::PciRoot), + ], + ) + .to_aml_bytes(sink); + } +} + +/// I/O port range for PCI configuration. +/// +/// +const PCI_CONFIG_IO_BASE: u16 = 0x0cf8; +const PCI_CONFIG_IO_SIZE: u8 = 8; + +/// Bus number range for the PCI0 root bridge. +const PCI_BUS_START: u16 = 0x00; +const PCI_BUS_END: u16 = 0xff; + +/// MMIO address region used for legacy VGA devices. +/// +/// +const LEGACY_VGA_BASE: u32 = 0x000a_0000; +const LEGACY_VGA_LIMIT: u32 = 0x000b_ffff; + +/// _CRS method for the PCI0 device (\_SB.PCI0._CRS). +/// +/// Refer to section 4.3 of the PCI Firmware Specification for more +/// information. +struct PciRootBridgeCrs {} + +// XXX(acpi): This implementation currently follows the original static EDK2 +// tables. It can be simplified to return a single ResourceTemplate +// with all final values already populated instead of dynamically +// updating them based on the values from FWDT. +impl Aml for PciRootBridgeCrs { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + // \_SB.PCI0.CRES + // + // This ResourceTemplate contains the base values for the PCI I/O ports + // and MMIO region reservations. + let mut cres: Vec<&dyn Aml> = Vec::new(); + + // PCI device numbers that belong to this bridge. + let bus_number = + aml::AddressSpace::new_bus_number(PCI_BUS_START, PCI_BUS_END); + cres.push(&bus_number); + + // Legacy PCI configuration I/O ports. + let pci_config_io_ports = aml::IO::new( + PCI_CONFIG_IO_BASE, + PCI_CONFIG_IO_BASE, + 1, + PCI_CONFIG_IO_SIZE, + ); + cres.push(&pci_config_io_ports); + + // I/O ports below the PCI config ports (0x0000-0x0cf7). + let pci_io_ports_low = + aml::AddressSpace::new_io(0x0000, PCI_CONFIG_IO_BASE - 1, None); + cres.push(&pci_io_ports_low); + + // IO ports above the PCI config ports (0x0d00-0xffff). + let pci_io_ports_high = aml::AddressSpace::new_io( + PCI_CONFIG_IO_BASE + PCI_CONFIG_IO_SIZE as u16, + 0xffff, + None, + ); + cres.push(&pci_io_ports_high); + + // Legacy VGA MMIO region. + let legacy_vga = aml::AddressSpace::new_memory( + aml::AddressSpaceCacheable::Cacheable, + true, + LEGACY_VGA_BASE, + LEGACY_VGA_LIMIT, + None, + ); + cres.push(&legacy_vga); + + // The _CRS method needs to reference the 32-bit PCI MMIO region to + // update its range based on the instance configuration. The reference + // is based on the byte offset of this AddressSpace inside the CRES + // ResourceTemplate. + let mmio32_offset = aml_len(&cres); + let mmio32 = aml::AddressSpace::new_memory( + aml::AddressSpaceCacheable::NotCacheable, + true, + 0xf800_0000_u32, + 0xfffb_ffff_u32, + None, + ); + cres.push(&mmio32); + + aml::Name::new("CRES".into(), &aml::ResourceTemplate::new(cres)) + .to_aml_bytes(sink); + + // \_SB.PCI0.CR64 + // + // This ResourceTemplate contains the 64-bit PCI MMIO region. The _CRS + // method concatenates it with CRES, if necessary, based on the + // instance configuration. + let mut cr64: Vec<&dyn Aml> = Vec::new(); + + // The _CRS method needs to reference the 64-bit PCI MMIO region to + // update its range based on the instance configuration. The reference + // is based on the byte offset of this AddressSpace inside the CR64 + // ResourceTemplate. + let mmio64_offset = aml_len(&cr64); + let mmio64 = aml::AddressSpace::new_memory( + aml::AddressSpaceCacheable::Cacheable, + true, + 0x0080_0000_0000_u64, + 0x0fff_ffff_ffff_u64, + None, + ); + cr64.push(&mmio64); + + aml::Name::new("CR64".into(), &aml::ResourceTemplate::new(cr64)) + .to_aml_bytes(sink); + + // \_SB.PCI0._CRS + // + // This method returns a ResourceTemplate describing the PCI root + // bridge resources. + // + // It read the FWDT OperationRegion that is declared in the SSDT. This + // region is populated by Propolis in lib/propolis/src/hw/qemu/fwcfg.rs + // and it stores the 32-bit and 64-bit MMIO regions reserved for PCI + // devices. + aml::Method::new( + "_CRS".into(), + 0, + true, + vec![ + &aml::Field::new( + // Create references to values in the FWDT OperationRegion. + "FWDT".into(), + aml::FieldAccessType::QWord, + aml::FieldLockRule::NoLock, + aml::FieldUpdateRule::Preserve, + vec![ + aml::FieldEntry::Named(*b"P0S_", 64), + aml::FieldEntry::Named(*b"P0E_", 64), + aml::FieldEntry::Named(*b"P0L_", 64), + aml::FieldEntry::Named(*b"P1S_", 64), + aml::FieldEntry::Named(*b"P1E_", 64), + aml::FieldEntry::Named(*b"P1L_", 64), + ], + ), + &aml::Field::new( + "FWDT".into(), + aml::FieldAccessType::DWord, + aml::FieldLockRule::NoLock, + aml::FieldUpdateRule::Preserve, + vec![ + aml::FieldEntry::Named(*b"P0SL", 32), + aml::FieldEntry::Named(*b"P0SH", 32), + aml::FieldEntry::Named(*b"P0EL", 32), + aml::FieldEntry::Named(*b"P0EH", 32), + aml::FieldEntry::Named(*b"P0LL", 32), + aml::FieldEntry::Named(*b"P0LH", 32), + aml::FieldEntry::Named(*b"P1SL", 32), + aml::FieldEntry::Named(*b"P1SH", 32), + aml::FieldEntry::Named(*b"P1EL", 32), + aml::FieldEntry::Named(*b"P1EH", 32), + aml::FieldEntry::Named(*b"P1LL", 32), + aml::FieldEntry::Named(*b"P1LH", 32), + ], + ), + // Create fields that reference values from the mmio32 + // AddressSpace from CRES. + &aml::CreateDWordField::new( + &aml::Path::new("PS32"), + &aml::Path::new("CRES"), + &(mmio32_offset + 0x0a), // Byte offset for min. + ), + &aml::CreateDWordField::new( + &aml::Path::new("PE32"), + &aml::Path::new("CRES"), + &(mmio32_offset + 0x0e), // Byte offset for max. + ), + &aml::CreateDWordField::new( + &aml::Path::new("PL32"), + &aml::Path::new("CRES"), + &(mmio32_offset + 0x16), // Byte offset for len. + ), + // Update the values of mmio32 based on the FWDT. + &aml::Store::new( + &aml::Path::new("PS32"), + &aml::Path::new("P0SL"), + ), + &aml::Store::new( + &aml::Path::new("PE32"), + &aml::Path::new("P0EL"), + ), + &aml::Store::new( + &aml::Path::new("PL32"), + &aml::Path::new("P0LL"), + ), + // Check if a 64-bit MMIO region is needed. + &aml::If::new( + &aml::LogicalAnd::new( + &aml::Equal::new(&aml::Path::new("P1SL"), &aml::ZERO), + &aml::Equal::new(&aml::Path::new("P1SH"), &aml::ZERO), + ), + vec![&aml::Return::new(&aml::Path::new("CRES"))], + ), + &aml::Else::new(vec![ + // Create fields that reference values from the mmio64 + // AddressSpace from CR64. + &aml::CreateQWordField::new( + &aml::Path::new("PS64"), + &aml::Path::new("CR64"), + &(mmio64_offset + 0x0e), + ), + &aml::CreateQWordField::new( + &aml::Path::new("PE64"), + &aml::Path::new("CR64"), + &(mmio64_offset + 0x16), + ), + &aml::CreateQWordField::new( + &aml::Path::new("PL64"), + &aml::Path::new("CR64"), + &(mmio64_offset + 0x26), + ), + // Update the values of mmio64 based on the FWDT. + &aml::Store::new( + &aml::Path::new("PS64"), + &aml::Path::new("P1S_"), + ), + &aml::Store::new( + &aml::Path::new("PE64"), + &aml::Path::new("P1E_"), + ), + &aml::Store::new( + &aml::Path::new("PL64"), + &aml::Path::new("P1L_"), + ), + // Concatenate CRES and CR64. + &aml::ConcatRes::new( + &aml::Local(0), + &aml::Path::new("CRES"), + &aml::Path::new("CR64"), + ), + &aml::Return::new(&aml::Local(0)), + ]), + ], + ) + .to_aml_bytes(sink); + } +} + +fn aml_len(vec: &[&dyn Aml]) -> usize { + let mut sink = Vec::new(); + vec.iter().fold(0, |_acc, aml| { + aml.to_aml_bytes(&mut sink); + sink.len() + }) +} + +/// Number of devices in the PCI0 root bridge. +// XXX(acpi): Value inherited from the original EDK2 static tables. It should +// be updated to match Propolis's expectations. +const PCI_DEVICES: u8 = 16; +const PCI_INT_PINS: u8 = 4; + +/// _PRT method for the PCI0 device (\_SB.PCI0._PRT) +/// +/// +struct PciRootBridgePrt {} + +impl Aml for PciRootBridgePrt { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let sources = ["^LPC_.LNKA", "^LPC_.LNKB", "^LPC_.LNKC", "^LPC_.LNKD"]; + let sources_len: u8 = sources.len() as u8; + + let mut ptr_entries = Vec::new(); + for device in 0..PCI_DEVICES { + for pin in 0..PCI_INT_PINS { + let source = if device == 1 && pin == 0 { + // Device 1, Pin 0 requires special handling. + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl#L200-L220 + "^LPC_.LNKS" + } else { + // PCI devices are connected in a crossing pattern to + // evenly distribute load across the interrupt lines. + // + // ┌──────────┐ ┌──────────┐ ┌──────────┐ + // │PCI Device│ │PCI Device│ │PCI Device│ + // │ #1 │ │ #2 │ │ #3 │ + // ┌───────────────┐ │ │ │ │ │ │ + // │ IO/APIC │ │ INTA │ │ │ │ │ + // │ │┌───┼────*─────┼┐┌───┼────*─────┼┐┌───┼────*─────┼─-- + // │ LNKA ├┼──┐│ INTB │││ │ │││ │ │ + // │ LNKB ├┼─┐└┼────*─────┼┼┘┌──┼────*─────┼┼┘┌──┼────*─────┼─-- + // │ LNKC ├┼┐│ │ INTC ││ │ │ ││ │ │ │ + // │ LNKD ├┘│└─┼────*─────┼┼─┘┌─┼────*─────┼┼─┘┌─┼────*─────┼─-- + // └───────────────┘ │ │ INTD ││ │ │ ││ │ │ │ + // └──┼────*─────┼┼──┘┌┼────*─────┼┼──┘┌┼────*─────┼─-- + // └──────────┘└───┘└──────────┘└───┘└──────────┘ + // + let idx = (device + pin + sources_len - 1) % sources_len; + sources[idx as usize] + }; + + ptr_entries.push(PrtEntry { device, pin, source }); + } + } + let ptr = ptr_entries.iter().map(|p| p as &dyn Aml).collect(); + + aml::Method::new( + "_PRT".into(), + 0, + false, + vec![&aml::Return::new(&aml::Package::new(ptr))], + ) + .to_aml_bytes(sink); + } +} + +/// Low-word of an _ADR value that refers to all PCI functions. +/// +/// +const PCI_ADR_ALL_FUNC: u32 = 0xffff; + +/// Representation of an entry in the _PRT table. +struct PrtEntry<'a> { + device: u8, + pin: u8, + source: &'a str, +} + +impl<'a> Aml for PrtEntry<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let addr: aml::DWord = ((self.device as u32) << 16) | PCI_ADR_ALL_FUNC; + + aml::Package::new(vec![ + &addr, + &self.pin, + &aml::Path::new(self.source), + &aml::ZERO, + ]) + .to_aml_bytes(sink); + } +} + +/// PCI to ISA bridge for the PCI0 device (\_SB.PCI0.LPC). +/// +/// Refer to the original _PRT table from EDK2 for more details on what is being +/// defined here. +/// +/// +struct PciRootBridgeLpc<'a> { + generators: &'a [&'a dyn DsdtGenerator], +} + +// XXX(acpi): This table is currently kept the same as the original EDK2 static +// table, but it could be modernized to remove devices that are not +// used in a virtual machine environment. +impl<'a> Aml for PciRootBridgeLpc<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "LPC_".into(), + vec![ + &aml::Name::new("_ADR".into(), &0x0001_0000_u64), + &Lnk::new("S", 0), + // PCI Interrupt Routing Configuration Registers, PIRQRC[A:D]. + &aml::OpRegion::new( + "PRR0".into(), + aml::OpRegionSpace::PCIConfig, + &0x60_u64, + &0x04_u64, + ), + &aml::Field::new( + "PRR0".into(), + aml::FieldAccessType::Any, + aml::FieldLockRule::NoLock, + aml::FieldUpdateRule::Preserve, + vec![ + aml::FieldEntry::Named(*b"PIRA", 8), + aml::FieldEntry::Named(*b"PIRB", 8), + aml::FieldEntry::Named(*b"PIRC", 8), + aml::FieldEntry::Named(*b"PIRD", 8), + ], + ), + // _STA method for LNKA, LNKB, LNKC, and LNKD. + &aml::Method::new( + "PSTA".into(), + 1, + false, + vec![ + &aml::If::new( + &aml::And::new(&aml::ZERO, &aml::Arg(0), &0x80_u64), + vec![&aml::Return::new(&0x09_u64)], + ), + &aml::Else::new(vec![&aml::Return::new(&0x0B_u64)]), + ], + ), + // _CRS method for LNKA, LNKB, LNKC, and LNKD. + &aml::Method::new( + "PCRS".into(), + 1, + true, + vec![ + &aml::Name::new( + "BUF0".into(), + &aml::ResourceTemplate::new(vec![ + &aml::Interrupt::new( + true, + false, + false, + true, + vec![0x00], + ), + ]), + ), + &aml::CreateDWordField::new( + &aml::Path::new("IRQW"), + &aml::Path::new("BUF0"), + &0x05_u64, // TODO: document. + ), + &aml::If::new( + &aml::LogicalNot::new(&aml::And::new( + &aml::ZERO, + &aml::Arg(0), + &0x80_u64, + )), + vec![&aml::Store::new( + &aml::Path::new("IRQW"), + &aml::Arg(0), + )], + ), + &aml::Return::new(&aml::Path::new("BUF0")), + ], + ), + // _PRS resource for LNKA, LNKB, LNKC, and LNKD. + &aml::Name::new( + "PPRS".into(), + &aml::ResourceTemplate::new(vec![&aml::Interrupt::new( + true, + false, + false, + true, + PCI_LINK_IRQS + .iter() + .filter(|i| **i != SCI_IRQ) // The SCI has special handling LNKS. + .map(|i| *i as u32) + .collect(), + )]), + ), + &Lnk::new("A", 1), + &Lnk::new("B", 2), + &Lnk::new("C", 3), + &Lnk::new("D", 4), + // Programmable Interrupt Controller (PIC). + &aml::Device::new( + "PIC_".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0000"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0020, 0x0020, 0x00, 0x02), + &aml::IO::new(0x00A0, 0x00A0, 0x00, 0x02), + &aml::IO::new(0x04d0, 0x04d0, 0x00, 0x02), + &aml::IrqNoFlags::new(2), + ]), + ), + ], + ), + // ISA DMA. + &aml::Device::new( + "DMAC".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0200"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0000, 0x0000, 0x00, 0x10), + &aml::IO::new(0x0081, 0x0081, 0x00, 0x03), + &aml::IO::new(0x0087, 0x0087, 0x00, 0x01), + &aml::IO::new(0x0089, 0x0089, 0x00, 0x03), + &aml::IO::new(0x008f, 0x008f, 0x00, 0x01), + &aml::IO::new(0x00c0, 0x00c0, 0x00, 0x20), + &aml::Dma::new( + aml::DmaChannelSpeed::Compatibility, + aml::DmaMasterStatus::NotMaster, + aml::DmaTransferType::Transfer8, + vec![4], + ), + ]), + ), + ], + ), + // 8254 Timer. + &aml::Device::new( + "TMR_".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0100"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0040, 0x0040, 0x00, 0x04), + &aml::IrqNoFlags::new(0), + ]), + ), + ], + ), + // Real Time Clock. + &aml::Device::new( + "RTC_".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0B00"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0070, 0x0070, 0x00, 0x02), + &aml::IrqNoFlags::new(8), + ]), + ), + ], + ), + // PCAT Speaker. + &aml::Device::new( + "SPKR".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0800"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![&aml::IO::new( + 0x0061, 0x0061, 0x01, 0x01, + )]), + ), + ], + ), + // Floating Point Coprocessor. + &aml::Device::new( + "FPU_".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0C04"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x00f0, 0x00f0, 0x00, 0x10), + &aml::IrqNoFlags::new(13), + ]), + ), + ], + ), + // Generic motherboard devices and pieces that don't fit + // anywhere else. + &aml::Device::new( + "XTRA".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0C02"), + ), + &aml::Name::new("_UID".into(), &aml::ONE), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0010, 0x0010, 0x00, 0x10), + &aml::IO::new(0x0022, 0x0022, 0x00, 0x1e), + &aml::IO::new(0x0044, 0x0044, 0x00, 0x1c), + &aml::IO::new(0x0062, 0x0062, 0x00, 0x02), + &aml::IO::new(0x0065, 0x0065, 0x00, 0x0b), + &aml::IO::new(0x0072, 0x0072, 0x00, 0x0e), + &aml::IO::new(0x0080, 0x0080, 0x00, 0x01), + &aml::IO::new(0x0084, 0x0084, 0x00, 0x03), + &aml::IO::new(0x0088, 0x0088, 0x00, 0x01), + &aml::IO::new(0x008c, 0x008c, 0x00, 0x03), + &aml::IO::new(0x0090, 0x0090, 0x00, 0x10), + &aml::IO::new(0x00a2, 0x00a2, 0x00, 0x1e), + &aml::IO::new(0x00e0, 0x00e0, 0x00, 0x10), + &aml::IO::new(0x01e0, 0x01e0, 0x00, 0x10), + &aml::IO::new(0x0160, 0x0160, 0x00, 0x10), + &aml::IO::new(0x0370, 0x0370, 0x00, 0x02), + &aml::IO::new(0x0402, 0x0402, 0x00, 0x01), + &aml::IO::new(0x0440, 0x0440, 0x00, 0x10), + // QEMU GPE0 BLK. + &aml::IO::new(0xafe0, 0xafe0, 0x00, 0x04), + // PMBLK1. + &aml::IO::new(0xb000, 0xb000, 0x00, 0x40), + // IO APIC. + &aml::Memory32Fixed::new( + false, + IO_APIC_ADDR, + 0x0000_1000, + ), + // LAPIC. + &aml::Memory32Fixed::new( + false, + LOCAL_APIC_ADDR, + 0x0010_0000, + ), + ]), + ), + ], + ), + &DsdtGeneratorAml::new(self.generators, DsdtScope::Lpc), + // QEMU panic device. + // + // XXX(acpi): This code could be generated by the QemuPvpanic + // struct and passed as a DsdtGenerator, but it's + // only present if the enable_isa configuration is + // enabled for the instance. So it's always + // generated here for now to maintain consistency + // with the original EDK2 static tables. + &aml::Device::new( + "PEVT".into(), + vec![ + &aml::Name::new("_HID".into(), &"QEMU0001"), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![&aml::IO::new( + 0x0505, 0x0505, 0x01, 0x01, + )]), + ), + &aml::OpRegion::new( + "PEOR".into(), + aml::OpRegionSpace::SystemIO, + &0x0505_u64, + &aml::ONE, + ), + &aml::Field::new( + "PEOR".into(), + aml::FieldAccessType::Byte, + aml::FieldLockRule::NoLock, + aml::FieldUpdateRule::Preserve, + vec![aml::FieldEntry::Named(*b"PEPT", 8)], + ), + &aml::Name::new("_STA".into(), &0x0f_u64), + &aml::Method::new( + "RDPT".into(), + 0, + false, + vec![ + &aml::Store::new( + &aml::Local(0), + &aml::Path::new("PEPT"), + ), + &aml::Return::new(&aml::Local(0)), + ], + ), + &aml::Method::new( + "WRPT".into(), + 1, + false, + vec![&aml::Store::new( + &aml::Path::new("PEPT"), + &aml::Arg(0), + )], + ), + ], + ), + ], + ) + .to_aml_bytes(sink); + } +} + +/// Represents a PCI IRQ link in the LPC device. +struct Lnk<'a> { + letter: &'a str, + uid: u32, +} + +impl<'a> Lnk<'a> { + fn new(letter: &'a str, uid: u32) -> Self { + Self { letter, uid } + } +} + +impl<'a> Lnk<'a> { + // AML code for the special SCI link. + // + fn to_aml_bytes_sci(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "LNKS".into(), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0C0F")), + &aml::Name::new("_UID".into(), &aml::ZERO), + &aml::Name::new("_STA".into(), &0x0b_u64), + &aml::Method::new("_SRS".into(), 1, false, vec![]), + &aml::Method::new("_DIS".into(), 0, false, vec![]), + &aml::Name::new( + "_PRS".into(), + &aml::ResourceTemplate::new(vec![&aml::Interrupt::new( + true, + false, + false, + true, + vec![0x09], + )]), + ), + &aml::Method::new( + "_CRS".into(), + 0, + false, + vec![&aml::Return::new(&aml::Path::new("_PRS"))], + ), + ], + ) + .to_aml_bytes(sink); + } +} + +impl<'a> Aml for Lnk<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + if self.uid == 0 { + self.to_aml_bytes_sci(sink); + return; + } + + let pir = aml::Path::new(&format!("PIR{}", self.letter)); + + aml::Device::new( + aml::Path::new(&format!("LNK{}", self.letter)), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0C0F")), + &aml::Name::new("_UID".into(), &self.uid), + &aml::Method::new( + "_STA".into(), + 0, + false, + vec![&aml::Return::new(&aml::MethodCall::new( + "PSTA".into(), + vec![&pir], + ))], + ), + &aml::Method::new( + "_DIS".into(), + 0, + false, + vec![&aml::Or::new(&pir, &pir, &0x80_u64)], + ), + &aml::Method::new( + "_CRS".into(), + 0, + false, + vec![&aml::Return::new(&aml::MethodCall::new( + "PCRS".into(), + vec![&pir], + ))], + ), + &aml::Method::new( + "_PRS".into(), + 0, + false, + vec![&aml::Return::new(&aml::MethodCall::new( + "PPRS".into(), + vec![], + ))], + ), + &aml::Method::new( + "_SRS".into(), + 1, + false, + vec![ + &aml::CreateDWordField::new( + &aml::Path::new("IRQW"), + &aml::Arg(0), + &0x05_u64, + ), + &aml::Store::new(&pir, &aml::Path::new("IRQW")), + ], + ), + ], + ) + .to_aml_bytes(sink); + } +} + +/// Length in bytes of the SSDT header. Used to calculate the offset of other +/// fields. +/// +/// +const SSDT_HEADER_LEN: usize = 36; + +/// Byte offset of the FWDT OperationRegion offset address field in the SSDT +/// table. This filed is updated in fwcfg.rs during table generation. +/// +/// SSDT header (36 bytes) + External operation prefix (1 byte) + +/// OperationRegion prefix (1 byte) + OperationRegion name (4 bytes) + +/// OperationRegion space (1 byte) + DWordPrefix (1 byte) +/// +/// +pub const SSDT_FWDT_ADDR_OFFSET: usize = SSDT_HEADER_LEN + 8; + +/// Number of bytes used to store the offset address value in the FWDT +/// OperationRegion. Size of a DWord. +pub const SSDT_FWDT_ADDR_LEN: usize = 4; + +/// The SSDT table is an extension to DSDT table and can be used to extend +/// resources defined in the DSDT. +/// +/// +pub struct Ssdt { + /// Offset of the area reserved for the FWDT OperationRegion data in the + /// overall ACPI tables storage. + fwdt_offset: usize, +} + +impl Ssdt { + pub fn new(fwdt_offset: usize) -> Self { + Self { fwdt_offset } + } +} + +// XXX(acpi): This implementation follows the original static EDK2 tables. It +// can probably be further simplified or eliminated entirely. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L426-L466 +impl Aml for Ssdt { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut ssdt = Vec::new(); + + // The FWDT OperationRegion is used to pass dynamic information about + // the instance from the platform to the virtual machine. The main + // information provided are the 32-bit and 64-bit PCI MMIO ranges. + // + // On boot, the \_SB.PCI0._CRS method reads FWDT and adjusts the PCI + // bus configuration based on the data it holds. + // + // XXX(acpi): This process can be removed if \_SB.PCI0._CRS returns a + // static ResourceTemplate that is generated with the right + // instance data. + aml::OpRegion::new( + "FWDT".into(), + aml::OpRegionSpace::SystemMemory, + &DWord::new(self.fwdt_offset as u32), + &DWord::new(0x30), + ) + .to_aml_bytes(&mut ssdt); + + // Sleep states. + // XXX(acpi): These sleep states are kept to keep the SSDT consistent + // with the original static EDK2 tables. Propolis doesn't + // handle these state properly, so they should be removed in + // the future. + // + // These values don't use the SleepState struct to keep the + // generated AML code consistent with original EDK tables. + aml::Name::new( + "\\_S3_".into(), + &aml::Package::new(vec![ + &Byte::new(PM1A_CNT_SLP_TYP_S3), + &Byte::new(0), + &Byte::new(0), + &Byte::new(0), + ]), + ) + .to_aml_bytes(&mut ssdt); + + aml::Name::new( + "\\_S4_".into(), + &aml::Package::new(vec![ + &Byte::new(PM1A_CNT_SLP_TYP_S4), + &Byte::new(0), + &Byte::new(0), + &Byte::new(0), + ]), + ) + .to_aml_bytes(&mut ssdt); + + // XXX(acpi): OEM ID, table ID, and revision are kept the same as the + // original static EDK2 tables for consistency. They could + // be set to Propolis-specific values in the future. + let mut sdt = Sdt::new(*b"SSDT", 36, 1, *b"REDHAT", *b"OVMF ", 0x1); + sdt.append_slice(ssdt.as_slice()); + sdt.to_aml_bytes(sink); + } +} + +// Provides consistent DWord AML values. The acpi_tables crate minimizes +// integers to the smallest word size that the number fits. +// +// XXX(acpi): Created just to keep tables consistent with the original EDK2 +// tables. +struct DWord { + value: u32, +} + +impl DWord { + fn new(value: u32) -> Self { + Self { value } + } +} + +impl Aml for DWord { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + sink.byte(0x0c); // DWordPrefix + sink.dword(self.value); + } +} + +// Provides consistent Byte AML values. The acpi_tables crate minimizes +// integers to the smallest word size that the number fits. +// +// XXX(acpi): Created just to keep tables consistent with the original EDK2 +// tables. +struct Byte { + value: u8, +} + +impl Byte { + fn new(value: u8) -> Self { + Self { value } + } +} + +impl Aml for Byte { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + sink.byte(0x0a); // BytePrefix + sink.byte(self.value); + } +} + +#[cfg(test)] +mod test { + use super::*; + + struct MockDsdtGenerator { + scope: DsdtScope, + } + impl DsdtGenerator for MockDsdtGenerator { + fn dsdt_scope(&self) -> DsdtScope { + self.scope + } + } + impl Aml for MockDsdtGenerator { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Name::new("TEST".into(), &format!("{:?}", self.scope)) + .to_aml_bytes(sink); + } + } + + #[test] + fn dsdt_generator_aml() { + let generators: Vec<&dyn DsdtGenerator> = vec![ + &MockDsdtGenerator { scope: DsdtScope::SystemBus }, + &MockDsdtGenerator { scope: DsdtScope::PciRoot }, + &MockDsdtGenerator { scope: DsdtScope::Lpc }, + ]; + + // Filter by SystemBus. + let mut got = Vec::new(); + DsdtGeneratorAml::new(&generators, DsdtScope::SystemBus) + .to_aml_bytes(&mut got); + + let mut expected = Vec::new(); + aml::Name::new("TEST".into(), &"SystemBus").to_aml_bytes(&mut expected); + assert_eq!(expected, got); + got.clear(); + + // Filter by PciRoot. + let mut got = Vec::new(); + DsdtGeneratorAml::new(&generators, DsdtScope::PciRoot) + .to_aml_bytes(&mut got); + + let mut expected = Vec::new(); + aml::Name::new("TEST".into(), &"PciRoot").to_aml_bytes(&mut expected); + assert_eq!(expected, got); + got.clear(); + + // Filter by Lpc. + let mut got = Vec::new(); + DsdtGeneratorAml::new(&generators, DsdtScope::Lpc) + .to_aml_bytes(&mut got); + + let mut expected = Vec::new(); + aml::Name::new("TEST".into(), &"Lpc").to_aml_bytes(&mut expected); + assert_eq!(expected, got); + got.clear(); + } + + #[test] + fn dsdt_valid_aml() { + let config = DsdtConfig { + generators: &[ + &MockDsdtGenerator { scope: DsdtScope::SystemBus }, + &MockDsdtGenerator { scope: DsdtScope::PciRoot }, + &MockDsdtGenerator { scope: DsdtScope::Lpc }, + ], + }; + let dsdt = Dsdt::new(config); + let mut aml = Vec::new(); + dsdt.to_aml_bytes(&mut aml); + + // Look for key elements. + assert!(aml.windows(4).any(|w| w == b"_SB_")); + assert!(aml.windows(4).any(|w| w == b"PCI0")); + assert!(aml.windows(4).any(|w| w == b"_PRT")); + assert!(aml.windows(4).any(|w| w == b"LPC_")); + } +} diff --git a/lib/propolis/src/firmware/acpi/facs.rs b/lib/propolis/src/firmware/acpi/facs.rs new file mode 100644 index 000000000..5bf804121 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/facs.rs @@ -0,0 +1,31 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a FACS ACPI table for an instance. +//! +//! The [`Facs`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use acpi_tables::{facs, Aml, AmlSink}; + +/// The FACS table stores information about the firmware. +/// +/// +pub struct Facs {} + +impl Facs { + pub fn new() -> Self { + Self {} + } +} + +// XXX(acpi): The acpi_tables crate generates version 1 of the FACS table while +// the original static EDK2 table was version 0. The only difference +// is the addition of the X_Firmware_Waking_Vector field, which is +// not used by Propolis. +impl Aml for Facs { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + facs::FACS::new().to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/firmware/acpi/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs new file mode 100644 index 000000000..9ad19c4d2 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -0,0 +1,153 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a FADT/FACP ACPI table for an instance. +//! +//! The [`Fadt`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use super::{OEM_ID, OEM_REVISION, OEM_TABLE_ID, SCI_IRQ}; +use acpi_tables::{ + // XXX(acpi): Use version 3 to keep FADT table consistent with the original + // EKD2 static tables. The acpi_tables crate also generates the + // MADT table using revision 1, which fwts reports not being + // compatible with FADT 6.5. + fadt_3::{FADTBuilder, Flags}, + gas::{AccessSize, AddressSpace, GAS}, + Aml, + AmlSink, +}; + +// Byte offset and length of fields that need to be referenced during table +// generation. +pub const FADT_FACS_OFFSET: usize = 36; +pub const FADT_FACS_LEN: usize = 4; + +pub const FADT_DSDT_OFFSET: usize = 40; +pub const FADT_DSDT_LEN: usize = 4; + +pub const FADT_X_DSDT_OFFSET: usize = 140; +pub const FADT_X_DSDT_LEN: usize = 8; + +// Values used to populate the FADT table. +const PM1A_EVT_BLK_ADDR: u32 = 0xb000; +const PM1A_CNT_BLK_ADDR: u32 = 0xb004; +const PM_TMR_BLK_ADDR: u32 = 0xb008; +const GPE0_BLK_ADDR: u32 = 0xafe0; + +const PM1A_EVT_BLK_LEN: u8 = 4; +const PM1A_CNT_BLK_LEN: u8 = 2; +const PM_TMR_BLK_LEN: u8 = 4; +const GPE0_BLK_LEN: u8 = 4; + +// Represent a bit flag for the FADT IA-PC boot architecture flags. +// +// +bitflags! { + pub struct FadtIaPcBootArchFlags: u16 { + const LEGACY_DEVICES = 1 << 0; + const ARCH_8042 = 1 << 1; + const VGA_NOT_PRESENT = 1 << 2; + const MSI_NOT_SUPPORTED = 1 << 3; + const PCIE_ASPM_CONTROLS = 1 << 4; + const CMOS_RTC_NOT_PRESENT = 1 << 5; + } +} + +/// The FADT table stores fixed hardware ACPI information. +/// +/// +pub struct Fadt { + facs_offset: u32, + dsdt_offset: u32, +} + +impl Fadt { + pub fn new(facs_offset: u32, dsdt_offset: u32) -> Self { + Self { facs_offset, dsdt_offset } + } +} + +// XXX(acpi): Values retained from the original EDK2 static tables. +// fwts reports 1 high failure for this table: +// - fadt: FADT X_GPE0_BLK Access width 0x00 but it should be 1 (byte access). +// +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Platform.h#L25-L56 +impl Aml for Fadt { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut fadt = FADTBuilder::new(*OEM_ID, *OEM_TABLE_ID, OEM_REVISION) + .firmware_ctrl_32(self.facs_offset) + .dsdt_32(self.dsdt_offset) + .dsdt_64(self.dsdt_offset as u64) + .flag(Flags::Wbinvd) + .flag(Flags::ProcC1) + .flag(Flags::SlpButton) + .flag(Flags::RtcS4) + .flag(Flags::TmrValExt) + .flag(Flags::ResetRegSup); + + fadt.sci_int = (SCI_IRQ as u16).into(); + fadt.smi_cmd = 0xb2.into(); + fadt.acpi_enable = 0xf1; + fadt.acpi_disable = 0xf0; + + fadt.pm1a_evt_blk = PM1A_EVT_BLK_ADDR.into(); + fadt.pm1a_cnt_blk = PM1A_CNT_BLK_ADDR.into(); + fadt.pm_tmr_blk = PM_TMR_BLK_ADDR.into(); + fadt.gpe0_blk = GPE0_BLK_ADDR.into(); + + fadt.pm1_evt_len = PM1A_EVT_BLK_LEN; + fadt.pm1_cnt_len = PM1A_CNT_BLK_LEN; + fadt.pm_tmr_len = PM_TMR_BLK_LEN; + fadt.gpe0_blk_len = GPE0_BLK_LEN; + + fadt.p_lvl2_lat = 101.into(); + fadt.p_lvl3_lat = 1001.into(); + + let iapc_boot_arch = FadtIaPcBootArchFlags::empty(); + fadt.iapc_boot_arch = iapc_boot_arch.bits().into(); + + fadt.reset_reg = GAS::new( + AddressSpace::SystemIo, + u8::BITS as u8, + 0, + AccessSize::Undefined, + 0x0cf9, + ); + fadt.reset_value = 0x06; + + fadt.x_pm1a_evt_blk = GAS::new( + AddressSpace::SystemIo, + PM1A_EVT_BLK_LEN * 8, + 0, + AccessSize::Undefined, + PM1A_EVT_BLK_ADDR as u64, + ); + fadt.x_pm1a_cnt_blk = GAS::new( + AddressSpace::SystemIo, + PM1A_CNT_BLK_LEN * 8, + 0, + AccessSize::Undefined, + PM1A_CNT_BLK_ADDR as u64, + ); + + fadt.x_pm_tmr_blk = GAS::new( + AddressSpace::SystemIo, + PM_TMR_BLK_LEN * 8, + 0, + AccessSize::Undefined, + PM_TMR_BLK_ADDR as u64, + ); + + fadt.x_gpe0_blk = GAS::new( + AddressSpace::SystemIo, + GPE0_BLK_LEN * 8, + 0, + AccessSize::Undefined, + GPE0_BLK_ADDR as u64, + ); + + fadt.finalize().to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/firmware/acpi/madt.rs b/lib/propolis/src/firmware/acpi/madt.rs new file mode 100644 index 000000000..6919bfd32 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/madt.rs @@ -0,0 +1,101 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a MADT/APIC ACPI table for an instance. +//! +//! The [`Madt`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use super::{ + IO_APIC_ADDR, LOCAL_APIC_ADDR, OEM_ID, OEM_REVISION, OEM_TABLE_ID, + PCI_LINK_IRQS, +}; +use acpi_tables::{madt, Aml, AmlSink}; + +const IO_APIC_ID: u8 = 0x02; +const IO_APIC_GSI_BASE: u32 = 0x0000; + +const TIMER_IRQ: u8 = 0; +const TIMER_GSI: u32 = 2; + +const LOCAL_APIC_INT_NUMBER: u8 = 1; + +pub struct MadtConfig { + pub num_cpus: u8, +} + +/// The MADT/APIC table describes the interrupts for the entire system. +/// +/// +pub struct Madt<'a> { + config: &'a MadtConfig, +} + +impl<'a> Madt<'a> { + pub fn new(config: &'a MadtConfig) -> Self { + Self { config } + } +} + +// XXX(acpi): Values retained from the original EDK2 static tables. +// fwts reports 3 medium failures for this table: +// - madt: LAPIC has no matching processor UID 0 +// - madt: LAPIC has no matching processor UID 1 +// - madt: LAPICNMI has no matching processor UID 255 +// +// +// +impl<'a> Aml for Madt<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut table = madt::MADT::new( + *OEM_ID, + *OEM_TABLE_ID, + OEM_REVISION, + madt::LocalInterruptController::Address(LOCAL_APIC_ADDR), + ) + .pc_at_compat(); + + // Processor Local APIC. + // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#i-o-apic-structure + for i in 0..self.config.num_cpus { + table.add_structure(madt::ProcessorLocalApic::new( + i, + i, + madt::EnabledStatus::Enabled, + )); + } + + // I/O APIC. + // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#i-o-apic-structure + table.add_structure(madt::IoApic::new( + IO_APIC_ID, + IO_APIC_ADDR, + IO_APIC_GSI_BASE, + )); + + // Interrupt Source Overrides. + // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#interrupt-source-override-structure + table.add_structure(madt::InterruptSourceOverride::new( + TIMER_IRQ, TIMER_GSI, + )); + + // Set level-triggered and active high for all PCI link targets. + PCI_LINK_IRQS.iter().for_each(|&i| { + table.add_structure( + madt::InterruptSourceOverride::new(i, i as u32) + .level_triggered() + .active_high(), + ); + }); + + // Local APIC NMI. + // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#local-apic-nmi-structure + table.add_structure(madt::ProcessorLocalApicNmi::new( + 0xff, // Apply to all processors. + LOCAL_APIC_INT_NUMBER, + )); + + table.to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs new file mode 100644 index 000000000..c48f0e180 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -0,0 +1,48 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! ACPI table and AML bytecode generation. + +pub mod dsdt; +pub mod facs; +pub mod fadt; +pub mod madt; +pub mod rsdp; +pub mod xsdt; + +pub use dsdt::{ + Dsdt, DsdtConfig, DsdtGenerator, DsdtScope, Ssdt, SSDT_FWDT_ADDR_LEN, + SSDT_FWDT_ADDR_OFFSET, +}; +pub use facs::Facs; +pub use fadt::{ + Fadt, FADT_DSDT_LEN, FADT_DSDT_OFFSET, FADT_FACS_LEN, FADT_FACS_OFFSET, + FADT_X_DSDT_LEN, FADT_X_DSDT_OFFSET, +}; +pub use madt::{Madt, MadtConfig}; +pub use rsdp::{ + Rsdp, RSDP_EXTENDED_CHECKSUM_OFFSET, RSDP_EXTENDED_TABLE_LEN, + RSDP_V1_CHECKSUM_OFFSET, RSDP_V1_TABLE_LEN, RSDP_XSDT_ADDR_LEN, + RSDP_XSDT_ADDR_OFFSET, +}; +pub use xsdt::{Xsdt, XSDT_HEADER_LEN}; + +// Values used to reference table checksums to recompute them after values are +// changed during table generation. +pub const TABLE_HEADER_CHECKSUM_OFFSET: usize = 9; +pub const TABLE_HEADER_CHECKSUM_LEN: usize = 1; + +// Internal values shared across tables. + +// XXX(acpi): Values inherited from the original EDK2 static tables. They could +// be set to Propolis-specific values in the future. +const OEM_ID: &[u8; 6] = b"OVMF "; +const OEM_TABLE_ID: &[u8; 8] = b"OVMFEDK2"; +const OEM_REVISION: u32 = 0x20130221; + +const SCI_IRQ: u8 = 0x09; +const PCI_LINK_IRQS: [u8; 4] = [0x05, SCI_IRQ, 0x0a, 0x0b]; + +const IO_APIC_ADDR: u32 = 0xfec0_0000; +const LOCAL_APIC_ADDR: u32 = 0xfee0_0000; diff --git a/lib/propolis/src/firmware/acpi/rsdp.rs b/lib/propolis/src/firmware/acpi/rsdp.rs new file mode 100644 index 000000000..8eb4d33af --- /dev/null +++ b/lib/propolis/src/firmware/acpi/rsdp.rs @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a RSDP ACPI table for an instance. +//! +//! The [`Rsdp`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use super::OEM_ID; +use acpi_tables::{rsdp, Aml, AmlSink}; + +// Byte offset and length of fields that need to be referenced during table +// generation. +pub const RSDP_XSDT_ADDR_OFFSET: usize = 24; +pub const RSDP_XSDT_ADDR_LEN: usize = 8; + +// The RSDP table has two checksums fields. +// +// - RSDP_V1_CHECKSUM_* points to the original checksum field defined in the +// ACPI 1.0 specification. +// +// - RSDP_EXTENDED_CHECKSUM_* points to the new checksum field that includes +// the entire table. +pub const RSDP_V1_CHECKSUM_OFFSET: usize = 8; +pub const RSDP_V1_TABLE_LEN: usize = 20; + +pub const RSDP_EXTENDED_CHECKSUM_OFFSET: usize = 32; +pub const RSDP_EXTENDED_TABLE_LEN: usize = 36; + +/// The RSDP table is the root table the operating system loads first to +/// discover the other tables. +/// +/// +pub struct Rsdp { + xsdt_addr: u64, +} + +impl Rsdp { + pub fn new(xsdt_addr: u64) -> Self { + Self { xsdt_addr } + } +} + +impl Aml for Rsdp { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + rsdp::Rsdp::new(*OEM_ID, self.xsdt_addr).to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/firmware/acpi/xsdt.rs b/lib/propolis/src/firmware/acpi/xsdt.rs new file mode 100644 index 000000000..b35b05a91 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/xsdt.rs @@ -0,0 +1,36 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates an XSDT ACPI table for an instance. +//! +//! The [`Xsdt`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use super::{OEM_ID, OEM_REVISION, OEM_TABLE_ID}; +use acpi_tables::{xsdt, Aml, AmlSink}; + +// Byte offset and length of fields that need to be referenced during table +// generation. +pub const XSDT_HEADER_LEN: usize = 36; + +/// The XSDT table provides the addresses of additional tables. +/// +/// +pub struct Xsdt { + entries: Vec, +} + +impl Xsdt { + pub fn new(entries: Vec) -> Self { + Self { entries } + } +} + +impl Aml for Xsdt { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut table = xsdt::XSDT::new(*OEM_ID, *OEM_TABLE_ID, OEM_REVISION); + self.entries.iter().for_each(|e| table.add_entry(*e)); + table.to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/firmware/mod.rs b/lib/propolis/src/firmware/mod.rs index 460e395ee..e1e598d38 100644 --- a/lib/propolis/src/firmware/mod.rs +++ b/lib/propolis/src/firmware/mod.rs @@ -2,4 +2,5 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +pub mod acpi; pub mod smbios; diff --git a/lib/propolis/src/hw/ps2/ctrl.rs b/lib/propolis/src/hw/ps2/ctrl.rs index a201a685d..5cbbf33d4 100644 --- a/lib/propolis/src/hw/ps2/ctrl.rs +++ b/lib/propolis/src/hw/ps2/ctrl.rs @@ -8,11 +8,13 @@ use std::mem::replace; use std::sync::{Arc, Mutex}; use crate::common::*; +use crate::firmware::acpi; use crate::hw::ibmpc; use crate::intr_pins::IntrPin; use crate::migrate::*; use crate::pio::{PioBus, PioFn}; +use acpi_tables::{aml, Aml, AmlSink}; use rfb::proto::KeyEvent; use super::keyboard::KeyEventRep; @@ -605,6 +607,9 @@ impl Lifecycle for PS2Ctrl { fn migrate(&self) -> Migrator<'_> { Migrator::Single(self) } + fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { + Some(self) + } } impl MigrateSingle for PS2Ctrl { fn export( @@ -1090,6 +1095,45 @@ impl Default for PS2Mouse { } } +impl acpi::DsdtGenerator for PS2Ctrl { + fn dsdt_scope(&self) -> acpi::DsdtScope { + acpi::DsdtScope::Lpc + } +} + +const PS2_KBD_IRQ: u8 = 1; + +impl Aml for PS2Ctrl { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "PS2K".into(), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0303")), + &aml::Name::new("_CID".into(), &aml::EISAName::new("PNP030B")), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new( + ibmpc::PORT_PS2_DATA, + ibmpc::PORT_PS2_DATA, + 0x00, + 0x01, + ), + &aml::IO::new( + ibmpc::PORT_PS2_CMD_STATUS, + ibmpc::PORT_PS2_CMD_STATUS, + 0x00, + 0x01, + ), + &aml::IrqNoFlags::new(PS2_KBD_IRQ), + ]), + ), + ], + ) + .to_aml_bytes(sink); + } +} + pub mod migrate { use crate::migrate::*; diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 5bad160e0..006c00523 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1056,7 +1056,9 @@ mod test { pub mod formats { use super::Entry; + use crate::firmware::acpi; use crate::hw::pci; + use acpi_tables::Aml; use zerocopy::{Immutable, IntoBytes}; /// A type for a range described in an E820 map entry. @@ -1302,4 +1304,489 @@ pub mod formats { assert_eq!(&expected[..], &entries[..]); } } + + /// Instance configuration that are relevant when building the ACPI tables. + pub struct AcpiConfig<'a> { + pub num_cpus: u8, + pub pci_window_32: PciWindow, + pub pci_window_64: PciWindow, + pub dsdt_generators: &'a [&'a dyn acpi::DsdtGenerator], + } + + /// A range of address to be used for PCI MMIO. + pub struct PciWindow { + pub base: u64, + pub end: u64, + } + impl PciWindow { + pub fn len(&self) -> u64 { + self.end - self.base + 1 + } + } + + /// The resulting values to be loaded into QEMU fw_cfg data. + pub struct AcpiTables { + pub tables: Entry, + pub rsdp: Entry, + pub table_loader: Entry, + } + + const FW_CFG_ACPI_TABLES_PATH: &str = "etc/acpi/tables"; + const FW_CFG_ACPI_RSDP_PATH: &str = "etc/acpi/rsdp"; + + /// Builds ACPI tables for an instance and provide three blobs of data that + /// can be loaded into the instance as QEMU firmware configuration: the + /// tables themselves, the RSDP table, and a [`TableLoader`] with commands + /// to run when the instance boots. + /// + /// The ACPI tables are organized in a hierarchy, with some tables having + /// fields that hold the address of another table. + /// + /// ```text + /// ┌─────────┐ ┌─────────┐ ┌─────────────┐ + /// │ RSDP │ ┌─▶│ XSDT │ ┌──▶│ FADT │ + /// ├─────────┤ │ ├─────────┤ │ ├─────────────┤ ┌──────────┐ + /// │ Pointer │ │ │ Entry │──┘ │ ........... │ ┌─▶│ FACS │ + /// ├─────────┤ │ ├─────────┤ │FIRMWARE_CTRL│─┘ └──────────┘ + /// │ Pointer │─┘ │ Entry │ │ ........... │ ┌──────────┐ + /// └─────────┘ ├─────────┤ │ DSDT │─┬─▶│ DSDT │ + /// │ ... │ │ ........... │ │ ├──────────┤ + /// ├─────────┤ │ X_DSDT │─┘ │Definition│ + /// │ Entry │───┐ │ ........... │ │ Blocks │ + /// ├─────────┤ │ └─────────────┘ └──────────┘ + /// │ Entry │─┐ │ ┌──────────┐ + /// └─────────┘ │ └─▶│ SSDT │ + /// │ ├──────────┤ + /// │ │Definition│ + /// │ │ Blocks │ + /// │ └──────────┘ + /// │ ┌────────────────────┐ + /// └───▶│ MADT │ + /// ├────────────────────┤ + /// │Interrupt Controller│ + /// │ Structures │ + /// └────────────────────┘ + /// ``` + /// Adapted from + /// + /// These addresses are only know at boot time, so each reference has a + /// corresponding [`AddPointerCommand`] that the firmware executes on boot. + /// And since the table has been modified, they also need a + /// [`AddChecksumCommand`] to recalculate the final table checksum. + pub struct AcpiTablesBuilder<'a> { + config: &'a AcpiConfig<'a>, + tables: Vec, + rsdp: Vec, + loader: TableLoader, + } + + impl<'a> AcpiTablesBuilder<'a> { + pub fn new(config: &'a AcpiConfig) -> Self { + let mut tables = Self { + config, + tables: Vec::new(), + rsdp: Vec::new(), + loader: TableLoader::new(), + }; + + tables.build(); + tables + } + + pub fn finish(self) -> AcpiTables { + AcpiTables { + tables: Entry::Bytes(self.tables), + rsdp: Entry::Bytes(self.rsdp), + table_loader: self.loader.finish(), + } + } + + fn build(&mut self) { + self.loader.add_allocate( + FW_CFG_ACPI_TABLES_PATH, + 64, + AllocZone::High, + ); + self.loader.add_allocate( + FW_CFG_ACPI_RSDP_PATH, + 16, + AllocZone::FSeg, + ); + + let facs_offset = self.add_facs(); + let dsdt_offset = self.add_dsdt(); + let fadt_offset = self.add_fadt(facs_offset, dsdt_offset); + + let madt_offset = self.add_madt(); + let ssdt_offset = self.add_ssdt(); + + let xsdt_entries = vec![fadt_offset, madt_offset, ssdt_offset]; + let xsdt_offset = self.add_xsdt(xsdt_entries); + + self.add_rsdp(xsdt_offset); + } + + fn add_facs(&mut self) -> usize { + let facs = acpi::Facs::new(); + let facs_offset = self.tables.len(); + facs.to_aml_bytes(&mut self.tables); + + facs_offset + } + + fn add_dsdt(&mut self) -> usize { + let dsdt_config = + acpi::DsdtConfig { generators: self.config.dsdt_generators }; + let dsdt = acpi::Dsdt::new(dsdt_config); + let dsdt_offset = self.tables.len(); + dsdt.to_aml_bytes(&mut self.tables); + + dsdt_offset + } + + fn add_ssdt(&mut self) -> usize { + // Add data for the FWDT OperationRegion declared in the SSDT + // table. + let fwdt_data_offset = self.tables.len(); + [ + self.config.pci_window_32.base, + self.config.pci_window_32.end, + self.config.pci_window_32.len(), + self.config.pci_window_64.base, + self.config.pci_window_64.end, + self.config.pci_window_64.len(), + ] + .iter() + .for_each(|data| { + self.tables.extend_from_slice(&data.to_le_bytes()); + }); + + let ssdt = acpi::Ssdt::new(fwdt_data_offset); + let ssdt_offset = self.tables.len(); + ssdt.to_aml_bytes(&mut self.tables); + + // Mark the FWDT Operatioon offset field as a pointer to the + // FWDT data. + self.loader.add_pointer( + FW_CFG_ACPI_TABLES_PATH, + FW_CFG_ACPI_TABLES_PATH, + (ssdt_offset + acpi::SSDT_FWDT_ADDR_OFFSET) as u32, + acpi::SSDT_FWDT_ADDR_LEN as u8, + ); + + // Recalculate checksum after changes. + self.reset_checksum(ssdt_offset); + + ssdt_offset + } + + fn add_fadt( + &mut self, + facs_offset: usize, + dsdt_offset: usize, + ) -> usize { + let fadt = acpi::Fadt::new(facs_offset as u32, dsdt_offset as u32); + let fadt_offset = self.tables.len(); + fadt.to_aml_bytes(&mut self.tables); + + // Mark the fields that reference other tables as pointers. + [ + (acpi::FADT_FACS_OFFSET, acpi::FADT_FACS_LEN), // FADT -> FACS + (acpi::FADT_DSDT_OFFSET, acpi::FADT_DSDT_LEN), // FADT -> DSDT + (acpi::FADT_X_DSDT_OFFSET, acpi::FADT_X_DSDT_LEN), // FADT -> X_DSDT + ] + .iter() + .for_each(|&(offset, size)| { + self.loader.add_pointer( + FW_CFG_ACPI_TABLES_PATH, + FW_CFG_ACPI_TABLES_PATH, + (fadt_offset + offset) as u32, + size as u8, + ); + }); + + // Recalculate checksum after changes. + self.reset_checksum(fadt_offset); + + fadt_offset + } + + fn add_madt(&mut self) -> usize { + let madt_config = + &acpi::MadtConfig { num_cpus: self.config.num_cpus }; + let madt = acpi::Madt::new(madt_config); + let madt_offset = self.tables.len(); + madt.to_aml_bytes(&mut self.tables); + + madt_offset + } + + fn add_xsdt(&mut self, entries: Vec) -> usize { + let xsdt = + acpi::Xsdt::new(entries.iter().map(|e| *e as u64).collect()); + let xsdt_offset = self.tables.len(); + xsdt.to_aml_bytes(&mut self.tables); + + // Mark the table entry fields as pointers. + for i in 0..entries.len() { + // Each entry offset in the overall tables data is: + // XSDT offset + XSDT header length + + // 8 * the number of entries before it. + let offset = xsdt_offset + acpi::XSDT_HEADER_LEN + 8 * i; + + self.loader.add_pointer( + FW_CFG_ACPI_TABLES_PATH, + FW_CFG_ACPI_TABLES_PATH, + offset as u32, + size_of::() as u8, + ); + } + + // Recalculate checksum after changes. + self.reset_checksum(xsdt_offset); + + xsdt_offset + } + + fn add_rsdp(&mut self, xsdt_offset: usize) { + let rsdp = acpi::Rsdp::new(xsdt_offset as u64); + rsdp.to_aml_bytes(&mut self.rsdp); + + // Mark the field with the XSDT address as pointer. + self.loader.add_pointer( + FW_CFG_ACPI_RSDP_PATH, + FW_CFG_ACPI_TABLES_PATH, + acpi::RSDP_XSDT_ADDR_OFFSET as u32, + acpi::RSDP_XSDT_ADDR_LEN as u8, + ); + + // Recalculate checksums after changes. + self.reset_rsdp_checksum( + acpi::RSDP_V1_CHECKSUM_OFFSET, + acpi::RSDP_V1_TABLE_LEN, + ); + self.reset_rsdp_checksum( + acpi::RSDP_EXTENDED_CHECKSUM_OFFSET, + acpi::RSDP_EXTENDED_TABLE_LEN, + ); + } + + /// Add a command to recompute a RSDP table checksum on boot. + /// + /// It is used when the table is modified during generation or it has + /// commands that will modify them on boot. + /// + /// Must be called after all modifications have been done. + fn reset_rsdp_checksum( + &mut self, + checksum_offset: usize, + table_len: usize, + ) { + let checksum_end = + checksum_offset + acpi::TABLE_HEADER_CHECKSUM_LEN; + + // Zero existing checksum so it doesn't affect the new value. + self.rsdp[checksum_offset..checksum_end] + .copy_from_slice(&0_u8.to_le_bytes()); + + self.loader.add_checksum( + FW_CFG_ACPI_RSDP_PATH, + checksum_offset as u32, + 0, + table_len as u32, + ); + } + + /// Add a command to recompute a table's checksum on boot. + /// + /// It is used when the table is modified during generation or it has + /// commands that will modify them on boot. + /// + /// Must be called after all modifications have been done. + fn reset_checksum(&mut self, table_offset: usize) { + let checksum_start = + table_offset + acpi::TABLE_HEADER_CHECKSUM_OFFSET; + let checksum_end = table_offset + + acpi::TABLE_HEADER_CHECKSUM_OFFSET + + acpi::TABLE_HEADER_CHECKSUM_LEN; + + // Zero existing checksum so it doesn't affect the new value. + self.tables[checksum_start..checksum_end] + .copy_from_slice(&0_u8.to_le_bytes()); + + self.loader.add_checksum( + FW_CFG_ACPI_TABLES_PATH, + checksum_start as u32, + table_offset as u32, + (self.tables.len() - table_offset) as u32, + ); + } + } + + pub const TABLE_LOADER_FILESZ: usize = 56; + pub const TABLE_LOADER_COMMAND_SIZE: usize = 128; + + /// Stores commands that will be executed by the EDK2 firmware when the + /// ACPI tables are loaded. + /// + /// Refer to the EDK2 source code for more information on the commands. + /// + /// + pub struct TableLoader { + commands: Vec, + } + + impl TableLoader { + pub fn new() -> Self { + Self { commands: Vec::new() } + } + + pub fn add_allocate( + &mut self, + file: &str, + align: u32, + zone: AllocZone, + ) { + assert!(align.is_power_of_two()); + + let cmd = AllocateCommand { + file: LoaderFileName::new(file), + align, + zone, + }; + self.write_command(CommandType::Allocate, cmd.as_bytes()); + } + + pub fn add_pointer( + &mut self, + dest_file: &str, + src_file: &str, + offset: u32, + size: u8, + ) { + assert!(matches!(size, 1 | 2 | 4 | 8)); + + let cmd = AddPointerCommand { + dest_file: LoaderFileName::new(dest_file), + src_file: LoaderFileName::new(src_file), + offset, + size, + }; + self.write_command(CommandType::AddPointer, cmd.as_bytes()); + } + + pub fn add_checksum( + &mut self, + file: &str, + result_offset: u32, + start: u32, + length: u32, + ) { + let cmd = AddChecksumCommand { + file: LoaderFileName::new(file), + result_offset, + start, + length, + }; + self.write_command(CommandType::AddChecksum, cmd.as_bytes()); + } + + pub fn finish(self) -> Entry { + Entry::Bytes(self.commands) + } + + fn write_command(&mut self, cmd_type: CommandType, payload: &[u8]) { + let start = self.commands.len(); + self.commands.resize(start + TABLE_LOADER_COMMAND_SIZE, 0); + + let cmd_bytes = (cmd_type as u32).to_le_bytes(); + self.commands[start..start + 4].copy_from_slice(&cmd_bytes); + + let payload_start = start + 4; + let payload_end = payload_start + payload.len(); + assert!(payload_end <= start + TABLE_LOADER_COMMAND_SIZE); + self.commands[payload_start..payload_end].copy_from_slice(payload); + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable)] + #[repr(u8)] + pub enum AllocZone { + High = 0x1, + FSeg = 0x2, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable)] + #[repr(u32)] + enum CommandType { + Allocate = 1, + AddPointer = 2, + AddChecksum = 3, + #[allow(dead_code)] + WritePointer = 4, + } + + #[derive(Clone, IntoBytes, Immutable)] + #[repr(C)] + struct LoaderFileName([u8; TABLE_LOADER_FILESZ]); + impl LoaderFileName { + fn new(name: &str) -> Self { + let bytes = name.as_bytes(); + assert!(bytes.len() < TABLE_LOADER_FILESZ); + + let mut buf = [0u8; TABLE_LOADER_FILESZ]; + buf[..bytes.len()].copy_from_slice(bytes); + Self(buf) + } + } + + #[derive(IntoBytes, Immutable)] + #[repr(C, packed)] + struct AllocateCommand { + file: LoaderFileName, + align: u32, + zone: AllocZone, + } + + #[derive(IntoBytes, Immutable)] + #[repr(C, packed)] + struct AddPointerCommand { + dest_file: LoaderFileName, + src_file: LoaderFileName, + offset: u32, + size: u8, + } + + #[derive(IntoBytes, Immutable)] + #[repr(C, packed)] + struct AddChecksumCommand { + file: LoaderFileName, + result_offset: u32, + start: u32, + length: u32, + } + + #[cfg(test)] + mod test_table_loader { + use super::*; + + #[test] + fn basic() { + let mut loader = TableLoader::new(); + loader.add_allocate("rsdp", 16, AllocZone::FSeg); + loader.add_allocate("tables", 64, AllocZone::High); + loader.add_pointer("rsdp", "tables", 16, 4); + loader.add_checksum("rsdp", 8, 0, 20); + + let Entry::Bytes(bytes) = loader.finish() else { + panic!("expected Bytes entry"); + }; + + assert_eq!(bytes.len(), TABLE_LOADER_COMMAND_SIZE * 4); + assert_eq!(bytes[0], CommandType::Allocate as u8); + assert_eq!(bytes[128], CommandType::Allocate as u8); + assert_eq!(bytes[256], CommandType::AddPointer as u8); + assert_eq!(bytes[384], CommandType::AddChecksum as u8); + } + } } diff --git a/lib/propolis/src/hw/uart/lpc.rs b/lib/propolis/src/hw/uart/lpc.rs index 5c1714e1c..517d3818f 100644 --- a/lib/propolis/src/hw/uart/lpc.rs +++ b/lib/propolis/src/hw/uart/lpc.rs @@ -7,10 +7,13 @@ use std::sync::{Arc, Mutex}; use super::uart16550::{migrate, Uart}; use crate::chardev::*; use crate::common::*; +use crate::firmware::acpi; use crate::intr_pins::IntrPin; use crate::migrate::*; use crate::pio::{PioBus, PioFn}; +use acpi_tables::{aml, Aml, AmlSink}; + // Low Pin Count UART pub const REGISTER_LEN: usize = 8; @@ -36,29 +39,43 @@ impl UartState { } pub struct LpcUart { + name: &'static str, + irq: u8, state: Mutex, + port: Mutex>, notify_readable: NotifierCell, notify_writable: NotifierCell, } impl LpcUart { - pub fn new(irq_pin: Box) -> Arc { + pub fn new( + name: &'static str, + irq: u8, + irq_pin: Box, + ) -> Arc { Arc::new(Self { + name, + irq, state: Mutex::new(UartState { uart: Uart::new(), irq_pin, auto_discard: true, paused: false, }), + port: Mutex::new(None), notify_readable: NotifierCell::new(), notify_writable: NotifierCell::new(), }) } + pub fn attach(self: &Arc, bus: &PioBus, port: u16) { let this = self.clone(); let piofn = Arc::new(move |_port: u16, rwo: RWOp| this.pio_rw(rwo)) as Arc; bus.register(port, REGISTER_LEN as u16, piofn).unwrap(); + + let mut current_port = self.port.lock().unwrap(); + *current_port = Some(port); } fn pio_rw(&self, rwo: RWOp) { assert!(rwo.offset() < REGISTER_LEN); @@ -172,6 +189,10 @@ impl Lifecycle for LpcUart { let mut state = self.state.lock().unwrap(); state.paused = false; } + + fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { + Some(self) + } } impl MigrateSingle for LpcUart { fn export( @@ -194,3 +215,46 @@ impl MigrateSingle for LpcUart { Ok(()) } } + +impl acpi::DsdtGenerator for LpcUart { + fn dsdt_scope(&self) -> acpi::DsdtScope { + acpi::DsdtScope::Lpc + } +} + +impl Aml for LpcUart { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let port = match *self.port.lock().unwrap() { + Some(p) => p, + None => return, // Device is not attached to any port. + }; + + #[allow(clippy::wildcard_in_or_patterns)] + let uid: u32 = match self.name { + "COM1" => 1, + "COM2" => 2, + "COM3" | "COM4" | _ => { + // XXX(acpi): COM3 and COM4 are also attached to the instance + // but the original EDK2 static tables didn't include them. + return; + } + }; + + aml::Device::new( + aml::Path::new(&format!("UAR{}", uid)), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0501")), + &aml::Name::new("_DDN".into(), &self.name), + &aml::Name::new("_UID".into(), &uid), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(port, port, 1, REGISTER_LEN as u8), + &aml::Irq::new(true, false, false, self.irq), + ]), + ), + ], + ) + .to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/lifecycle.rs b/lib/propolis/src/lifecycle.rs index 1d8e194c5..da8adeb30 100644 --- a/lib/propolis/src/lifecycle.rs +++ b/lib/propolis/src/lifecycle.rs @@ -6,6 +6,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use futures::future::{self, BoxFuture}; +use crate::firmware::acpi; use crate::migrate::Migrator; /// General trait for emulated devices in the system. @@ -96,6 +97,17 @@ pub trait Lifecycle: Send + Sync + 'static { fn migrate(&'_ self) -> Migrator<'_> { Migrator::Empty } + + /// Returns this device as a [`DsdtGenerator`] if it contributes to the + /// DSDT ACPI table. + /// + /// Devices that implement [`DsdtGenerator`] should override this method + /// to return `Some(self)` so they can be automatically discovered. + /// + /// [`DsdtGenerator`]: crate::firmware::acpi::DsdtGenerator + fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { + None + } } /// Indicator for tracking [Lifecycle] states.