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
35 changes: 18 additions & 17 deletions platform/dev-qemu/Cargo.lock

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

8 changes: 7 additions & 1 deletion platform/dev-qemu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ style = "deny"
[package.metadata.cargo-machete]
ignored = ["riscv-rt"]

[features]
time-alarm-wake = ["dep:time-alarm-service-interface"]
time-alarm-power-input = ["time-alarm-wake"]

[dependencies]
riscv-rt = "0.17.0"

embassy-qemu-riscv = { git = "https://github.com/kurtjd/qemu-riscv-rs", features = [
# Restore upstream after the zero-gap I2C transaction-boundary fix and input-validity support are merged.
embassy-qemu-riscv = { git = "https://github.com/dymk/qemu-riscv-rs", rev = "a1ff86d62c683928464bf310de7d8ad965d5bd9b", features = [
"defmt",
] }
embassy-executor = { version = "0.10.0", features = [
Expand All @@ -68,6 +73,7 @@ time-alarm-service-relay = { git = "https://github.com/OpenDevicePartnership/emb
] }
odp-service-common = { git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "main" }
platform-common = { path = "../platform-common", features = ["mock"] }
time-alarm-service-interface = { git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "main", optional = true }

static_cell = "2.1.0"
defmt = "0.3.6"
Expand Down
38 changes: 38 additions & 0 deletions platform/dev-qemu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,44 @@ E.g. to connect with [ec-test-app](https://github.com/OpenDevicePartnership/odp-
To run without logging (skips `defmt-print`):
`cargo run-headless`

## CPU retention-wake fixture

Build with `ODP_WAKE_SOURCE=ac cargo build --locked --release --features time-alarm-wake`
or select `dc` explicitly. The feature requires one of those build-time
values; a missing or invalid selection fails at startup rather than assuming
a power source. Both alarms are disabled and the source is selected before
the TimeAlarm runner starts. Ordinary builds retain their initialization policy.

This test-only source selection is not hardware AC/DC detection. GPIO1 is an
active-high wake-request level driven by the real TimeAlarm service; GPIO0
remains the HID interrupt. HID-over-I2C and UART/MCTP continue to share that
same TimeAlarm service. Clear or disable the corresponding timers through
the existing relay commands to release their wake latches. The CPU retention
test harness connects the separate `ec-gpio1` channel to the host GPIO1 and
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.

## Sockets
While `dev-qemu` is running, the `ec` machine exposes two sockets that external
programs (such as another QEMU instance) can connect to:
Expand Down
19 changes: 18 additions & 1 deletion platform/dev-qemu/src/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ bind_interrupts!(struct Irqs {
I2C_TARGET => target::InterruptHandler<peripherals::I2C_TARGET>;
});

#[cfg(feature = "time-alarm-power-input")]
bind_interrupts!(struct GpioIrqs {
GPIO => embassy_qemu_riscv::gpio::InterruptHandler;
});

/// Board IO for the dev-qemu platform.
///
/// This minimal development board provides a UART interface for ODP service
Expand All @@ -24,6 +29,10 @@ pub struct Board {
pub i2c: I2c<'static, I2cAsync>,
/// Interrupt line the HID device drives to signal the host (active low).
pub gpio: Output<'static>,
#[cfg(feature = "time-alarm-wake")]
pub wake_gpio: Output<'static>,
#[cfg(feature = "time-alarm-power-input")]
pub power_input: embassy_qemu_riscv::gpio::Input<'static, embassy_qemu_riscv::gpio::Async>,
}

impl BoardIo for Board {
Expand All @@ -41,6 +50,14 @@ 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 }
Board {
uart,
i2c,
gpio,
#[cfg(feature = "time-alarm-wake")]
wake_gpio: Output::new(p.GPIO1, Level::Low),
#[cfg(feature = "time-alarm-power-input")]
power_input: embassy_qemu_riscv::gpio::Input::new_async(p.GPIO2, GpioIrqs),
}
}
}
84 changes: 84 additions & 0 deletions platform/dev-qemu/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,101 @@ mod mctp;
use board::Board;
use defmt_semihosting as _;
use embassy_executor::Spawner;
#[cfg(feature = "time-alarm-wake")]
use embedded_services::info;
use platform_common::board::BoardIo;
use semihosting as _; // Panic handler

#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_qemu_riscv::init();
let board = Board::init(p);
#[cfg(not(feature = "time-alarm-wake"))]
let (relay, time_alarm) = platform_common::mock::init_with_time_alarm(spawner).await;
#[cfg(feature = "time-alarm-wake")]
let (relay, time_alarm) = {
use time_alarm_service_interface::{AcpiTimerId, AlarmTimerSeconds, TimeAlarmService};
#[cfg(not(feature = "time-alarm-power-input"))]
let source = match option_env!("ODP_WAKE_SOURCE") {
Some("ac") => AcpiTimerId::AcPower,
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(feature = "time-alarm-power-input")]
let source = {
assert!(
board.power_input.is_initialized(),
"TimeAlarm GPIO2 power input must be explicitly initialized"
);
power_source(&board.power_input)
};
info!("TimeAlarm wake source: {:?}", source);
for timer in [AcpiTimerId::AcPower, AcpiTimerId::DcPower] {
service
.set_timer_value(timer, AlarmTimerSeconds::DISABLED)
.expect("Failed to disable initial fixture alarm");
}
service.set_power_source(source);
})
.await;
spawner.spawn(time_alarm_wake(service, board.wake_gpio).expect("Failed to spawn TimeAlarm wake task"));
#[cfg(feature = "time-alarm-power-input")]
spawner.spawn(
time_alarm_power_input(service, board.power_input).expect("Failed to spawn TimeAlarm power input task"),
);
(relay, service)
};

// This allows thermal and battery to continue receiving requests over uart/mctp, but also allows
// time-alarm to simultaneously receive requests over uart/mctp or hidi2c
spawner.spawn(mctp::uart_service(board.uart, relay).expect("Failed to spawn UART service task"));
hid::init(spawner, board.i2c, board.gpio, time_alarm).await;
}

#[cfg(feature = "time-alarm-wake")]
#[embassy_executor::task]
async fn time_alarm_wake(
service: platform_common::mock::time_alarm::TimeAlarmService,
mut gpio: embassy_qemu_riscv::gpio::Output<'static>,
) {
loop {
let requested = service.wait_for_wake_signal().await;
if requested {
gpio.set_high();
} else {
gpio.set_low();
}
info!("TimeAlarm wake requested: {}", requested);
}
}

#[cfg(feature = "time-alarm-power-input")]
fn power_source(
input: &embassy_qemu_riscv::gpio::Input<'_, embassy_qemu_riscv::gpio::Async>,
) -> time_alarm_service_interface::AcpiTimerId {
use time_alarm_service_interface::AcpiTimerId;
if input.is_high() {
AcpiTimerId::AcPower
} else {
AcpiTimerId::DcPower
}
}

#[cfg(feature = "time-alarm-power-input")]
#[embassy_executor::task]
async fn time_alarm_power_input(
service: platform_common::mock::time_alarm::TimeAlarmService,
mut input: embassy_qemu_riscv::gpio::Input<'static, embassy_qemu_riscv::gpio::Async>,
) {
use time_alarm_service_interface::AcpiTimerId;
loop {
let source = power_source(&input);
service.set_power_source(source);
info!("TimeAlarm power input: {:?}", source);
match source {
AcpiTimerId::AcPower => input.wait_for_low().await,
AcpiTimerId::DcPower => input.wait_for_high().await,
}
}
}
10 changes: 9 additions & 1 deletion platform/platform-common/src/mock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,20 @@ pub async fn init(spawner: embassy_executor::Spawner) -> MockOdpRelayHandler {
pub async fn init_with_time_alarm(
spawner: embassy_executor::Spawner,
) -> (MockOdpRelayHandler, time_alarm_service::Service<'static>) {
init_with_time_alarm_configure(spawner, |_| {}).await
}

/// Configure TimeAlarm before its runner starts and retain its board control handle.
pub async fn init_with_time_alarm_configure(
spawner: embassy_executor::Spawner,
configure: impl FnOnce(time_alarm::TimeAlarmService),
) -> (MockOdpRelayHandler, time_alarm::TimeAlarmService) {
embedded_services::info!("Initializing mock services...");
embedded_services::init().await;

let thermal = thermal::init(spawner).await;
let battery = battery::init(spawner).await;
let tas = time_alarm::init(spawner).await;
let tas = time_alarm::init_with(spawner, configure).await;

let relay = MockOdpRelayHandler::new(
battery_service_relay::BatteryServiceRelayHandler::new(battery),
Expand Down
Loading
Loading