From 8084e7ac0903aac9d88b60fa062e2c6897d26324 Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Tue, 11 Aug 2026 15:10:44 -0700 Subject: [PATCH 1/7] ast10x0: scaffold timer qemu test suite Kernel target + single-process test_timer app + interrupt object (IRQ 44). Stub app passes trivially; harness verified to detect both PASS and FAIL. --- target/ast10x0/tests/timer/BUILD.bazel | 90 +++++++++++++++++++++++++ target/ast10x0/tests/timer/main.rs | 21 ++++++ target/ast10x0/tests/timer/system.json5 | 64 ++++++++++++++++++ target/ast10x0/tests/timer/target.rs | 35 ++++++++++ 4 files changed, 210 insertions(+) create mode 100644 target/ast10x0/tests/timer/BUILD.bazel create mode 100644 target/ast10x0/tests/timer/main.rs create mode 100644 target/ast10x0/tests/timer/system.json5 create mode 100644 target/ast10x0/tests/timer/target.rs diff --git a/target/ast10x0/tests/timer/BUILD.bazel b/target/ast10x0/tests/timer/BUILD.bazel new file mode 100644 index 00000000..bd94d242 --- /dev/null +++ b/target/ast10x0/tests/timer/BUILD.bazel @@ -0,0 +1,90 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image", "system_image_test") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +# Test app: exercises TimerManager against object_wait; calls +# debug_shutdown(Ok|Err) to report the result. +rust_app( + name = "test_timer", + srcs = ["main.rs"], + codegen_crate_name = "app_test_timer", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/orchestrator/sm:orchestrator_sm", + "//services/orchestrator/timer:orchestrator_timer", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "timer", + apps = [":test_timer"], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +system_image_test( + name = "timer_test", + image = ":timer", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":timer", + tags = ["kernel"], +) diff --git a/target/ast10x0/tests/timer/main.rs b/target/ast10x0/tests/timer/main.rs new file mode 100644 index 00000000..0c48ac53 --- /dev/null +++ b/target/ast10x0/tests/timer/main.rs @@ -0,0 +1,21 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Timer QEMU test: TimerManager <-> object_wait seam. Scaffold stub. + +#![no_main] +#![no_std] + +use userspace::{entry, syscall}; + +#[entry] +fn entry() { + pw_log::info!("timer test: scaffold up"); + let _ = syscall::debug_shutdown(Ok(())); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/timer/system.json5 b/target/ast10x0/tests/timer/system.json5 new file mode 100644 index 00000000..1d21d3d7 --- /dev/null +++ b/target/ast10x0/tests/timer/system.json5 @@ -0,0 +1,64 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 timer QEMU test: TimerManager <-> object_wait seam. +// +// Single app, single process. The interrupt object (IRQ 44, self-fired via +// debug_trigger_interrupt) is the "event arrived" source for the +// cancel-via-event scenario; deadlines come from TimerManager::next_deadline. +// +// Memory map (AST10x0: 768 KB SRAM, no XIP), same shape as tests/mctp/ipc_client: +// 0x00000000 - 0x00000500 vector table (1280 B) +// 0x00000500 - 0x00020200 kernel flash (~127 KB) +// then app flash (128 KB) +// 0x00060000 - 0x00080000 kernel RAM (128 KB) +// then app RAM (32 KB) +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, + }, + kernel: { + flash_start_address: 0x00000500, + flash_size_bytes: 129792, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "test_timer", + flash_size_bytes: 131072, + processes: [ + { + name: "test_timer_process", + ram_size_bytes: 32768, + objects: [ + { + name: "timer_irq", + type: "interrupt", + irqs: [ + { + name: "test_irq", + number: 44, + }, + ], + }, + { + type: "thread", + name: "test_timer_thread", + kernel_stack_size_bytes: 4096, + },], + + }, + ], + constants: [ + { + name: "test_irq", + type: "u32", + value: 44, + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/timer/target.rs b/target/ast10x0/tests/timer/target.rs new file mode 100644 index 00000000..12fd4720 --- /dev/null +++ b/target/ast10x0/tests/timer/target.rs @@ -0,0 +1,35 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 Timer Test"; + + fn main() -> ! { + codegen::start(); + #[expect(clippy::empty_loop)] + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + let sentinel: &[u8] = if code == 0 { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); From 9163311c2180b93b565936359b2410ce91db2066 Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Tue, 11 Aug 2026 15:11:32 -0700 Subject: [PATCH 2/7] ast10x0: timer test scenario 1 - ordered expiry, tie-break, one-shot --- target/ast10x0/tests/timer/main.rs | 94 +++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/target/ast10x0/tests/timer/main.rs b/target/ast10x0/tests/timer/main.rs index 0c48ac53..6cbb3bb5 100644 --- a/target/ast10x0/tests/timer/main.rs +++ b/target/ast10x0/tests/timer/main.rs @@ -1,17 +1,105 @@ // Licensed under the Apache-2.0 license // SPDX-License-Identifier: Apache-2.0 -//! Timer QEMU test: TimerManager <-> object_wait seam. Scaffold stub. +//! Timer QEMU test: proves the TimerManager <-> object_wait seam with the +//! production types (userspace Instant, orchestrator-sm ComponentId). +//! +//! The wait loop is the intended orchestrator runtime shape: block in +//! object_wait on {interrupt object, next timer deadline}; DeadlineExceeded +//! drains TimerManager::poll, Ok is the event path. #![no_main] #![no_std] +use app_test_timer::{handle, signals}; +use openprot_orchestrator_sm::ComponentId; +use openprot_orchestrator_timer::{Expired, TimerManager}; +use pw_status::{Error, Result}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; use userspace::{entry, syscall}; +const C0: ComponentId = ComponentId::new(0); +const C1: ComponentId = ComponentId::new(1); + +/// Chain capacity 4 — matches the crate's own unit-test sizing. +type Tm = TimerManager; + +/// Scenario 1: arm Boot(C0)@+50ms, Boot(C1)@+100ms, Commit@+100ms. Expect +/// expiries in exactly that order (Boot(C1) before Commit exercises the +/// boot-before-commit tie-break at the shared deadline), each at or after its +/// armed offset (lower bound only — no upper bound under QEMU), one-shot. +fn scenario_ordered_expiry() -> Result<()> { + pw_log::info!("scenario 1: ordered expiry + tie-break + one-shot"); + let mut tm = Tm::new(); + let t0 = SystemClock::now(); + tm.arm_boot(C0, t0 + Duration::from_millis(50)) + .map_err(|_| Error::ResourceExhausted)?; + tm.arm_boot(C1, t0 + Duration::from_millis(100)) + .map_err(|_| Error::ResourceExhausted)?; + tm.arm_commit(t0 + Duration::from_millis(100)); + + let expected: [(Expired, Duration); 3] = [ + (Expired::Boot(C0), Duration::from_millis(50)), + (Expired::Boot(C1), Duration::from_millis(100)), + (Expired::Commit, Duration::from_millis(100)), + ]; + + let mut idx = 0; + while idx < expected.len() { + let deadline = tm.next_deadline().ok_or(Error::Internal)?; + match syscall::object_wait(handle::TIMER_IRQ, signals::TEST_IRQ, deadline) { + Err(Error::DeadlineExceeded) => { + let now = SystemClock::now(); + while let Some(fired) = tm.poll(now) { + let (want, offset) = expected[idx]; + if fired != want { + pw_log::error!("scenario 1: wrong expiry at index {}", idx as u32); + return Err(Error::Internal); + } + if now < t0 + offset { + pw_log::error!("scenario 1: expiry {} fired early", idx as u32); + return Err(Error::Internal); + } + idx += 1; + } + } + Ok(_) => { + pw_log::error!("scenario 1: unexpected event wakeup"); + return Err(Error::Internal); + } + Err(e) => return Err(e), + } + } + + if tm.poll(SystemClock::now()).is_some() { + pw_log::error!("scenario 1: watchdog fired twice (not one-shot)"); + return Err(Error::Internal); + } + if tm.next_deadline().is_some() { + pw_log::error!("scenario 1: deadline outstanding after full drain"); + return Err(Error::Internal); + } + pw_log::info!("scenario 1: PASS"); + Ok(()) +} + +fn run_test() -> Result<()> { + scenario_ordered_expiry()?; + Ok(()) +} + #[entry] fn entry() { - pw_log::info!("timer test: scaffold up"); - let _ = syscall::debug_shutdown(Ok(())); + match run_test() { + Ok(()) => { + pw_log::info!("timer test: all scenarios PASSED"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(e) => { + pw_log::error!("timer test FAILED: {}", e as u32); + let _ = syscall::debug_shutdown(Err(e)); + } + } loop {} } From c6df61e1ae96897a9a8a5ea1cda309d5af1821ff Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Tue, 11 Aug 2026 15:11:56 -0700 Subject: [PATCH 3/7] ast10x0: timer test scenario 2 - re-arm replaces, never stacks --- target/ast10x0/tests/timer/main.rs | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/target/ast10x0/tests/timer/main.rs b/target/ast10x0/tests/timer/main.rs index 6cbb3bb5..130e4dd4 100644 --- a/target/ast10x0/tests/timer/main.rs +++ b/target/ast10x0/tests/timer/main.rs @@ -83,8 +83,52 @@ fn scenario_ordered_expiry() -> Result<()> { Ok(()) } +/// Scenario 2: arm Boot(C0)@+30ms then immediately re-arm @+80ms. The 30 ms +/// deadline must be gone: the single wait must run to >= 80 ms and yield +/// exactly one Boot(C0). (If the re-arm stacked, next_deadline would be the +/// 30 ms entry and the elapsed-time check below would fail.) +fn scenario_rearm_replaces() -> Result<()> { + pw_log::info!("scenario 2: re-arm replaces"); + let mut tm = Tm::new(); + let t0 = SystemClock::now(); + tm.arm_boot(C0, t0 + Duration::from_millis(30)) + .map_err(|_| Error::ResourceExhausted)?; + tm.arm_boot(C0, t0 + Duration::from_millis(80)) + .map_err(|_| Error::ResourceExhausted)?; + + let deadline = tm.next_deadline().ok_or(Error::Internal)?; + match syscall::object_wait(handle::TIMER_IRQ, signals::TEST_IRQ, deadline) { + Err(Error::DeadlineExceeded) => {} + Ok(_) => { + pw_log::error!("scenario 2: unexpected event wakeup"); + return Err(Error::Internal); + } + Err(e) => return Err(e), + } + + let now = SystemClock::now(); + if now < t0 + Duration::from_millis(80) { + pw_log::error!("scenario 2: woke before the replaced 80ms deadline"); + return Err(Error::Internal); + } + match tm.poll(now) { + Some(Expired::Boot(id)) if id == C0 => {} + _ => { + pw_log::error!("scenario 2: expected exactly Boot(C0)"); + return Err(Error::Internal); + } + } + if tm.poll(SystemClock::now()).is_some() || tm.next_deadline().is_some() { + pw_log::error!("scenario 2: stacked entry survived the re-arm"); + return Err(Error::Internal); + } + pw_log::info!("scenario 2: PASS"); + Ok(()) +} + fn run_test() -> Result<()> { scenario_ordered_expiry()?; + scenario_rearm_replaces()?; Ok(()) } From fd967a5b118320fda3fb8256f36f0ee87758c28a Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Tue, 11 Aug 2026 15:12:27 -0700 Subject: [PATCH 4/7] ast10x0: timer test scenario 3 - event before deadline cancels watchdog --- target/ast10x0/tests/timer/main.rs | 49 +++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/target/ast10x0/tests/timer/main.rs b/target/ast10x0/tests/timer/main.rs index 130e4dd4..99091bb5 100644 --- a/target/ast10x0/tests/timer/main.rs +++ b/target/ast10x0/tests/timer/main.rs @@ -11,7 +11,7 @@ #![no_main] #![no_std] -use app_test_timer::{handle, signals}; +use app_test_timer::{constants, handle, signals}; use openprot_orchestrator_sm::ComponentId; use openprot_orchestrator_timer::{Expired, TimerManager}; use pw_status::{Error, Result}; @@ -126,9 +126,56 @@ fn scenario_rearm_replaces() -> Result<()> { Ok(()) } +/// Scenario 3: arm Boot(C0)@+500ms, then fire the test IRQ. object_wait must +/// return Ok (event beat the deadline); the "component reported in" analogue +/// then cancels the watchdog and nothing is left armed. The trigger is issued +/// before the wait — interrupt objects latch the pending signal, so this does +/// not race. +fn scenario_cancel_via_event() -> Result<()> { + pw_log::info!("scenario 3: cancel via event"); + let mut tm = Tm::new(); + let t0 = SystemClock::now(); + tm.arm_boot(C0, t0 + Duration::from_millis(500)) + .map_err(|_| Error::ResourceExhausted)?; + + syscall::debug_trigger_interrupt(constants::TEST_IRQ)?; + + let deadline = tm.next_deadline().ok_or(Error::Internal)?; + let wait = match syscall::object_wait(handle::TIMER_IRQ, signals::TEST_IRQ, deadline) { + Ok(wait) => wait, + Err(Error::DeadlineExceeded) => { + pw_log::error!("scenario 3: deadline fired although the event was pending"); + return Err(Error::Internal); + } + Err(e) => return Err(e), + }; + if !wait.pending_signals.contains(signals::TEST_IRQ) { + pw_log::error!("scenario 3: woke without TEST_IRQ pending"); + return Err(Error::Internal); + } + syscall::interrupt_ack(handle::TIMER_IRQ, signals::TEST_IRQ)?; + + if SystemClock::now() >= t0 + Duration::from_millis(500) { + pw_log::error!("scenario 3: event did not beat the 500ms deadline"); + return Err(Error::Internal); + } + tm.cancel_boot(C0); + if tm.next_deadline().is_some() { + pw_log::error!("scenario 3: deadline still armed after cancel"); + return Err(Error::Internal); + } + if tm.poll(SystemClock::now()).is_some() { + pw_log::error!("scenario 3: cancelled watchdog fired"); + return Err(Error::Internal); + } + pw_log::info!("scenario 3: PASS"); + Ok(()) +} + fn run_test() -> Result<()> { scenario_ordered_expiry()?; scenario_rearm_replaces()?; + scenario_cancel_via_event()?; Ok(()) } From 7e47da5808e99008a2b7284f20a0ff11db1909fd Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 12 Aug 2026 10:04:25 -0700 Subject: [PATCH 5/7] Move timer QEMU test under tests/orchestrator --- target/ast10x0/tests/{ => orchestrator}/timer/BUILD.bazel | 0 target/ast10x0/tests/{ => orchestrator}/timer/main.rs | 0 target/ast10x0/tests/{ => orchestrator}/timer/system.json5 | 0 target/ast10x0/tests/{ => orchestrator}/timer/target.rs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename target/ast10x0/tests/{ => orchestrator}/timer/BUILD.bazel (100%) rename target/ast10x0/tests/{ => orchestrator}/timer/main.rs (100%) rename target/ast10x0/tests/{ => orchestrator}/timer/system.json5 (100%) rename target/ast10x0/tests/{ => orchestrator}/timer/target.rs (100%) diff --git a/target/ast10x0/tests/timer/BUILD.bazel b/target/ast10x0/tests/orchestrator/timer/BUILD.bazel similarity index 100% rename from target/ast10x0/tests/timer/BUILD.bazel rename to target/ast10x0/tests/orchestrator/timer/BUILD.bazel diff --git a/target/ast10x0/tests/timer/main.rs b/target/ast10x0/tests/orchestrator/timer/main.rs similarity index 100% rename from target/ast10x0/tests/timer/main.rs rename to target/ast10x0/tests/orchestrator/timer/main.rs diff --git a/target/ast10x0/tests/timer/system.json5 b/target/ast10x0/tests/orchestrator/timer/system.json5 similarity index 100% rename from target/ast10x0/tests/timer/system.json5 rename to target/ast10x0/tests/orchestrator/timer/system.json5 diff --git a/target/ast10x0/tests/timer/target.rs b/target/ast10x0/tests/orchestrator/timer/target.rs similarity index 100% rename from target/ast10x0/tests/timer/target.rs rename to target/ast10x0/tests/orchestrator/timer/target.rs From 4d258019ac4a051456f358d492bd47e13e4bc10b Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 12 Aug 2026 10:36:42 -0700 Subject: [PATCH 6/7] Add README for timer QEMU test --- .../tests/orchestrator/timer/README.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 target/ast10x0/tests/orchestrator/timer/README.md diff --git a/target/ast10x0/tests/orchestrator/timer/README.md b/target/ast10x0/tests/orchestrator/timer/README.md new file mode 100644 index 00000000..bf7307c1 --- /dev/null +++ b/target/ast10x0/tests/orchestrator/timer/README.md @@ -0,0 +1,77 @@ +# Timer QEMU test + +This test runs on an emulated AST10x0 (under QEMU) and checks that the +orchestrator's timer bookkeeping, [`TimerManager`](../../../../../services/orchestrator/timer/src/lib.rs), +works with the real system clock and the kernel's wait call. + +The timer *logic* is already proven by host-based unit tests: ordering, +tie-breaks, one-shot firing, resetting a timer, and cancelling one are all +asserted there against a fake clock. This test does not re-prove that logic. Its +job is the part the unit tests can't reach — the integration with real hardware +timing: + +- real time actually elapsing, and the `Instant + Duration` math that goes with + it, instead of a hand-advanced counter; +- the kernel's wait call really returning "deadline passed" when a deadline + lapses and "signal arrived" when an interrupt fires — the unit tests never call + it. + +The scenarios below mirror the unit tests on purpose, so the same behavior is +confirmed once the real clock and the real wait call are in the loop. + +## The idea being tested + +The orchestrator watches each component with a timer. It sets a deadline, then +waits for either the component to signal progress or the deadline to pass: + +```text +1. Ask TimerManager for the next deadline. +2. Wait for either a signal or that deadline. +3. If the deadline passed first -> a timer expired (the component was too slow). + If a signal arrived first -> the component reported in; cancel its timer. +``` + +This test recreates that wait loop and confirms each part behaves. + +## How the test is set up + +- Firmware image running on the chip. +- A fake "component signalled progress" event, produced by triggering interrupt + number 44 from within the test itself. +- Deadlines come from `TimerManager`; the time comes from the system clock. +- When done, the program shuts the machine down and prints `TEST_RESULT:PASS` + (or `FAIL`). + +## What each scenario checks + +1. **Timers expire in the right order.** Three timers are set (two component + timers and one commit timer, two of them sharing the same deadline). The test + confirms they expire in the expected order, none expire early, and each fires + only once. +2. **Resetting a timer replaces the old one.** A timer is set for 30 ms, then + immediately reset to 80 ms. The test confirms only the 80 ms deadline is left + — the old 30 ms one is gone, not stacked on top. +3. **A signal cancels the timer.** A timer is set for 500 ms, but the progress + signal is sent first. The test confirms the wait returns because of the + signal (not the deadline), the timer is cancelled, and nothing is left + pending. + +## Files + +- `main.rs` — the three scenarios and the program's entry point. +- `system.json5` — how the program, its memory, and the interrupt are laid out. +- `target.rs` — prints the final `TEST_RESULT:PASS/FAIL` line. +- `BUILD.bazel` — how the test image is built and run. + +## Running it + +```sh +bazelisk test --config=virt_ast10x0 \ + //target/ast10x0/tests/orchestrator/timer:timer_test --test_output=all +``` + +Other build targets in this folder: + +- `:timer` — the bootable image. +- `:timer_test` — runs that image in QEMU (the command above). +- `:no_panics_test` — checks the image contains no panic code paths. From dd94a57aa4425dc964098d101714ba8d24f4930b Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 12 Aug 2026 13:01:20 -0700 Subject: [PATCH 7/7] Add orchestrator server runtime crate and integration QEMU test --- services/orchestrator/server/BUILD.bazel | 23 + services/orchestrator/server/src/lib.rs | 19 + services/orchestrator/server/src/runtime.rs | 84 ++++ .../tests/orchestrator/runtime/BUILD.bazel | 94 ++++ .../tests/orchestrator/runtime/main.rs | 404 ++++++++++++++++++ .../tests/orchestrator/runtime/system.json5 | 66 +++ .../tests/orchestrator/runtime/target.rs | 35 ++ 7 files changed, 725 insertions(+) create mode 100644 services/orchestrator/server/BUILD.bazel create mode 100644 services/orchestrator/server/src/lib.rs create mode 100644 services/orchestrator/server/src/runtime.rs create mode 100644 target/ast10x0/tests/orchestrator/runtime/BUILD.bazel create mode 100644 target/ast10x0/tests/orchestrator/runtime/main.rs create mode 100644 target/ast10x0/tests/orchestrator/runtime/system.json5 create mode 100644 target/ast10x0/tests/orchestrator/runtime/target.rs diff --git a/services/orchestrator/server/BUILD.bazel b/services/orchestrator/server/BUILD.bazel new file mode 100644 index 00000000..9a3cd056 --- /dev/null +++ b/services/orchestrator/server/BUILD.bazel @@ -0,0 +1,23 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") + +rust_library( + name = "orchestrator_server", + srcs = [ + "src/lib.rs", + "src/runtime.rs", + ], + crate_name = "openprot_orchestrator_server", + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/orchestrator/sm:orchestrator_sm", + "//services/orchestrator/timer:orchestrator_timer", + "@pigweed//pw_kernel/userspace", + ], +) diff --git a/services/orchestrator/server/src/lib.rs b/services/orchestrator/server/src/lib.rs new file mode 100644 index 00000000..ae559d3b --- /dev/null +++ b/services/orchestrator/server/src/lib.rs @@ -0,0 +1,19 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Orchestrator server: the in-process runtime that drives the pure +//! [`openprot_orchestrator_sm`] state machine. +//! +//! Orchestrator-sm names timeouts as [`Event`](openprot_orchestrator_sm::Event)s +//! but owns no clock. [`TimerManager`] lives here, in the same process, and +//! multiplexes orchestrator-sm's boot and commit watchdogs onto the single +//! deadline the runtime's `object_wait` already accepts — no separate timer +//! task, no IPC on the arm/cancel path. + +#![no_std] +#![forbid(unsafe_code)] + +pub mod runtime; + +pub use openprot_orchestrator_timer::{Full, TimerManager}; +pub use runtime::BootWatchdogs; diff --git a/services/orchestrator/server/src/runtime.rs b/services/orchestrator/server/src/runtime.rs new file mode 100644 index 00000000..e0910a37 --- /dev/null +++ b/services/orchestrator/server/src/runtime.rs @@ -0,0 +1,84 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Kernel-clock binding for [`TimerManager`]. +//! +//! [`BootWatchdogs`] instantiates the host-generic [`TimerManager`] with the +//! kernel's [`Instant`] and translates the run loop's relative boot/commit +//! windows into the absolute deadlines the manager tracks. The absolute +//! [`wait_deadline`](BootWatchdogs::wait_deadline) it returns is exactly the +//! argument the loop hands to `object_wait`; after each wake the loop drains +//! [`poll_expired`](BootWatchdogs::poll_expired) into orchestrator-sm. + +use openprot_orchestrator_sm::{ComponentId, Event}; +use openprot_orchestrator_timer::{Expired, Full, TimerManager}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +/// The orchestrator's watchdogs, driven by the kernel monotonic clock. +/// +/// `N` bounds the boot watchdogs to the chain length, matching +/// [`TimerManager`]. +pub struct BootWatchdogs { + timers: TimerManager, +} + +impl BootWatchdogs { + pub const fn new() -> Self { + Self { + timers: TimerManager::new(), + } + } + + /// Now plus `after`, saturating to [`Instant::MAX`] on overflow so a huge + /// window degrades to "wait indefinitely" rather than firing immediately. + fn deadline_in(after: Duration) -> Instant { + SystemClock::now() + .checked_add_duration(after) + .unwrap_or(Instant::MAX) + } + + /// Arm (or re-arm) `id`'s boot watchdog to fire `after` from now. Returns + /// [`Full`] when a new component would exceed `N`; the run loop must + /// escalate rather than proceed with an unsupervised component. + pub fn arm_boot(&mut self, id: ComponentId, after: Duration) -> Result<(), Full> { + self.timers.arm_boot(id, Self::deadline_in(after)) + } + + /// Cancel `id`'s boot watchdog. + pub fn cancel_boot(&mut self, id: ComponentId) { + self.timers.cancel_boot(id); + } + + /// Arm the commit watchdog to fire `after` from now. + pub fn arm_commit(&mut self, after: Duration) { + self.timers.arm_commit(Self::deadline_in(after)); + } + + /// Cancel the commit watchdog. + pub fn cancel_commit(&mut self) { + self.timers.cancel_commit(); + } + + /// Absolute deadline to pass to `object_wait`; [`Instant::MAX`] when nothing + /// is armed, so the loop blocks until a signal wakes it. + pub fn wait_deadline(&self) -> Instant { + self.timers.next_deadline().unwrap_or(Instant::MAX) + } + + /// Pop the next watchdog due as of now, or `None`. Call in a loop after each + /// `object_wait` return to drain every deadline that has passed this tick. + pub fn poll_expired(&mut self) -> Option { + self.timers + .poll(SystemClock::now()) + .map(|expired| match expired { + Expired::Boot(id) => Event::Timeout(id), + Expired::Commit => Event::CommitTimeout, + }) + } +} + +impl Default for BootWatchdogs { + fn default() -> Self { + Self::new() + } +} diff --git a/target/ast10x0/tests/orchestrator/runtime/BUILD.bazel b/target/ast10x0/tests/orchestrator/runtime/BUILD.bazel new file mode 100644 index 00000000..d8d6a98f --- /dev/null +++ b/target/ast10x0/tests/orchestrator/runtime/BUILD.bazel @@ -0,0 +1,94 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image", "system_image_test") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +# Test app: drives the real orchestrator-sm core through the server runtime +# (BootWatchdogs) — arming boot watchdogs, blocking in object_wait on +# wait_deadline, and feeding poll_expired events back into the core. +# debug_shutdown(Ok|Err) reports. +rust_app( + name = "test_runtime", + srcs = ["main.rs"], + codegen_crate_name = "app_test_runtime", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/orchestrator/config:orchestrator_config", + "//services/orchestrator/server:orchestrator_server", + "//services/orchestrator/sm:orchestrator_sm", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:heapless", + ], +) + +system_image( + name = "runtime", + apps = [":test_runtime"], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +system_image_test( + name = "runtime_test", + image = ":runtime", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":runtime", + tags = ["kernel"], +) diff --git a/target/ast10x0/tests/orchestrator/runtime/main.rs b/target/ast10x0/tests/orchestrator/runtime/main.rs new file mode 100644 index 00000000..b3c4ec5f --- /dev/null +++ b/target/ast10x0/tests/orchestrator/runtime/main.rs @@ -0,0 +1,404 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Orchestrator integration QEMU test: all four subcomponents wired end to end +//! under the kernel — the pure core ([`Orchestrator`], `orchestrator-sm`), the +//! server runtime ([`BootWatchdogs`], `orchestrator-server`) which wraps the +//! watchdog keeper (`orchestrator-timer`), and the board device table +//! ([`DeviceConfig`], `orchestrator-config`). +//! +//! The runtime owns the clock and the mapping, so the shell stays thin: +//! - boot windows come from the device table ([`BootCheckpoint::timeout`]); +//! the shell only converts `core::time::Duration` to the kernel's +//! [`Duration`] at the arm site. +//! - [`BootWatchdogs::arm_boot`] takes that *relative* window; the runtime +//! computes the absolute deadline. +//! - [`BootWatchdogs::wait_deadline`] is handed straight to `object_wait`. +//! - [`BootWatchdogs::poll_expired`] yields the `Event`s the core consumes — +//! no mapping in the shell. +//! +//! Coverage: the *inner checkpoint walk* (`bl1` → `kernel`, re-armed through the +//! runtime) for a single component, the *outer component walk* across a +//! multi-component chain (nearest-of-many deadlines, correct-id recovery), and +//! the commit watchdog. The interrupt object (IRQ 44, self-fired) stands in for +//! a component reaching a checkpoint. + +#![no_main] +#![no_std] + +use app_test_runtime::{constants, handle, signals}; +use openprot_orchestrator_server::BootWatchdogs; +use openprot_orchestrator_sm::{ + Chain, ComponentAttrs, ComponentId, Effect, EffectError, Event, Orchestrator, Platform, + PowerOnResult, State, +}; +use orchestrator_config::{BootCheckpoint, DeviceConfig}; +use pw_status::{Error, Result}; +use userspace::time::Duration; +use userspace::{entry, syscall}; + +/// The components this test supervises. +const C0: ComponentId = ComponentId::new(0); +const C1: ComponentId = ComponentId::new(1); + +/// Chain capacity and effect-sink cap for the core (`E >= 2*N + 2`). +const N: usize = 4; +const E: usize = 2 * N + 2; +const MAX_RETRY: u8 = 3; + +/// Commit watchdog window. Not a boot window, so it stays a local constant +/// rather than coming from the device table. +const COMMIT_WINDOW: Duration = Duration::from_millis(50); + +type Core = Orchestrator; +type Watchdogs = BootWatchdogs; + +/// The device table: per-checkpoint windows, exactly as a board would declare +/// them. Two checkpoints so the inner walk exercises re-arm-on-progress +/// (`bl1` then `kernel`). +const SOC: DeviceConfig = DeviceConfig::new( + "soc", + 0, + &[ + BootCheckpoint::new("bl1", 0, core::time::Duration::from_millis(50)), + BootCheckpoint::new("kernel", 0, core::time::Duration::from_millis(50)), + ], +); + +/// The device table speaks `core::time::Duration`; the runtime speaks the +/// kernel's [`Duration`]. Converting is the shell's job. +fn window(timeout: core::time::Duration) -> Duration { + Duration::from_millis(timeout.as_millis() as u64) +} + +/// A fake [`Platform`] for the run loop. It records the `ReleaseReset(id)` +/// effects that open each component's boot supervision; every other effect is +/// accepted so the core can settle. +struct FakePlatform { + released: heapless::Vec, +} + +impl FakePlatform { + const fn new() -> Self { + Self { + released: heapless::Vec::new(), + } + } + + fn was_released(&self, id: ComponentId) -> bool { + self.released.contains(&id) + } +} + +impl Platform for FakePlatform { + fn execute(&mut self, effect: Effect) -> core::result::Result<(), EffectError> { + if let Effect::ReleaseReset(id) = effect { + let _ = self.released.push(id); + } + Ok(()) + } +} + +/// A fresh core with the given components, each passive/required. +fn new_core(ids: &[ComponentId]) -> Result { + let mut v = heapless::Vec::<(ComponentId, ComponentAttrs), N>::new(); + for id in ids { + v.push((*id, ComponentAttrs::passive_required())) + .map_err(|_| Error::ResourceExhausted)?; + } + let chain: Chain = v.try_into().map_err(|_| Error::Internal)?; + Ok(Orchestrator::new(chain, MAX_RETRY)) +} + +/// Power on, then pass verification for each component in chain order. Each +/// `VerificationPassed` releases its component (speculative release), so all +/// are released before any boots. +fn drive_releases(core: &mut Core, plat: &mut FakePlatform, ids: &[ComponentId]) -> Result<()> { + core.dispatch(plat, Event::PowerGood(PowerOnResult::Provisioned)); + for id in ids { + core.dispatch(plat, Event::VerificationPassed(*id)); + if !plat.was_released(*id) { + return Err(Error::Internal); + } + } + Ok(()) +} + +/// Run one component's inner checkpoint walk through the runtime and return its +/// single terminal event. `reached` simulates the device: it fires its progress +/// signal for the first `reached` checkpoints, then goes quiet — so +/// `reached == len` boots, anything less times out at checkpoint `reached`. +fn checkpoint_walk( + wd: &mut Watchdogs, + id: ComponentId, + checkpoints: &[BootCheckpoint], + reached: usize, +) -> Result { + let mut k = 0usize; + wd.arm_boot(id, window(checkpoints[k].timeout())) + .map_err(|_| Error::ResourceExhausted)?; + loop { + // Simulated device reaching checkpoint `k`: latch its progress signal + // before the wait (interrupt objects hold it pending, so no race). + if k < reached { + syscall::debug_trigger_interrupt(constants::BOOT_PROGRESS)?; + } + + let deadline = wd.wait_deadline(); + match syscall::object_wait(handle::BOOT_SIGNAL, signals::BOOT_PROGRESS, deadline) { + Ok(wait) => { + if !wait.pending_signals.contains(signals::BOOT_PROGRESS) { + return Err(Error::Internal); + } + syscall::interrupt_ack(handle::BOOT_SIGNAL, signals::BOOT_PROGRESS)?; + k += 1; + if k == checkpoints.len() { + wd.cancel_boot(id); + return Ok(Event::Booted(id)); + } + // Forward progress: re-arm the next checkpoint through the runtime. + wd.arm_boot(id, window(checkpoints[k].timeout())) + .map_err(|_| Error::ResourceExhausted)?; + } + Err(Error::DeadlineExceeded) => { + // The window lapsed: the runtime already mapped it to an `Event`. + return wd.poll_expired().ok_or(Error::Internal); + } + Err(e) => return Err(e), + } + } +} + +/// Simulate `id`'s device reporting in: latch the progress signal, wait, ack, +/// and retire its watchdog through the runtime. +fn confirm(wd: &mut Watchdogs, id: ComponentId) -> Result<()> { + syscall::debug_trigger_interrupt(constants::BOOT_PROGRESS)?; + match syscall::object_wait( + handle::BOOT_SIGNAL, + signals::BOOT_PROGRESS, + wd.wait_deadline(), + ) { + Ok(wait) => { + if !wait.pending_signals.contains(signals::BOOT_PROGRESS) { + return Err(Error::Internal); + } + syscall::interrupt_ack(handle::BOOT_SIGNAL, signals::BOOT_PROGRESS)?; + wd.cancel_boot(id); + Ok(()) + } + Err(e) => Err(e), + } +} + +/// Inner walk, happy path: a single component passes every checkpoint (windows +/// from the device table, re-armed through the runtime), the walk yields +/// `Booted`, and the core stays `Ready`. A late `Timeout` is then a no-op — the +/// watchdog was retired by the confirmation. +fn scenario_checkpoint_confirmed() -> Result<()> { + pw_log::info!("scenario 1: checkpoint walk confirmed"); + let mut core = new_core(&[C0])?; + let mut plat = FakePlatform::new(); + let mut wd = Watchdogs::new(); + + drive_releases(&mut core, &mut plat, &[C0])?; + if core.state() != State::Ready { + pw_log::error!("scenario 1: single component did not reach Ready on release"); + return Err(Error::Internal); + } + + let terminal = checkpoint_walk(&mut wd, C0, SOC.checkpoints(), SOC.checkpoints().len())?; + if terminal != Event::Booted(C0) { + pw_log::error!("scenario 1: walk did not confirm boot"); + return Err(Error::Internal); + } + core.dispatch(&mut plat, terminal); + if core.state() != State::Ready { + pw_log::error!("scenario 1: core left Ready after boot confirmed"); + return Err(Error::Internal); + } + + // The watchdog is retired: a stale timeout must not re-open recovery. + core.dispatch(&mut plat, Event::Timeout(C0)); + if core.state() != State::Ready { + pw_log::error!("scenario 1: stale timeout re-opened recovery"); + return Err(Error::Internal); + } + + pw_log::info!("scenario 1: PASS"); + Ok(()) +} + +/// Inner walk, timeout path: the device never signals, the first checkpoint's +/// window lapses, the runtime surfaces `Timeout`, and the core recovers. +fn scenario_checkpoint_timeout() -> Result<()> { + pw_log::info!("scenario 2: checkpoint walk timeout drives recovery"); + let mut core = new_core(&[C0])?; + let mut plat = FakePlatform::new(); + let mut wd = Watchdogs::new(); + + drive_releases(&mut core, &mut plat, &[C0])?; + + let terminal = checkpoint_walk(&mut wd, C0, SOC.checkpoints(), 0)?; + if terminal != Event::Timeout(C0) { + pw_log::error!("scenario 2: walk did not time out"); + return Err(Error::Internal); + } + core.dispatch(&mut plat, terminal); + if core.state() != State::Recovering(C0) { + pw_log::error!("scenario 2: core did not enter recovery"); + return Err(Error::Internal); + } + + pw_log::info!("scenario 2: PASS"); + Ok(()) +} + +/// Outer walk, all confirm: a two-component chain, both boot watchdogs armed at +/// once, both components report in, the core reaches `Ready`. +fn scenario_chain_all_confirm() -> Result<()> { + pw_log::info!("scenario 3: multi-component chain all confirm"); + let mut core = new_core(&[C0, C1])?; + let mut plat = FakePlatform::new(); + let mut wd = Watchdogs::new(); + + drive_releases(&mut core, &mut plat, &[C0, C1])?; + if core.state() != State::Ready { + pw_log::error!("scenario 3: chain did not reach Ready on release"); + return Err(Error::Internal); + } + + // Both released speculatively: arm both watchdogs before either reports. + let boot = window(SOC.checkpoints()[0].timeout()); + wd.arm_boot(C0, boot) + .map_err(|_| Error::ResourceExhausted)?; + wd.arm_boot(C1, boot) + .map_err(|_| Error::ResourceExhausted)?; + + confirm(&mut wd, C0)?; + core.dispatch(&mut plat, Event::Booted(C0)); + confirm(&mut wd, C1)?; + core.dispatch(&mut plat, Event::Booted(C1)); + + if core.state() != State::Ready { + pw_log::error!("scenario 3: core left Ready after both booted"); + return Err(Error::Internal); + } + + pw_log::info!("scenario 3: PASS"); + Ok(()) +} + +/// Outer walk, one lapses: both watchdogs armed, `C0` reports in, `C1` goes +/// quiet. With only `C1` left, the runtime's nearest deadline is `C1`'s; it +/// lapses and `poll_expired` surfaces `Timeout(C1)`, recovering the right one. +fn scenario_chain_one_timeout() -> Result<()> { + pw_log::info!("scenario 4: multi-component chain, one times out"); + let mut core = new_core(&[C0, C1])?; + let mut plat = FakePlatform::new(); + let mut wd = Watchdogs::new(); + + drive_releases(&mut core, &mut plat, &[C0, C1])?; + + let boot = window(SOC.checkpoints()[0].timeout()); + wd.arm_boot(C0, boot) + .map_err(|_| Error::ResourceExhausted)?; + wd.arm_boot(C1, boot) + .map_err(|_| Error::ResourceExhausted)?; + + confirm(&mut wd, C0)?; + core.dispatch(&mut plat, Event::Booted(C0)); + + // Only C1 remains armed; wait for its window to lapse. + match syscall::object_wait( + handle::BOOT_SIGNAL, + signals::BOOT_PROGRESS, + wd.wait_deadline(), + ) { + Ok(_) => { + pw_log::error!("scenario 4: unexpected signal, C1's device is quiet"); + return Err(Error::Internal); + } + Err(Error::DeadlineExceeded) => { + let event = wd.poll_expired().ok_or(Error::Internal)?; + if event != Event::Timeout(C1) { + pw_log::error!("scenario 4: runtime timed out the wrong component"); + return Err(Error::Internal); + } + core.dispatch(&mut plat, event); + } + Err(e) => return Err(e), + } + + if core.state() != State::Recovering(C1) { + pw_log::error!("scenario 4: core did not recover C1"); + return Err(Error::Internal); + } + + pw_log::info!("scenario 4: PASS"); + Ok(()) +} + +/// Commit path of the runtime binding: arm the commit watchdog, let it lapse +/// against the real clock, and confirm the runtime surfaces `CommitTimeout` +/// (and nothing more). +fn scenario_commit_timeout() -> Result<()> { + pw_log::info!("scenario 5: commit watchdog surfaces CommitTimeout"); + let mut wd = Watchdogs::new(); + + wd.arm_commit(COMMIT_WINDOW); + match syscall::object_wait( + handle::BOOT_SIGNAL, + signals::BOOT_PROGRESS, + wd.wait_deadline(), + ) { + Ok(_) => { + pw_log::error!("scenario 5: unexpected signal, no device is armed"); + return Err(Error::Internal); + } + Err(Error::DeadlineExceeded) => { + if wd.poll_expired() != Some(Event::CommitTimeout) { + pw_log::error!("scenario 5: runtime did not surface CommitTimeout"); + return Err(Error::Internal); + } + } + Err(e) => return Err(e), + } + + // One-shot: the watchdog is drained, nothing more is due. + if wd.poll_expired().is_some() { + pw_log::error!("scenario 5: commit watchdog fired twice"); + return Err(Error::Internal); + } + + pw_log::info!("scenario 5: PASS"); + Ok(()) +} + +fn run_test() -> Result<()> { + scenario_checkpoint_confirmed()?; + scenario_checkpoint_timeout()?; + scenario_chain_all_confirm()?; + scenario_chain_one_timeout()?; + scenario_commit_timeout()?; + Ok(()) +} + +#[entry] +fn entry() { + match run_test() { + Ok(()) => { + pw_log::info!("runtime integration test: all scenarios PASSED"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(e) => { + pw_log::error!("runtime integration test FAILED: {}", e as u32); + let _ = syscall::debug_shutdown(Err(e)); + } + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/orchestrator/runtime/system.json5 b/target/ast10x0/tests/orchestrator/runtime/system.json5 new file mode 100644 index 00000000..002c5c2b --- /dev/null +++ b/target/ast10x0/tests/orchestrator/runtime/system.json5 @@ -0,0 +1,66 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 runtime integration QEMU test: the orchestrator server runtime +// (BootWatchdogs) driving the pure core through an object_wait loop. +// +// Single app, single process. The interrupt object (IRQ 44, self-fired via +// debug_trigger_interrupt) stands in for a component's boot-progress signal; +// deadlines come from BootWatchdogs::wait_deadline and expiries from +// BootWatchdogs::poll_expired. +// +// Memory map (AST10x0: 768 KB SRAM, no XIP), same shape as tests/orchestrator/boot_walk: +// 0x00000000 - 0x00000500 vector table (1280 B) +// 0x00000500 - 0x00020200 kernel flash (~127 KB) +// then app flash (128 KB) +// 0x00060000 - 0x00080000 kernel RAM (128 KB) +// then app RAM (32 KB) +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, + }, + kernel: { + flash_start_address: 0x00000500, + flash_size_bytes: 129792, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "test_runtime", + flash_size_bytes: 131072, + processes: [ + { + name: "test_runtime_process", + ram_size_bytes: 32768, + objects: [ + { + name: "boot_signal", + type: "interrupt", + irqs: [ + { + name: "boot_progress", + number: 44, + }, + ], + }, + { + type: "thread", + name: "test_runtime_thread", + kernel_stack_size_bytes: 4096, + },], + + }, + ], + constants: [ + { + name: "boot_progress", + type: "u32", + value: 44, + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/orchestrator/runtime/target.rs b/target/ast10x0/tests/orchestrator/runtime/target.rs new file mode 100644 index 00000000..961db53e --- /dev/null +++ b/target/ast10x0/tests/orchestrator/runtime/target.rs @@ -0,0 +1,35 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 Runtime Integration Test"; + + fn main() -> ! { + codegen::start(); + #[expect(clippy::empty_loop)] + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + let sentinel: &[u8] = if code == 0 { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target);