From c7f40c9c1384d5f607d6f8b08e23cb3d6cc478d0 Mon Sep 17 00:00:00 2001 From: Kurtis Dinelle Date: Tue, 29 Sep 2026 12:16:52 -0700 Subject: [PATCH] dev-qemu: Add task for testing PCC/eSPI --- platform/dev-qemu/Cargo.lock | 4 +- platform/dev-qemu/README.md | 30 +++++++- platform/dev-qemu/qemu-ec.sh | 14 +++- platform/dev-qemu/src/board.rs | 10 ++- platform/dev-qemu/src/main.rs | 2 + platform/dev-qemu/src/pcc.rs | 122 +++++++++++++++++++++++++++++++++ 6 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 platform/dev-qemu/src/pcc.rs diff --git a/platform/dev-qemu/Cargo.lock b/platform/dev-qemu/Cargo.lock index ad3a333..9f205da 100644 --- a/platform/dev-qemu/Cargo.lock +++ b/platform/dev-qemu/Cargo.lock @@ -388,7 +388,7 @@ dependencies = [ [[package]] name = "embassy-qemu-riscv" version = "0.2.1" -source = "git+https://github.com/kurtjd/qemu-riscv-rs#60430f9284ecc00ca79fb3b3d5e23b851b7e0336" +source = "git+https://github.com/kurtjd/qemu-riscv-rs#f010ade58d56ea0cee81920969410bd29bbd7a0f" dependencies = [ "critical-section", "defmt 1.1.0", @@ -982,7 +982,7 @@ dependencies = [ [[package]] name = "qemu-riscv-pac" version = "0.1.0" -source = "git+https://github.com/kurtjd/qemu-riscv-rs#60430f9284ecc00ca79fb3b3d5e23b851b7e0336" +source = "git+https://github.com/kurtjd/qemu-riscv-rs#f010ade58d56ea0cee81920969410bd29bbd7a0f" dependencies = [ "critical-section", "riscv", diff --git a/platform/dev-qemu/README.md b/platform/dev-qemu/README.md index 05ced1d..1d36a93 100644 --- a/platform/dev-qemu/README.md +++ b/platform/dev-qemu/README.md @@ -1,7 +1,7 @@ # dev-qemu A platform targeting QEMU RISCV using mock embedded-services. -It runs on the custom ODP `ec` machine, which exposes the EC's I2C-target and GPIO lines as +It runs on the custom ODP `ec` machine, which exposes the EC's I2C-target, GPIO, and eSPI interfaces as sockets that external programs (such as another QEMU instance) can connect to. UART uses a PTY by default and can instead use a stable socket for QEMU co-simulation. @@ -26,15 +26,40 @@ To run without logging (skips `defmt-print`): `cargo run-headless` ## Sockets -While `dev-qemu` is running, the `ec` machine exposes two sockets that external +While `dev-qemu` is running, the `ec` machine exposes three sockets that external programs (such as another QEMU instance) can connect to: - I2C target: `/tmp/qemu-ec-i2c.sock` - GPIO: `/tmp/qemu-ec-gpio.sock` +- eSPI target: `/tmp/qemu-ec-espi.sock` Set `EC_UART_SOCK` to replace the UART PTY with another socket, for example `EC_UART_SOCK=/tmp/qemu-ec-uart.sock cargo run --release`. +## PCC PING/PONG + +The firmware responds to command `1` on eSPI mailbox 0 (PCC Type 3 subspace 0). +Start the ARM host and EC QEMU instances with the same `EC_ESPI_SOCK`, using +QEMU builds that support the mailbox-only eSPI interface and publish PCCT. +In the Windows ARM64 guest, use `ec-test-cli` from `odp-platform-common`: + +```text +ec-test-cli --source acpi pcc probe +ec-test-cli --source acpi pcc ping --sequence 42 +``` + +This requires a Windows ARM64 image with the native-PCC-enabled `ectest` driver. +Probe queries interface metadata only; ping must print +`PCC response: PONG sequence=42` and exit successfully. + +The fixed eight-byte payload is ASCII `PING` followed by a little-endian `u32` +sequence; the reply is `PONG` followed by the same sequence. It starts at mailbox +offset 16, after the extended PCC header. Request PCC Length is not used because +the inspected Windows provider leaves it unset; response Length is 12 bytes +(command plus payload). Unsupported commands or markers complete with an error. +Mailbox 0 starts with command-complete set so Windows can acquire the idle +channel before sending its first command. No Type 4 notification is implemented. + ## Configuration `qemu-ec.sh` reads the following environment variables: @@ -45,3 +70,4 @@ Set `EC_UART_SOCK` to replace the UART PTY with another socket, for example | `EC_I2C_SOCK` | `/tmp/qemu-ec-i2c.sock` | Path for the I2C-target socket. | | `EC_GPIO_SOCK` | `/tmp/qemu-ec-gpio.sock` | Path for the GPIO socket. | | `EC_UART_SOCK` | (unset) | UART socket path; when unset, use a PTY. | +| `EC_ESPI_SOCK` | `/tmp/qemu-ec-espi.sock` | eSPI target socket path; set empty to disable. | diff --git a/platform/dev-qemu/qemu-ec.sh b/platform/dev-qemu/qemu-ec.sh index 900e760..92e716f 100755 --- a/platform/dev-qemu/qemu-ec.sh +++ b/platform/dev-qemu/qemu-ec.sh @@ -27,6 +27,8 @@ # ODP_QEMU_TAG Tag of the GHCR image to pull. # EC_I2C_SOCK Path for the I2C-target socket (default: /tmp/qemu-ec-i2c.sock). # EC_GPIO_SOCK Path for the GPIO socket (default: /tmp/qemu-ec-gpio.sock). +# EC_ESPI_SOCK Path for the eSPI-target socket (default: /tmp/qemu-ec-espi.sock; +# set empty to disable). # EC_UART_SOCK Optional path for a UART socket (default: unset, use a PTY). set -euo pipefail @@ -44,6 +46,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ODP_QEMU_TAG="${ODP_QEMU_TAG:-sha-7e461b3}" EC_I2C_SOCK="${EC_I2C_SOCK:-/tmp/qemu-ec-i2c.sock}" EC_GPIO_SOCK="${EC_GPIO_SOCK:-/tmp/qemu-ec-gpio.sock}" +EC_ESPI_SOCK="${EC_ESPI_SOCK-/tmp/qemu-ec-espi.sock}" EC_UART_SOCK="${EC_UART_SOCK:-}" # GHCR image that publishes the prebuilt QEMU (with `ec` machine support). @@ -110,7 +113,7 @@ fi # - `-machine ec` EC board exposing the I2C-target and GPIO sockets. # - `-bios none` dev-qemu is a bare-metal kernel; no firmware needed. # - `-serial pty` By default, UART0 uses a PTY for terminal/ec-test-cli. -# - `-chardev socket,...` The I2C-target, GPIO, and optional UART lines as UNIX +# - `-chardev socket,...` The I2C-target, GPIO, eSPI, and optional UART lines as UNIX # sockets that external programs can connect to. QEMU_ARGS=( -machine ec @@ -133,6 +136,15 @@ else QEMU_ARGS+=(-serial pty) fi +if [[ -n "$EC_ESPI_SOCK" ]]; then + if [[ -L "$EC_ESPI_SOCK" || ( -e "$EC_ESPI_SOCK" && ! -S "$EC_ESPI_SOCK" ) ]]; then + echo "error: EC_ESPI_SOCK must not name a symlink or non-socket: $EC_ESPI_SOCK" >&2 + exit 1 + fi + rm -f -- "$EC_ESPI_SOCK" + QEMU_ARGS+=(-chardev "socket,id=ec-espi-target,path=${EC_ESPI_SOCK},server=on,wait=off") +fi + QEMU_ARGS+=( -chardev "socket,id=ec-i2c-target,path=${EC_I2C_SOCK},server=on,wait=off" -chardev "socket,id=ec-gpio0,path=${EC_GPIO_SOCK},server=on,wait=off" diff --git a/platform/dev-qemu/src/board.rs b/platform/dev-qemu/src/board.rs index a9a087f..d2e9daf 100644 --- a/platform/dev-qemu/src/board.rs +++ b/platform/dev-qemu/src/board.rs @@ -1,3 +1,4 @@ +use embassy_qemu_riscv::espi::{self, Espi}; use embassy_qemu_riscv::gpio::{Level, Output}; use embassy_qemu_riscv::i2c::target::{self, Async as I2cAsync, I2c}; use embassy_qemu_riscv::uart::{buffered, Async}; @@ -11,12 +12,13 @@ const I2C_ADDR: u8 = 0x2C; bind_interrupts!(struct Irqs { UART0 => uart::buffered::InterruptHandler; I2C_TARGET => target::InterruptHandler; + ESPI_TARGET => espi::InterruptHandler; }); /// Board IO for the dev-qemu platform. /// /// This minimal development board provides a UART interface for ODP service -/// communication plus an I2C target and GPIO line (for HIDI2C service). +/// communication, an eSPI target for PCC, and I2C/GPIO for HIDI2C. pub struct Board { /// UART for ODP service communication. pub uart: buffered::Uart<'static, Async>, @@ -24,6 +26,8 @@ pub struct Board { pub i2c: I2c<'static, I2cAsync>, /// Interrupt line the HID device drives to signal the host (active low). pub gpio: Output<'static>, + /// eSPI target for the PCC mailbox. + pub espi: Espi<'static, espi::Async>, } impl BoardIo for Board { @@ -41,6 +45,8 @@ impl BoardIo for Board { // Start high (since this is an active-low signal) let gpio = Output::new(p.GPIO0, Level::High); - Board { uart, i2c, gpio } + let espi = Espi::new_async(p.ESPI_TARGET, Irqs, espi::Config::default()).expect("Failed to initialize eSPI"); + + Board { uart, i2c, gpio, espi } } } diff --git a/platform/dev-qemu/src/main.rs b/platform/dev-qemu/src/main.rs index c48e504..16ef884 100644 --- a/platform/dev-qemu/src/main.rs +++ b/platform/dev-qemu/src/main.rs @@ -3,6 +3,7 @@ mod board; mod hid; +mod pcc; use board::Board; use defmt::info; @@ -32,6 +33,7 @@ async fn main(spawner: Spawner) { let relay = platform_common::mock::init(spawner).await; spawner.spawn(uart_service(board.uart, relay).expect("Failed to spawn UART service task")); + spawner.spawn(pcc::task(board.espi).expect("Failed to spawn PCC ping task")); // Bring up a minimal HID-over-I2C device so a host (e.g. Windows) can // complete its initial HID handshake against the EC diff --git a/platform/dev-qemu/src/pcc.rs b/platform/dev-qemu/src/pcc.rs new file mode 100644 index 0000000..e135704 --- /dev/null +++ b/platform/dev-qemu/src/pcc.rs @@ -0,0 +1,122 @@ +use defmt::{info, warn}; +use embassy_qemu_riscv::espi::{Async, Espi, Mailbox}; + +const MAILBOX: Mailbox = Mailbox::Mailbox0; +const LENGTH_OFFSET: usize = 8; +const HEADER_SIZE: usize = 16; +const PAYLOAD_OFFSET: usize = HEADER_SIZE; +const PING_COMMAND: u32 = 1; +const NOTIFY_ON_COMPLETION: u32 = 1; +const COMPLETE: u32 = 1; +const ERROR: u32 = 2; + +#[repr(C)] +#[derive(defmt::Format)] +struct ExtendedPccHeader { + signature: u32, + flags: u32, + length: u32, + command: u32, +} + +const _: () = assert!(core::mem::size_of::() == HEADER_SIZE); + +impl ExtendedPccHeader { + fn from_bytes(bytes: [u8; HEADER_SIZE]) -> Self { + Self { + signature: u32::from_le_bytes(bytes[0..4].try_into().unwrap()), + flags: u32::from_le_bytes(bytes[4..8].try_into().unwrap()), + length: u32::from_le_bytes(bytes[8..12].try_into().unwrap()), + command: u32::from_le_bytes(bytes[12..16].try_into().unwrap()), + } + } +} + +// This task demonstrates the basic response flow of the EC on the type 3 subspace. +// +// See: https://uefi.org/specs/ACPI/6.5/14_Platform_Communications_Channel.html#doorbell-protocol +// for a diagram and explanation of the flow. +// +// Currently, type 4 subspace (async notifications) is not demonstrated here. +#[embassy_executor::task] +pub async fn task(mut espi: Espi<'static, Async>) { + // Ensure initially the CC bit is set (the host reads this to know it can send a command) + // Corresponds to step 1 in diagram + espi.set_shared_status(MAILBOX, COMPLETE); + info!("PCC ping responder ready"); + + loop { + // Wait for doorbell from host + let events = espi.wait_for_events().await; + if events.doorbells[0] { + // Ensure the host has cleared the CC bit + // Corresponds to optional step 5 in the diagram + if espi.mailbox_status(MAILBOX).shared_status & COMPLETE != 0 { + warn!("PCC doorbell ignored: command complete is still set"); + espi.acknowledge_doorbell(MAILBOX); + } else { + let mut header_bytes = [0u8; HEADER_SIZE]; + // Extract the header from the shared memory region which tells us what the command is + // (as well as flags that tell us if the host wants to be interrupted) + // + // See: https://uefi.org/specs/ACPI/6.5/14_Platform_Communications_Channel.html#extended-pcc-subspace-shared-memory-region + // + // Note: Length will be reported as 0 because the Windows interface I go through + // on the host side to talk to the PCC engine does not write length. + let header = if espi.read_mailbox(MAILBOX, 0, &mut header_bytes).is_ok() { + let header = ExtendedPccHeader::from_bytes(header_bytes); + info!("PCC request header: {}", header); + Some(header) + } else { + None + }; + let notify_on_completion = header + .as_ref() + .is_some_and(|header| header.flags & NOTIFY_ON_COMPLETION != 0); + + // Now extract the payload for the command and process it + // Corresponds to step 6 in the diagram + let mut payload = [0u8; 8]; + let valid = header.is_some_and(|header| header.command == PING_COMMAND) + && espi.read_mailbox(MAILBOX, PAYLOAD_OFFSET, &mut payload).is_ok() + && &payload[..4] == b"PING"; + + if valid { + payload[..4].copy_from_slice(b"PONG"); + info!( + "PCC PING sequence={} PONG", + u32::from_le_bytes(payload[4..8].try_into().unwrap()) + ); + } else { + payload = [0; 8]; + warn!("Invalid PCC ping request"); + } + + // Here we write the response back into the same type 3 shared memory region + // Also corresponds to step 6 in the diagram + espi.write_mailbox(MAILBOX, PAYLOAD_OFFSET, &payload) + .expect("PCC response payload is in bounds"); + let length = if valid { 4 + payload.len() as u32 } else { 4 }; + espi.write_mailbox(MAILBOX, LENGTH_OFFSET, &length.to_le_bytes()) + .expect("PCC response length is in bounds"); + espi.acknowledge_doorbell(MAILBOX); + + // If the command was valid, set the CC bit which the host will check + // Corresponds to step 7 in the diagram + espi.set_shared_status(MAILBOX, COMPLETE | if valid { 0 } else { ERROR }); + + // Finally if the host requested to be interrupted, then notify the host (via vwire under the hood) + // Corresponds to step 8 + if notify_on_completion { + espi.raise_host_irq(MAILBOX); + } + } + } + + // As mentioned, we aren't really doing anything for mailbox1 (type 4 subspace) yet + if events.doorbells[1] { + espi.acknowledge_doorbell(Mailbox::Mailbox1); + warn!("Unexpected PCC mailbox 1 doorbell"); + } + } +}