From e4ffdaba614c545b9268b790df54a75b216b073e Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 12 Aug 2026 18:46:11 -0700 Subject: [PATCH] ast10x0: add watchdog peripheral driver and QEMU smoke test --- target/ast10x0/peripherals/BUILD.bazel | 3 + target/ast10x0/peripherals/lib.rs | 1 + target/ast10x0/peripherals/wdt/mod.rs | 110 +++++++++++++++ target/ast10x0/peripherals/wdt/registers.rs | 88 ++++++++++++ target/ast10x0/peripherals/wdt/types.rs | 54 ++++++++ .../peripherals/wdt/wdt_smoke/BUILD.bazel | 73 ++++++++++ .../peripherals/wdt/wdt_smoke/system.json5 | 17 +++ .../tests/peripherals/wdt/wdt_smoke/target.rs | 129 ++++++++++++++++++ 8 files changed, 475 insertions(+) create mode 100644 target/ast10x0/peripherals/wdt/mod.rs create mode 100644 target/ast10x0/peripherals/wdt/registers.rs create mode 100644 target/ast10x0/peripherals/wdt/types.rs create mode 100644 target/ast10x0/tests/peripherals/wdt/wdt_smoke/BUILD.bazel create mode 100644 target/ast10x0/tests/peripherals/wdt/wdt_smoke/system.json5 create mode 100644 target/ast10x0/tests/peripherals/wdt/wdt_smoke/target.rs diff --git a/target/ast10x0/peripherals/BUILD.bazel b/target/ast10x0/peripherals/BUILD.bazel index 5e10fb2fa..68e44c8eb 100644 --- a/target/ast10x0/peripherals/BUILD.bazel +++ b/target/ast10x0/peripherals/BUILD.bazel @@ -84,6 +84,9 @@ rust_library( "spimonitor/traits.rs", "spimonitor/types.rs", "uart/mod.rs", + "wdt/mod.rs", + "wdt/registers.rs", + "wdt/types.rs", ], crate_name = "ast10x0_peripherals", crate_root = "lib.rs", diff --git a/target/ast10x0/peripherals/lib.rs b/target/ast10x0/peripherals/lib.rs index 8d2303dda..2620de99f 100644 --- a/target/ast10x0/peripherals/lib.rs +++ b/target/ast10x0/peripherals/lib.rs @@ -12,3 +12,4 @@ pub mod sgpiom; pub mod smc; pub mod spimonitor; pub mod uart; +pub mod wdt; diff --git a/target/ast10x0/peripherals/wdt/mod.rs b/target/ast10x0/peripherals/wdt/mod.rs new file mode 100644 index 000000000..3ff307c6b --- /dev/null +++ b/target/ast10x0/peripherals/wdt/mod.rs @@ -0,0 +1,110 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! AST10x0 watchdog peripheral driver. +//! +//! The AST10x0 provides four independent watchdog timers clocked at 1 MHz. +//! Obtain an instance via one of the `WdtRegisters` constructors (all unsafe), +//! wrap it in a [`Watchdog`], then [`start`](Watchdog::start) the timer and +//! [`feed`](Watchdog::feed) it before the window elapses. +//! +//! ```no_run +//! use peripherals::wdt::{ResetMode, Watchdog, WdtConfig, WdtRegisters}; +//! +//! // SAFETY: single owner of WDT0 for the lifetime of `wdt`. +//! let mut wdt = Watchdog::new(unsafe { WdtRegisters::new_wdt0() }); +//! wdt.start(WdtConfig { +//! timeout_ms: 1000, +//! reset_on_timeout: true, +//! reset_mode: ResetMode::SocSystem, +//! }) +//! .unwrap(); +//! wdt.feed(); +//! ``` + +mod registers; +mod types; + +pub use registers::WdtRegisters; +pub use types::{ResetMode, WdtConfig, WdtError, WDT_CLOCK_HZ}; + +use types::RESTART_MAGIC; + +/// Blocking AST10x0 watchdog driver bound to one watchdog instance. +pub struct Watchdog { + regs: WdtRegisters, +} + +impl Watchdog { + /// Wrap a watchdog register accessor in a driver. + #[must_use] + pub const fn new(regs: WdtRegisters) -> Self { + Self { regs } + } + + /// Program the reload window and start counting. + /// + /// The counter is loaded from the reload value and the timer is enabled with + /// the requested reset behavior. Returns [`WdtError::InvalidTimeout`] if the + /// window does not fit the 32-bit counter. + pub fn start(&mut self, config: WdtConfig) -> Result<(), WdtError> { + let ticks = config.reload_ticks()?; + let regs = self.regs.regs(); + + // Load the reload value, then trigger a reload so the counter starts from it. + regs.wdt004().write(|w| unsafe { w.bits(ticks) }); + regs.wdt008().write(|w| unsafe { w.bits(RESTART_MAGIC) }); + + regs.wdt00c().write(|w| { + match config.reset_mode { + ResetMode::SocSystem => w + .rst_sys_mode() + .soc_system_ewvergated_by_reset_mask_registers(), + ResetMode::FullChip => w.rst_sys_mode().full_chip(), + ResetMode::CpuFmcOnly => w + .rst_sys_mode() + .cpufmc_only_just_reboot_firmware_no_any_other_ips_will_be_reset(), + }; + if config.reset_on_timeout { + w.rst_sys_after_timeout().enable(); + } else { + w.rst_sys_after_timeout().disable(); + } + w.wdtenbl_sig().enable() + }); + + Ok(()) + } + + /// Reload the counter, restarting the timeout window. + pub fn feed(&mut self) { + self.regs + .regs() + .wdt008() + .write(|w| unsafe { w.bits(RESTART_MAGIC) }); + } + + /// Stop the watchdog by clearing its enable bit. + pub fn disable(&mut self) { + self.regs + .regs() + .wdt00c() + .modify(|_, w| w.wdtenbl_sig().disable()); + } + + /// Report whether the watchdog counter has reached zero at least once. + #[must_use] + pub fn is_timeout(&self) -> bool { + self.regs + .regs() + .wdt010() + .read() + .indicate_timeout() + .is_timeout_occur() + } + + /// Clear the latched timeout / interrupt status. + pub fn clear_timeout(&mut self) { + self.regs.regs().wdt014().write(|w| unsafe { w.bits(0x01) }); + } +} diff --git a/target/ast10x0/peripherals/wdt/registers.rs b/target/ast10x0/peripherals/wdt/registers.rs new file mode 100644 index 000000000..5d89f026d --- /dev/null +++ b/target/ast10x0/peripherals/wdt/registers.rs @@ -0,0 +1,88 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! AST10x0 watchdog low-level register accessor. + +use ast1060_pac as device; +use core::marker::PhantomData; + +/// Safe wrapper around one AST10x0 watchdog register block. +/// +/// The SoC exposes four independent watchdog instances (`WDT0`–`WDT3`) at a +/// `0x80` stride; each is reached through its own constructor. +pub struct WdtRegisters { + base: *const device::wdt::RegisterBlock, + /// Prevent `Send` and `Sync`. + /// + /// MMIO register blocks must not be transferred across threads or + /// shared by reference due to potential side effects and lack of + /// synchronization guarantees. + _not_send_sync: PhantomData<*const ()>, +} + +impl WdtRegisters { + /// Create a register accessor from a raw watchdog register block pointer. + /// + /// # Safety + /// + /// - `base` must be a valid, non-null pointer to an AST1060 watchdog register block. + /// - The block must remain valid for the lifetime of this value. + /// - Caller must enforce exclusive (or otherwise coordinated) access to the + /// register block for the duration of use. + pub const unsafe fn new(base: *const device::wdt::RegisterBlock) -> Self { + Self { + base, + _not_send_sync: PhantomData, + } + } + + /// Create a register accessor for watchdog instance 0 (`0x7e78_5000`). + /// + /// # Safety + /// + /// Caller must ensure exclusive access to the singleton `WDT0` peripheral is + /// coordinated for the lifetime of this value. + pub unsafe fn new_wdt0() -> Self { + // SAFETY: Caller upholds the singleton access contract. + unsafe { Self::new(device::Wdt::ptr()) } + } + + /// Create a register accessor for watchdog instance 1 (`0x7e78_5080`). + /// + /// # Safety + /// + /// Caller must ensure exclusive access to the singleton `WDT1` peripheral is + /// coordinated for the lifetime of this value. + pub unsafe fn new_wdt1() -> Self { + // SAFETY: Caller upholds the singleton access contract. + unsafe { Self::new(device::Wdt1::ptr()) } + } + + /// Create a register accessor for watchdog instance 2 (`0x7e78_5100`). + /// + /// # Safety + /// + /// Caller must ensure exclusive access to the singleton `WDT2` peripheral is + /// coordinated for the lifetime of this value. + pub unsafe fn new_wdt2() -> Self { + // SAFETY: Caller upholds the singleton access contract. + unsafe { Self::new(device::Wdt2::ptr()) } + } + + /// Create a register accessor for watchdog instance 3 (`0x7e78_5180`). + /// + /// # Safety + /// + /// Caller must ensure exclusive access to the singleton `WDT3` peripheral is + /// coordinated for the lifetime of this value. + pub unsafe fn new_wdt3() -> Self { + // SAFETY: Caller upholds the singleton access contract. + unsafe { Self::new(device::Wdt3::ptr()) } + } + + #[inline] + pub(crate) fn regs(&self) -> &device::wdt::RegisterBlock { + // SAFETY: Constructor guarantees a valid, non-null register block pointer. + unsafe { &*self.base } + } +} diff --git a/target/ast10x0/peripherals/wdt/types.rs b/target/ast10x0/peripherals/wdt/types.rs new file mode 100644 index 000000000..54a658322 --- /dev/null +++ b/target/ast10x0/peripherals/wdt/types.rs @@ -0,0 +1,54 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! AST10x0 watchdog types and configuration. + +/// Frequency of the AST10x0 watchdog counter clock (1 MHz), so one reload tick +/// equals one microsecond. +pub const WDT_CLOCK_HZ: u32 = 1_000_000; + +/// Magic value written to the restart register to reload the counter. +pub(crate) const RESTART_MAGIC: u32 = 0x0000_4755; + +/// Watchdog driver errors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WdtError { + /// The requested timeout is zero or maps to a reload value that does not fit + /// in the 32-bit counter. + InvalidTimeout, +} + +/// Selects what the watchdog resets when the counter expires. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ResetMode { + /// Reset the SoC system, gated by the reset-mask registers. + #[default] + SocSystem, + /// Full-chip reset. + FullChip, + /// Reboot only the CPU/FMC firmware; other IPs keep running. + CpuFmcOnly, +} + +/// Watchdog start-up configuration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct WdtConfig { + /// Timeout window in milliseconds. + pub timeout_ms: u32, + /// Drive a hardware reset when the counter expires. When `false`, expiry + /// only latches the timeout status (and any enabled interrupt). + pub reset_on_timeout: bool, + /// What to reset when `reset_on_timeout` is set. + pub reset_mode: ResetMode, +} + +impl WdtConfig { + /// Convert the configured millisecond window into counter reload ticks. + pub(crate) fn reload_ticks(&self) -> Result { + let ticks = u64::from(self.timeout_ms) * u64::from(WDT_CLOCK_HZ / 1000); + if ticks == 0 || ticks > u64::from(u32::MAX) { + return Err(WdtError::InvalidTimeout); + } + Ok(ticks as u32) + } +} diff --git a/target/ast10x0/tests/peripherals/wdt/wdt_smoke/BUILD.bazel b/target/ast10x0/tests/peripherals/wdt/wdt_smoke/BUILD.bazel new file mode 100644 index 000000000..1e0c3e0ca --- /dev/null +++ b/target/ast10x0/tests/peripherals/wdt/wdt_smoke/BUILD.bazel @@ -0,0 +1,73 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +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") + +system_image( + name = "wdt_smoke", + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + userspace = False, +) + +system_image_test( + name = "wdt_smoke_test", + image = ":wdt_smoke", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":wdt_smoke", + tags = ["kernel"], +) + +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", + "//hal/blocking", + "//target/ast10x0:entry", + "//target/ast10x0/peripherals", + "@ast1060_pac", + "@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_log/rust:pw_log", + "@rust_crates//:cortex-m-semihosting", + ], +) diff --git a/target/ast10x0/tests/peripherals/wdt/wdt_smoke/system.json5 b/target/ast10x0/tests/peripherals/wdt/wdt_smoke/system.json5 new file mode 100644 index 000000000..66e54be80 --- /dev/null +++ b/target/ast10x0/tests/peripherals/wdt/wdt_smoke/system.json5 @@ -0,0 +1,17 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 Kernel watchdog smoke test configuration. +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, + }, + kernel: { + flash_start_address: 0x00000500, + flash_size_bytes: 262144, + ram_start_address: 0x00040500, + ram_size_bytes: 391936, // ends at RAM_NC boundary (0x000A0000) + }, +} diff --git a/target/ast10x0/tests/peripherals/wdt/wdt_smoke/target.rs b/target/ast10x0/tests/peripherals/wdt/wdt_smoke/target.rs new file mode 100644 index 000000000..0889f817e --- /dev/null +++ b/target/ast10x0/tests/peripherals/wdt/wdt_smoke/target.rs @@ -0,0 +1,129 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use ast10x0_peripherals::wdt::{ + ResetMode, Watchdog, WdtConfig, WdtError, WdtRegisters, WDT_CLOCK_HZ, +}; +use console_backend::console_backend_write_all; +use target_common::{declare_target, TargetInterface}; +use {codegen as _, entry as _}; + +pub struct Target {} + +fn run_smoke_test() -> bool { + pw_log::info!("=== AST10x0 watchdog smoke test ==="); + + // SAFETY: The test owns WDT0 access for its runtime. + let mut wdt = Watchdog::new(unsafe { WdtRegisters::new_wdt0() }); + + // A zero window cannot be programmed into the counter. + if wdt.start(WdtConfig { + timeout_ms: 0, + reset_on_timeout: false, + reset_mode: ResetMode::SocSystem, + }) != Err(WdtError::InvalidTimeout) + { + pw_log::error!("zero timeout was not rejected"); + return false; + } + + // Arm a 1s status-only watchdog (no hardware reset in QEMU). + if wdt + .start(WdtConfig { + timeout_ms: 1000, + reset_on_timeout: false, + reset_mode: ResetMode::SocSystem, + }) + .is_err() + { + pw_log::error!("start failed"); + return false; + } + + // SAFETY: The test owns WDT0 access for its runtime. + let regs = unsafe { &*ast1060_pac::Wdt::ptr() }; + + let expected_ticks = WDT_CLOCK_HZ / 1000 * 1000; + if regs.wdt004().read().bits() != expected_ticks { + pw_log::error!("reload value not programmed"); + return false; + } + + let ctrl = regs.wdt00c().read(); + if !ctrl.wdtenbl_sig().is_enable() { + pw_log::error!("enable bit not set after start"); + return false; + } + if !ctrl.rst_sys_after_timeout().is_disable() { + pw_log::error!("reset-on-timeout should be disabled"); + return false; + } + if !ctrl + .rst_sys_mode() + .is_soc_system_ewvergated_by_reset_mask_registers() + { + pw_log::error!("reset mode not SoC-system"); + return false; + } + + if wdt.is_timeout() { + pw_log::error!("timeout latched immediately after start"); + return false; + } + + // Feeding must not fault and must leave the watchdog running. + wdt.feed(); + if !regs.wdt00c().read().wdtenbl_sig().is_enable() { + pw_log::error!("feed disturbed the enable bit"); + return false; + } + + // Re-arm requesting a full-chip reset and confirm the mode bits. + if wdt + .start(WdtConfig { + timeout_ms: 500, + reset_on_timeout: true, + reset_mode: ResetMode::FullChip, + }) + .is_err() + { + pw_log::error!("re-arm failed"); + return false; + } + let ctrl = regs.wdt00c().read(); + if !ctrl.rst_sys_after_timeout().is_enable() || !ctrl.rst_sys_mode().is_full_chip() { + pw_log::error!("reset configuration mismatch"); + return false; + } + + // Disabling must clear the enable bit so no reset can fire. + wdt.disable(); + if regs.wdt00c().read().wdtenbl_sig().is_enable() { + pw_log::error!("disable did not clear the enable bit"); + return false; + } + + pw_log::info!("=== AST10x0 watchdog smoke test complete ==="); + true +} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 watchdog smoke test"; + + fn main() -> ! { + let sentinel = if run_smoke_test() { + 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);