diff --git a/platform/dev-qemu/Cargo.lock b/platform/dev-qemu/Cargo.lock index 03e2b6c..1827c29 100644 --- a/platform/dev-qemu/Cargo.lock +++ b/platform/dev-qemu/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ [[package]] name = "battery-service" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "battery-service-interface", "defmt 0.3.100", @@ -42,7 +42,7 @@ dependencies = [ [[package]] name = "battery-service-interface" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "defmt 0.3.100", "embedded-batteries-async", @@ -51,7 +51,7 @@ dependencies = [ [[package]] name = "battery-service-relay" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "battery-service-interface", "defmt 0.3.100", @@ -292,6 +292,7 @@ dependencies = [ "semihosting", "static_cell", "time-alarm-service", + "time-alarm-service-interface", "time-alarm-service-relay", "uart-service", ] @@ -389,7 +390,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/dymk/qemu-riscv-rs?rev=a1ff86d62c683928464bf310de7d8ad965d5bd9b#a1ff86d62c683928464bf310de7d8ad965d5bd9b" dependencies = [ "critical-section", "defmt 1.1.0", @@ -630,7 +631,7 @@ dependencies = [ [[package]] name = "embedded-services" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "bitfield 0.17.0", "cortex-m", @@ -748,7 +749,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hidi2c-target-service" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "defmt 0.3.100", "embassy-futures", @@ -819,7 +820,7 @@ dependencies = [ [[package]] name = "mctp-rs" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities)", "crc", @@ -894,7 +895,7 @@ dependencies = [ [[package]] name = "odp-service-common" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "embedded-services", "static_cell", @@ -959,7 +960,7 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "power-policy-interface" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "defmt 0.3.100", "embassy-sync 0.8.0", @@ -1001,7 +1002,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/dymk/qemu-riscv-rs?rev=a1ff86d62c683928464bf310de7d8ad965d5bd9b#a1ff86d62c683928464bf310de7d8ad965d5bd9b" dependencies = [ "critical-section", "riscv", @@ -1261,7 +1262,7 @@ dependencies = [ [[package]] name = "thermal-service" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "defmt 0.3.100", "embassy-futures", @@ -1278,7 +1279,7 @@ dependencies = [ [[package]] name = "thermal-service-interface" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "defmt 0.3.100", "embassy-time", @@ -1289,7 +1290,7 @@ dependencies = [ [[package]] name = "thermal-service-relay" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "defmt 0.3.100", "embedded-services", @@ -1330,7 +1331,7 @@ dependencies = [ [[package]] name = "time-alarm-service" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "defmt 0.3.100", "embassy-futures", @@ -1346,7 +1347,7 @@ dependencies = [ [[package]] name = "time-alarm-service-interface" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "bitfield 0.17.0", "defmt 0.3.100", @@ -1358,7 +1359,7 @@ dependencies = [ [[package]] name = "time-alarm-service-relay" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "bitfield 0.17.0", "defmt 0.3.100", @@ -1439,7 +1440,7 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uart-service" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#b2914600404371ba150093bec40d4e7e116b3369" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#baf1a1577c71375ec0ee33830b874f40e17b9423" dependencies = [ "defmt 0.3.100", "embassy-sync 0.8.0", diff --git a/platform/dev-qemu/Cargo.toml b/platform/dev-qemu/Cargo.toml index 0bbda45..4bfea21 100644 --- a/platform/dev-qemu/Cargo.toml +++ b/platform/dev-qemu/Cargo.toml @@ -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 = [ @@ -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" diff --git a/platform/dev-qemu/README.md b/platform/dev-qemu/README.md index 05ced1d..247874b 100644 --- a/platform/dev-qemu/README.md +++ b/platform/dev-qemu/README.md @@ -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: diff --git a/platform/dev-qemu/src/board.rs b/platform/dev-qemu/src/board.rs index a9a087f..fdc53b1 100644 --- a/platform/dev-qemu/src/board.rs +++ b/platform/dev-qemu/src/board.rs @@ -13,6 +13,11 @@ bind_interrupts!(struct Irqs { I2C_TARGET => target::InterruptHandler; }); +#[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 @@ -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 { @@ -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), + } } } diff --git a/platform/dev-qemu/src/main.rs b/platform/dev-qemu/src/main.rs index 0457b92..a7beec9 100644 --- a/platform/dev-qemu/src/main.rs +++ b/platform/dev-qemu/src/main.rs @@ -8,6 +8,8 @@ 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 @@ -15,10 +17,92 @@ use semihosting as _; // Panic handler 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, + } + } +} diff --git a/platform/platform-common/src/mock/mod.rs b/platform/platform-common/src/mock/mod.rs index f04d673..6822cb3 100644 --- a/platform/platform-common/src/mock/mod.rs +++ b/platform/platform-common/src/mock/mod.rs @@ -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), diff --git a/platform/platform-common/src/mock/time_alarm.rs b/platform/platform-common/src/mock/time_alarm.rs index 68005d5..e5d896a 100644 --- a/platform/platform-common/src/mock/time_alarm.rs +++ b/platform/platform-common/src/mock/time_alarm.rs @@ -2,9 +2,16 @@ use embedded_services::info; use static_cell::StaticCell; use time_alarm_service as tas; -type TimeAlarmService = tas::Service<'static>; +pub type TimeAlarmService = tas::Service<'static>; pub async fn init(spawner: embassy_executor::Spawner) -> TimeAlarmService { + init_with(spawner, |_| {}).await +} + +pub async fn init_with( + spawner: embassy_executor::Spawner, + configure: impl FnOnce(TimeAlarmService), +) -> TimeAlarmService { info!("Initializing time and alarm service..."); static TZ_STORAGE: StaticCell> = StaticCell::new(); @@ -25,15 +32,22 @@ pub async fn init(spawner: embassy_executor::Spawner) -> TimeAlarmService { static CLOCK: StaticCell = StaticCell::new(); let clock = CLOCK.init(tas::mock::MockDatetimeClock::new_running()); - let service = odp_service_common::spawn_service!(spawner, TimeAlarmService, |resources| tas::Service::new( - resources, - clock, - tz_storage, - ac_exp_storage, - ac_pol_storage, - dc_exp_storage, - dc_pol_storage, - )) + let service = odp_service_common::spawn_service!(spawner, TimeAlarmService, |resources| async { + tas::Service::new( + resources, + clock, + tz_storage, + ac_exp_storage, + ac_pol_storage, + dc_exp_storage, + dc_pol_storage, + ) + .await + .map(|(service, runner)| { + configure(service); + (service, runner) + }) + }) .expect("Failed to initialize time-alarm service"); service