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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ jobs:
EC_TEST_CLI_REV: 20347787

# Tag of the prebuilt QEMU image pulled from the odp-qemu-builder GHCR package
ODP_QEMU_TAG: sha-7e461b3
ODP_QEMU_TAG: sha-f8af056

steps:
- uses: actions/checkout@v4
Expand All @@ -359,6 +359,8 @@ jobs:
sudo apt-get update && sudo apt-get install -y
libudev-dev
libfdt1
libjpeg-turbo8
libpng16-16t64

- name: Install ec-test-cli
run: cargo install --git https://github.com/OpenDevicePartnership/odp-platform-common --locked --rev ${{ env.EC_TEST_CLI_REV }} ec-test-cli
Expand Down
16 changes: 16 additions & 0 deletions platform/dev-imxrt/Cargo.lock

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

16 changes: 16 additions & 0 deletions platform/dev-mcxa/Cargo.lock

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

16 changes: 16 additions & 0 deletions platform/dev-mec/Cargo.lock

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

16 changes: 16 additions & 0 deletions platform/dev-npcx/Cargo.lock

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

16 changes: 16 additions & 0 deletions platform/dev-qemu/Cargo.lock

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

60 changes: 31 additions & 29 deletions platform/dev-qemu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ default and can instead use a stable socket for QEMU co-simulation.
- [Docker](https://docs.docker.com/get-docker/) — `qemu-ec.sh` pulls a prebuilt
`qemu-system-riscv32` (with `ec` machine support) from the
[`odp-qemu-builder`](https://github.com/OpenDevicePartnership/odp-qemu-builder)
GHCR image and caches it under `target/qemu-ec/`. To use a local QEMU instead,
set `QEMU=/path/to/qemu-system-riscv32`.
GHCR image and caches it under `target/qemu-ec/<tag>/`. To use a local QEMU
instead, set `QEMU=/path/to/qemu-system-riscv32`.
- QEMU's runtime libraries (Ubuntu: `sudo apt install libfdt1 libjpeg-turbo8 libpng16-16t64`).
- `defmt-print`: `cargo install defmt-print`

## Run
Expand Down Expand Up @@ -43,43 +44,44 @@ verifies actual standby return; the EC log alone does not prove host resume.

## Emulated runtime AC/DC input

`cargo build --release --features time-alarm-power-input` includes the wake
output and uses GPIO2 (high = AC, low = DC) instead of `ODP_WAKE_SOURCE`.
The input must be explicitly initialized by the QEMU model before startup;
the firmware rejects an unset validity bit rather than assuming DC. Source
selection happens before the TimeAlarm runner starts. Socket bytes alone do
not establish validity.

Use the GPIO2-enabled QEMU model with `input-reset-mask=4` and `input-reset=4`
for cold AC, or `input-reset=0` for cold DC, and its `ec-gpio2` control socket.
Raw bytes `01` and `00` select AC and DC at runtime. The dedicated input task
resamples after opposite-level waits on the shared GPIO interrupt. This is a
level interface, not an event counter: a transient that returns before the
task samples it is not a recorded power-source change.

The model retains the last valid source across control-socket disconnects
and EC warm resets, and diagnoses malformed bytes. GPIO1 remains the wake
output and GPIO0 remains the HID interrupt. The default build and the explicit
construction-time `time-alarm-wake` fixture are unchanged. This emulated
external input neither detects physical power nor reports Windows power state.
Build with `--features time-alarm-power-input` to take the power source from
GPIO2 (high = AC, low = DC) instead of `ODP_WAKE_SOURCE`. `cargo run` starts it
on DC (`EC_POWER_SOURCE=ac` for AC); switch it by writing a raw byte to the
control socket:

```sh
cargo run --release --features time-alarm-power-input
printf '\001' | nc -NU /tmp/qemu-ec-power.sock # AC
printf '\000' | nc -NU /tmp/qemu-ec-power.sock # DC
```

Sending AC sets TimeAlarm's source to AC and connects a mock PSU (a 20 V / 3 A
wall adapter) to the power policy, so battery 0's `_PSR` reports online about
0.8 s later. Sending DC reverses both.

QEMU's `gpio2 source control disconnected; retaining input level N` warning is
expected at startup and after each write; `N` is the current level.

## Sockets
While `dev-qemu` is running, the `ec` machine exposes two sockets that external
While `dev-qemu` is running, the `ec` machine exposes these 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`
- GPIO2 AC/DC control (power-input builds): `/tmp/qemu-ec-power.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`.

## Configuration
`qemu-ec.sh` reads the following environment variables:

| Variable | Default | Description |
|----------------|--------------------------|-------------------------------------------------|
| `QEMU` | (pulled from GHCR) | Override the `qemu-system-riscv32` binary. |
| `ODP_QEMU_TAG` | (pinned in `qemu-ec.sh`) | Tag of the odp-qemu-builder GHCR image to pull. |
| `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. |
| Variable | Default | Description |
|-------------------|---------------------------|-------------------------------------------------|
| `QEMU` | (pulled from GHCR) | Override the `qemu-system-riscv32` binary. |
| `ODP_QEMU_TAG` | (pinned in `qemu-ec.sh`) | Tag of the odp-qemu-builder GHCR image to pull. |
| `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_POWER_SOURCE` | `dc` (power-input builds) | Initial GPIO2 level: `ac` or `dc`. |
| `EC_POWER_SOCK` | `/tmp/qemu-ec-power.sock` | Path for the GPIO2 control socket. |
5 changes: 5 additions & 0 deletions platform/dev-qemu/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ fn main() {

println!("cargo:rustc-env=RISCV_RT_BASE_ISA=rv32i");
println!("cargo:rerun-if-env-changed=RISCV_RT_BASE_ISA");

// qemu-ec.sh looks for this symbol to initialize GPIO2, which this feature's firmware requires.
if env::var_os("CARGO_FEATURE_TIME_ALARM_POWER_INPUT").is_some() {
println!("cargo:rustc-link-arg-bins=--defsym=__odp_qemu_ec_power_input=1");
}
}
55 changes: 49 additions & 6 deletions platform/dev-qemu/qemu-ec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
# `scripts/integration-test.sh`, and CI. It:
#
# 1. Resolves a `qemu-system-riscv32` binary that supports the `ec` machine
# (GPIO + I2C target sockets). The binary is pulled once from the
# `odp-qemu-builder` image published on GHCR and cached under `target/`.
# (GPIO + I2C target sockets). The binary is pulled once per image tag from
# the `odp-qemu-builder` image published on GHCR and cached under
# `target/qemu-ec/<tag>/`.
# 2. Launches QEMU on the `ec` machine, exposing the EC's I2C-target and GPIO
# lines as UNIX-domain sockets that external programs can connect to. UART
# uses a PTY by default or a stable socket when EC_UART_SOCK is set.
# uses a PTY by default or a stable socket when EC_UART_SOCK is set. When
# GPIO2 is initialized as the AC/DC input (see EC_POWER_SOURCE), it also
# gets its own control socket.
# 3. Routes the defmt log stream: in the normal (interactive) path the
# semihosting output is piped straight into `defmt-print`; in the headless
# path (`DEFMT_LOG=off`) QEMU is run raw with no logging.
Expand All @@ -28,6 +31,13 @@
# 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_UART_SOCK Optional path for a UART socket (default: unset, use a PTY).
# EC_POWER_SOURCE
# Initial GPIO2 AC/DC input, `ac` or `dc`. Defaults to `dc` for
# firmware built with `time-alarm-power-input` (build.rs marks
# it); otherwise unset, leaving GPIO2 uninitialized.
# EC_POWER_SOCK Path for the GPIO2 control socket when GPIO2 is initialized
# (default: /tmp/qemu-ec-power.sock). Write the raw byte 0x01
# for AC or 0x00 for DC.

set -euo pipefail

Expand All @@ -41,17 +51,37 @@ fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

ODP_QEMU_TAG="${ODP_QEMU_TAG:-sha-7e461b3}"
ODP_QEMU_TAG="${ODP_QEMU_TAG:-sha-f8af056}"
EC_I2C_SOCK="${EC_I2C_SOCK:-/tmp/qemu-ec-i2c.sock}"
EC_GPIO_SOCK="${EC_GPIO_SOCK:-/tmp/qemu-ec-gpio.sock}"
EC_UART_SOCK="${EC_UART_SOCK:-}"
EC_POWER_SOURCE="${EC_POWER_SOURCE:-}"
EC_POWER_SOCK="${EC_POWER_SOCK:-/tmp/qemu-ec-power.sock}"

# Firmware built with `time-alarm-power-input` stops at startup unless GPIO2 is
# initialized. Its build.rs leaves this marker symbol, so start it on DC.
if [[ -z "$EC_POWER_SOURCE" && -n "$ELF" ]] && grep -qaF __odp_qemu_ec_power_input "$ELF"; then
EC_POWER_SOURCE=dc
fi

# GPIO2 (bit 2) initial level: high is AC, low is DC.
case "$EC_POWER_SOURCE" in
"") ;;
ac) EC_POWER_LEVEL=0x4 ;;
dc) EC_POWER_LEVEL=0x0 ;;
*)
echo "error: EC_POWER_SOURCE must be 'ac' or 'dc', got '$EC_POWER_SOURCE'" >&2
exit 1
;;
esac

# GHCR image that publishes the prebuilt QEMU (with `ec` machine support).
QEMU_IMAGE="ghcr.io/opendevicepartnership/odp-qemu-builder/qemu:${ODP_QEMU_TAG}"
# Location of the binary inside that image.
QEMU_IMAGE_BIN="/usr/local/bin/qemu-system-riscv32"
# Where we cache the extracted binary on the host.
QEMU_CACHE_DIR="${SCRIPT_DIR}/target/qemu-ec"
# Where we cache the extracted binary on the host. Caching per tag means bumping
# ODP_QEMU_TAG pulls the new binary instead of reusing an older one.
QEMU_CACHE_DIR="${SCRIPT_DIR}/target/qemu-ec/${ODP_QEMU_TAG}"
QEMU_CACHE_BIN="${QEMU_CACHE_DIR}/qemu-system-riscv32"

# Resolve the QEMU binary, pulling it from GHCR on first use.
Expand Down Expand Up @@ -112,6 +142,8 @@ fi
# - `-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
# sockets that external programs can connect to.
# - `-global odp-gpio.*` With EC_POWER_SOURCE, initialize GPIO2 (the AC/DC input)
# for its control socket, which QEMU otherwise rejects.
QEMU_ARGS=(
-machine ec
-bios none
Expand Down Expand Up @@ -139,6 +171,17 @@ QEMU_ARGS+=(
-kernel "$ELF"
)

if [[ -n "$EC_POWER_SOURCE" ]]; then
QEMU_ARGS+=(
-global odp-gpio.input-reset-mask=0x4
-global "odp-gpio.input-reset=${EC_POWER_LEVEL}"
-chardev "socket,id=ec-gpio2,path=${EC_POWER_SOCK},server=on,wait=off"
)
echo "GPIO2 power input starts on ${EC_POWER_SOURCE}; write 0x01 (AC) or 0x00 (DC) to ${EC_POWER_SOCK}" >&2
echo "Expected: QEMU warns \"gpio2 source control disconnected; retaining input level N\" at startup" \
"and after each client disconnects (N: 1 = AC, 0 = DC)." >&2
fi

# Headless path: no defmt logging, so semihosting is left disabled and QEMU runs
# raw. The "char device redirected to /dev/pts/N" line goes to stdout where
# callers (integration-test.sh) grep it.
Expand Down
11 changes: 7 additions & 4 deletions platform/dev-qemu/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ async fn init_time_alarm(
Some("dc") => AcpiTimerId::DcPower,
_ => panic!("Build the wake fixture with ODP_WAKE_SOURCE=ac or dc"),
};
let (relay, service) = platform_common::mock::init_with_time_alarm_configure(spawner, |service| {
#[cfg_attr(not(feature = "time-alarm-power-input"), expect(unused_variables))]
let (relay, service, psu) = platform_common::mock::init_with_time_alarm_configure(spawner, |service| {
#[cfg(feature = "time-alarm-power-input")]
let source = {
assert!(
Expand All @@ -65,8 +66,9 @@ async fn init_time_alarm(
.await;
spawner.spawn(time_alarm_wake(service, wake_gpio).expect("Failed to spawn TimeAlarm wake task"));
#[cfg(feature = "time-alarm-power-input")]
spawner
.spawn(time_alarm_power_input(service, power_input).expect("Failed to spawn TimeAlarm power input task"));
spawner.spawn(
time_alarm_power_input(service, psu, power_input).expect("Failed to spawn TimeAlarm power input task"),
);
(relay, service)
};

Expand All @@ -85,9 +87,10 @@ async fn time_alarm_wake(service: mock_time_alarm::TimeAlarmService, gpio: embas
#[embassy_executor::task]
async fn time_alarm_power_input(
service: mock_time_alarm::TimeAlarmService,
psu: platform_common::mock::power_policy::MockPsuControl,
input: embassy_qemu_riscv::gpio::Input<'static, embassy_qemu_riscv::gpio::Async>,
) {
mock_time_alarm::monitor_power_input(service, input)
mock_time_alarm::monitor_power_input(service, psu, input)
.await
.expect("TimeAlarm power input failed");
}
Loading
Loading