Skip to content
Draft
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
4 changes: 2 additions & 2 deletions platform/dev-qemu/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 28 additions & 2 deletions platform/dev-qemu/README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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:

Expand All @@ -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. |
14 changes: 13 additions & 1 deletion platform/dev-qemu/qemu-ec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
10 changes: 8 additions & 2 deletions platform/dev-qemu/src/board.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -11,19 +12,22 @@ const I2C_ADDR: u8 = 0x2C;
bind_interrupts!(struct Irqs {
UART0 => uart::buffered::InterruptHandler<peripherals::UART0>;
I2C_TARGET => target::InterruptHandler<peripherals::I2C_TARGET>;
ESPI_TARGET => espi::InterruptHandler<peripherals::ESPI_TARGET>;
});

/// 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>,
/// I2C target acting as the HID-over-I2C device endpoint.
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 {
Expand All @@ -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 }
}
}
2 changes: 2 additions & 0 deletions platform/dev-qemu/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

mod board;
mod hid;
mod pcc;

use board::Board;
use defmt::info;
Expand Down Expand Up @@ -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
Expand Down
122 changes: 122 additions & 0 deletions platform/dev-qemu/src/pcc.rs
Original file line number Diff line number Diff line change
@@ -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::<ExtendedPccHeader>() == 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");
}
}
}
Loading