Add orchestrator server runtime crate and integration QEMU test
diff --git a/services/orchestrator/server/BUILD.bazel b/services/orchestrator/server/BUILD.bazel new file mode 100644 index 0000000..9a3cd05 --- /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 0000000..ae559d3 --- /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 0000000..e0910a3 --- /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<const N: usize> { + timers: TimerManager<Instant, ComponentId, N>, +} + +impl<const N: usize> BootWatchdogs<N> { + 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<Event> { + self.timers + .poll(SystemClock::now()) + .map(|expired| match expired { + Expired::Boot(id) => Event::Timeout(id), + Expired::Commit => Event::CommitTimeout, + }) + } +} + +impl<const N: usize> Default for BootWatchdogs<N> { + 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 0000000..d8d6a98 --- /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 0000000..b3c4ec5 --- /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<N, E>; +type Watchdogs = BootWatchdogs<N>; + +/// 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<u8, u8> = 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<ComponentId, N>, +} + +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<Core> { + 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<N> = 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<u8>], + reached: usize, +) -> Result<Event> { + 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 0000000..002c5c2 --- /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 0000000..961db53 --- /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);