From 0fde0b582c256ee9504b1b7040ca0649d0b3eed2 Mon Sep 17 00:00:00 2001 From: ppannuto-claude Date: Tue, 25 Aug 2026 23:07:49 -0700 Subject: [PATCH 1/6] Add QEMU ARM MPS2 AN385/AN386 boards (Cortex-M3/M4) Tock has QEMU-backed RISC-V and x86 boards but no ARM one, making it hard to test the Cortex-M port without real hardware. This adds two new boards targeting QEMU's `mps2-an385` (Cortex-M3) and `mps2-an386` (Cortex-M4) machines: emulations of ARM's own CMSDK reference platform, in the same spirit as the existing `qemu_rv32_virt`/`qemu_rv64_virt` boards (a stable virtual target, not a real vendor chip). New `chips/qemu_arm_mps2_chip` crate, shared by both boards (an385/an386 differ only in CPU core): - CMSDK APB UART driver (console/debug UART) - CMSDK APB Timer driven as the kernel's Alarm/Time HIL, by keeping RELOAD fixed at u32::MAX so the hardware free-runs and re-arming an alarm just shortens the current countdown by writing VALUE directly - LEDs via the FPGAIO block's LED0 register, not GPIO: QEMU emulates all four CMSDK AHB GPIO banks on this whole machine family (an385/386/500/511 and even the Cortex-M33 TrustZone an505/an521) as inert stubs that discard writes and always read 0, so pin state is never observable under this QEMU model. FPGAIO is genuinely emulated, so that's the only place LED state is real. - Vector table setup lives here (as concrete, non-generic per-core modules gated by cortex-m3/cortex-m4 Cargo features), not in the board crates: putting it in a board crate collides at LTO time with components::process_console's own `_estack` extern declaration (fn-typed vs data-typed references to the same linker symbol landing in one compilation unit), because the generic QemuArmMps2Chip::print_state gets monomorphized into whichever crate first instantiates a concrete CortexMVariant. Also required adding `thumbv7m-none-eabi` to rust-toolchain.toml (an385 is the first Cortex-M3 target in the workspace), widening the CI QEMU build's --target-list to include arm-softmmu, and wiring both boards into tools/ci/qemu-runner. Both boards verified booting under QEMU 10.2.1 (console, debug writer, process console, and the timer-backed alarm all confirmed working via an interactive `list` command over the emulated UART). Co-Authored-By: Claude Sonnet 5 --- Cargo.lock | 40 +++ Cargo.toml | 3 + Makefile | 2 +- boards/README.md | 2 + boards/qemu_arm_mps2_an385/.cargo/config.toml | 11 + boards/qemu_arm_mps2_an385/Cargo.toml | 27 ++ boards/qemu_arm_mps2_an385/Makefile | 30 ++ boards/qemu_arm_mps2_an385/README.md | 69 ++++ boards/qemu_arm_mps2_an385/chip_layout.ld | 26 ++ boards/qemu_arm_mps2_an385/layout.ld | 6 + boards/qemu_arm_mps2_an385/src/io.rs | 30 ++ boards/qemu_arm_mps2_an385/src/main.rs | 256 +++++++++++++ boards/qemu_arm_mps2_an386/.cargo/config.toml | 11 + boards/qemu_arm_mps2_an386/Cargo.toml | 27 ++ boards/qemu_arm_mps2_an386/Makefile | 30 ++ boards/qemu_arm_mps2_an386/README.md | 37 ++ boards/qemu_arm_mps2_an386/chip_layout.ld | 26 ++ boards/qemu_arm_mps2_an386/layout.ld | 6 + boards/qemu_arm_mps2_an386/src/io.rs | 30 ++ boards/qemu_arm_mps2_an386/src/main.rs | 256 +++++++++++++ chips/qemu_arm_mps2_chip/Cargo.toml | 30 ++ chips/qemu_arm_mps2_chip/src/chip.rs | 99 +++++ chips/qemu_arm_mps2_chip/src/interrupts.rs | 24 ++ chips/qemu_arm_mps2_chip/src/led.rs | 78 ++++ chips/qemu_arm_mps2_chip/src/lib.rs | 62 ++++ chips/qemu_arm_mps2_chip/src/timer.rs | 182 ++++++++++ chips/qemu_arm_mps2_chip/src/uart.rs | 339 ++++++++++++++++++ chips/qemu_arm_mps2_chip/src/vectors_m3.rs | 61 ++++ chips/qemu_arm_mps2_chip/src/vectors_m4.rs | 53 +++ rust-toolchain.toml | 1 + tools/ci/qemu-runner/src/main.rs | 60 ++++ 31 files changed, 1913 insertions(+), 1 deletion(-) create mode 100644 boards/qemu_arm_mps2_an385/.cargo/config.toml create mode 100644 boards/qemu_arm_mps2_an385/Cargo.toml create mode 100644 boards/qemu_arm_mps2_an385/Makefile create mode 100644 boards/qemu_arm_mps2_an385/README.md create mode 100644 boards/qemu_arm_mps2_an385/chip_layout.ld create mode 100644 boards/qemu_arm_mps2_an385/layout.ld create mode 100644 boards/qemu_arm_mps2_an385/src/io.rs create mode 100644 boards/qemu_arm_mps2_an385/src/main.rs create mode 100644 boards/qemu_arm_mps2_an386/.cargo/config.toml create mode 100644 boards/qemu_arm_mps2_an386/Cargo.toml create mode 100644 boards/qemu_arm_mps2_an386/Makefile create mode 100644 boards/qemu_arm_mps2_an386/README.md create mode 100644 boards/qemu_arm_mps2_an386/chip_layout.ld create mode 100644 boards/qemu_arm_mps2_an386/layout.ld create mode 100644 boards/qemu_arm_mps2_an386/src/io.rs create mode 100644 boards/qemu_arm_mps2_an386/src/main.rs create mode 100644 chips/qemu_arm_mps2_chip/Cargo.toml create mode 100644 chips/qemu_arm_mps2_chip/src/chip.rs create mode 100644 chips/qemu_arm_mps2_chip/src/interrupts.rs create mode 100644 chips/qemu_arm_mps2_chip/src/led.rs create mode 100644 chips/qemu_arm_mps2_chip/src/lib.rs create mode 100644 chips/qemu_arm_mps2_chip/src/timer.rs create mode 100644 chips/qemu_arm_mps2_chip/src/uart.rs create mode 100644 chips/qemu_arm_mps2_chip/src/vectors_m3.rs create mode 100644 chips/qemu_arm_mps2_chip/src/vectors_m4.rs diff --git a/Cargo.lock b/Cargo.lock index 8a6bd64536b..0f3e2f97514 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,46 @@ dependencies = [ "kernel", ] +[[package]] +name = "qemu_arm_mps2_an385" +version = "0.2.3-dev" +dependencies = [ + "capsules-core", + "capsules-extra", + "capsules-system", + "components", + "cortexm", + "cortexm3", + "kernel", + "qemu_arm_mps2_chip", + "tock_build_scripts", +] + +[[package]] +name = "qemu_arm_mps2_an386" +version = "0.2.3-dev" +dependencies = [ + "capsules-core", + "capsules-extra", + "capsules-system", + "components", + "cortexm", + "cortexm4", + "kernel", + "qemu_arm_mps2_chip", + "tock_build_scripts", +] + +[[package]] +name = "qemu_arm_mps2_chip" +version = "0.2.3-dev" +dependencies = [ + "cortexm", + "cortexm3", + "cortexm4", + "kernel", +] + [[package]] name = "qemu_i486_q35" version = "0.2.3-dev" diff --git a/Cargo.toml b/Cargo.toml index 5780cb36381..3efa64e022e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,8 @@ members = [ "boards/teensy40", "boards/nano33ble", "boards/nano33ble_rev2", + "boards/qemu_arm_mps2_an385", + "boards/qemu_arm_mps2_an386", "boards/qemu_i486_q35", "boards/qemu_rv32_virt", "boards/qemu_rv64_virt", @@ -104,6 +106,7 @@ members = [ "chips/nrf5x-unsafe", "chips/pci-x86", "chips/x86_q35", + "chips/qemu_arm_mps2_chip", "chips/qemu_rv32_virt_chip", "chips/qemu_virt_chip", "chips/psc3", diff --git a/Makefile b/Makefile index c9113b29180..69f250b7289 100644 --- a/Makefile +++ b/Makefile @@ -626,7 +626,7 @@ define ci_setup_qemu_riscv @# Use the latest QEMU as it has OpenTitan support @printf "Building QEMU, this could take a few minutes\n\n" @git clone https://github.com/qemu/qemu ./tools/ci/qemu 2>/dev/null || echo "qemu already cloned, checking out" - @cd tools/ci/qemu; git checkout ${QEMU_COMMIT_HASH}; ../qemu/configure --target-list=riscv32-softmmu --disable-linux-io-uring --disable-libdaxctl; + @cd tools/ci/qemu; git checkout ${QEMU_COMMIT_HASH}; ../qemu/configure --target-list=riscv32-softmmu,arm-softmmu --disable-linux-io-uring --disable-libdaxctl; @# Build qemu @$(MAKE) -C "tools/ci/qemu/build" -j2 || (echo "You might need to install some missing packages" || exit 127) endef diff --git a/boards/README.md b/boards/README.md index 7a086003414..f0e1faffe1c 100644 --- a/boards/README.md +++ b/boards/README.md @@ -118,6 +118,8 @@ Virtual hardware platforms that are regularly tested as part of the CI. |-------------------------------------------------------------------|------------------|----------------|------------|-----------------------------|---------------| | [QEMU RISC-V 32 bit `virt` platform](qemu_rv32_virt/README.md) | RISC-V RV32IMAC | QEMU | custom | custom | Yes (7.2.0) | | [QEMU RISC-V 64 bit `virt` platform](qemu_rv64_virt/README.md) | RISC-V RV64IMAC | QEMU | custom | custom | Yes | +| [QEMU ARM MPS2 AN385](qemu_arm_mps2_an385/README.md) | ARM Cortex-M3 | QEMU | custom | custom | Yes (10.2.1) | +| [QEMU ARM MPS2 AN386](qemu_arm_mps2_an386/README.md) | ARM Cortex-M4 | QEMU | custom | custom | Yes (10.2.1) | | [LiteX on Digilent Arty A-7](litex/arty/README.md) | RISC-V RV32IMC | LiteX+VexRiscV | custom | tockloader (flash-file)[^1] | No | | [Verilated LiteX Simulation](litex/sim/README.md) | RISC-V RV32IMC | LiteX+VexRiscv | custom | tockloader (flash-file)[^1] | No | | [VeeR EL2 simulation](veer_el2_sim/README.md) | RISC-V RV32IMC | VeeR EL2 | custom | custom | No | diff --git a/boards/qemu_arm_mps2_an385/.cargo/config.toml b/boards/qemu_arm_mps2_an385/.cargo/config.toml new file mode 100644 index 00000000000..3129f42c25e --- /dev/null +++ b/boards/qemu_arm_mps2_an385/.cargo/config.toml @@ -0,0 +1,11 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2026. + +include = [ + "../../cargo/tock_flags.toml", + "../../cargo/unstable_flags.toml", +] + +[build] +target = "thumbv7m-none-eabi" diff --git a/boards/qemu_arm_mps2_an385/Cargo.toml b/boards/qemu_arm_mps2_an385/Cargo.toml new file mode 100644 index 00000000000..99e8a3aebe2 --- /dev/null +++ b/boards/qemu_arm_mps2_an385/Cargo.toml @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2026. + +[package] +name = "qemu_arm_mps2_an385" +version.workspace = true +authors.workspace = true +build = "../build.rs" +edition.workspace = true + +[dependencies] +components = { path = "../components" } +cortexm = { path = "../../arch/cortex-m" } +cortexm3 = { path = "../../arch/cortex-m3" } +kernel = { path = "../../kernel" } +qemu_arm_mps2_chip = { path = "../../chips/qemu_arm_mps2_chip", features = ["cortex-m3"] } + +capsules-core = { path = "../../capsules/core" } +capsules-extra = { path = "../../capsules/extra" } +capsules-system = { path = "../../capsules/system" } + +[build-dependencies] +tock_build_scripts = { path = "../build_scripts" } + +[lints] +workspace = true diff --git a/boards/qemu_arm_mps2_an385/Makefile b/boards/qemu_arm_mps2_an385/Makefile new file mode 100644 index 00000000000..357a515cc2c --- /dev/null +++ b/boards/qemu_arm_mps2_an385/Makefile @@ -0,0 +1,30 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2026. + +# Makefile for building the Tock kernel for the qemu-system-arm `mps2-an385` +# (Cortex-M3) platform / machine type. + +include ../Makefile.common + +QEMU_CMD := qemu-system-arm + +# Peripherals attached by default: +# - CMSDK APB UART0 (attached to stdio) +QEMU_BASE_CMDLINE := \ + $(QEMU_CMD) \ + -machine mps2-an385 \ + -nographic + +# Run the kernel inside a qemu-system-arm "mps2-an385" machine simulation. +.PHONY: run +run: $(TOCK_ROOT_DIRECTORY)target/$(TARGET)/release/$(PLATFORM).elf + @echo + @echo -e "Running $$($(QEMU_CMD) --version | head -n1) with\n"\ + " - kernel $<" + @echo "To exit type C-a x" + @echo + $(QEMU_BASE_CMDLINE) -kernel $< + +.PHONY: qemu +qemu: run diff --git a/boards/qemu_arm_mps2_an385/README.md b/boards/qemu_arm_mps2_an385/README.md new file mode 100644 index 00000000000..6d75c131c6a --- /dev/null +++ b/boards/qemu_arm_mps2_an385/README.md @@ -0,0 +1,69 @@ +QEMU ARM MPS2 AN385 (Cortex-M3) Platform +========================================= + +This board crate targets QEMU's `mps2-an385` machine: an emulation of ARM's +own "MPS2 + AN385" Cortex-M System Design Kit (CMSDK) reference platform, +not a real vendor chip. It is the ARM counterpart to `qemu_rv32_virt` / +`qemu_rv64_virt`: a stable, purely virtual target for exercising the +Cortex-M port under QEMU, useful for CI and kernel development without +access to real ARM hardware. + +Currently supported peripherals: + +- One CMSDK APB UART (of the five present on the machine), used as the + console/debug UART. +- One CMSDK APB Timer, backing the kernel's `Alarm`/`Time` HIL. +- The `FPGAIO` block's `LED0` register, exposing the machine's two + simulated LEDs. + +Not supported, and not planned for this machine specifically: + +- **GPIO.** QEMU emulates all four CMSDK AHB GPIO banks on this machine (and + on every other MPS2/MPS2-TZ machine, including the Cortex-M33 `an505`/ + `an521` images) as inert stubs: writes are discarded and reads always + return 0 (see QEMU's `hw/arm/mps2.c`, `create_unimplemented_device(..., + "cmsdk-ahb-gpio", ...)`). There is no way to observe pin state changes + under this QEMU machine, so this board does not implement a GPIO capsule. + LEDs are wired to the separate, genuinely-emulated `FPGAIO` register + instead (see `chips/qemu_arm_mps2_chip/src/led.rs`). +- SPI, I2C, and the machine's LAN9118 Ethernet controller: present on the + memory map but not driven by this chip crate. + +See also `qemu_arm_mps2_an386`, the Cortex-M4 sibling of this board: same +peripheral map (an385/an386 differ only in CPU core), sharing this same +`qemu_arm_mps2_chip` crate. + +Running QEMU +------------ + +To run the board in QEMU, `qemu-system-arm` must be started with the +`-machine mps2-an385` argument and `-kernel $TOCK_KERNEL.elf`. Unlike the +RISC-V `virt` boards, QEMU loads and executes a Cortex-M ELF directly from +its vector table at address 0; no bootloader or `-bios` indirection is +needed. `-nographic` suppresses QEMU's graphical window (there is no display +device on this machine to show regardless). + +- **`run`**: Start Tock on an emulated QEMU board: + + ``` + $ make run + [...] + text data bss dec hex filename + 57388 0 13356 70744 11458 target/thumbv7m-none-eabi/release/qemu_arm_mps2_an385 + + Running QEMU emulator version 10.2.1 with + - kernel target/thumbv7m-none-eabi/release/qemu_arm_mps2_an385.elf + To exit type C-a x + + QEMU MPS2 AN385 (Cortex-M3) initialization complete. + Entering main loop. + tock$ + ``` + +With the default linker script, this board loads processes from +flash=0x00040000-0x0007FFFF into ram=0x21004000-0x2101FFFF (RAM above the +kernel's own static allocations). Kernel and app flash are both well within +QEMU's hard 4 MiB cap for code at address 0 (`armv7m_load_kernel(..., 0, +0x400000)` in `hw/arm/mps2.c`); RAM is a modest slice of the 16 MiB QEMU +always backs at 0x21000000 regardless of what a board's linker script +claims. diff --git a/boards/qemu_arm_mps2_an385/chip_layout.ld b/boards/qemu_arm_mps2_an385/chip_layout.ld new file mode 100644 index 00000000000..83f95a67a59 --- /dev/null +++ b/boards/qemu_arm_mps2_an385/chip_layout.ld @@ -0,0 +1,26 @@ +/* Licensed under the Apache License, Version 2.0 or the MIT License. */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ +/* Copyright Tock Contributors 2026. */ + +/* Memory layout for the QEMU ARM MPS2 AN385 (Cortex-M3) machine. + * + * QEMU maps code/flash (SSRAM1) at 0x0, up to a hard 4 MiB cap + * (`armv7m_load_kernel(..., 0, 0x400000)` in `hw/arm/mps2.c`), and RAM at + * 0x21000000, backed by a fixed 16 MiB (`default_ram_size` in the same + * file; QEMU errors if `-m` is overridden). We only claim a modest slice + * of each; QEMU backs whatever the linker script declares regardless of + * the machine's full capacity. + * + * rom = 256KB (kernel) + * prog = 256KB (apps) + * ram = 128KB + */ + +MEMORY +{ + rom (rx) : ORIGIN = 0x00000000, LENGTH = 0x00040000 + prog (rx) : ORIGIN = 0x00040000, LENGTH = 0x00040000 + ram (rwx) : ORIGIN = 0x21000000, LENGTH = 0x00020000 +} + +PAGE_SIZE = 2K; diff --git a/boards/qemu_arm_mps2_an385/layout.ld b/boards/qemu_arm_mps2_an385/layout.ld new file mode 100644 index 00000000000..76b8c289841 --- /dev/null +++ b/boards/qemu_arm_mps2_an385/layout.ld @@ -0,0 +1,6 @@ +/* Licensed under the Apache License, Version 2.0 or the MIT License. */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ +/* Copyright Tock Contributors 2026. */ + +INCLUDE ./chip_layout.ld +INCLUDE tock_kernel_layout.ld diff --git a/boards/qemu_arm_mps2_an385/src/io.rs b/boards/qemu_arm_mps2_an385/src/io.rs new file mode 100644 index 00000000000..42c45f2a2a3 --- /dev/null +++ b/boards/qemu_arm_mps2_an385/src/io.rs @@ -0,0 +1,30 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +use core::panic::PanicInfo; + +use kernel::debug; +use kernel::hil::uart; + +/// Panic handler. +#[panic_handler] +pub unsafe fn panic_fmt(info: &PanicInfo) -> ! { + debug::panic_print::( + qemu_arm_mps2_chip::uart::UartPanicWriterConfig { + base: qemu_arm_mps2_chip::uart::UART0_BASE, + params: uart::Parameters { + baud_rate: 115200, + stop_bits: uart::StopBits::One, + parity: uart::Parity::None, + hw_flow_control: false, + width: uart::Width::Eight, + }, + }, + info, + &cortexm3::support::nop, + crate::PANIC_RESOURCES.get(), + ); + + loop {} +} diff --git a/boards/qemu_arm_mps2_an385/src/main.rs b/boards/qemu_arm_mps2_an385/src/main.rs new file mode 100644 index 00000000000..594494907ff --- /dev/null +++ b/boards/qemu_arm_mps2_an385/src/main.rs @@ -0,0 +1,256 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! Tock kernel for the QEMU ARM MPS2 AN385 (Cortex-M3) machine. +//! +//! This is a purely virtual platform: ARM's own CMSDK reference design, as +//! emulated by QEMU, not a real vendor chip. See `chips/qemu_arm_mps2_chip` +//! for the peripheral drivers and `README.md` for what is and is not +//! emulated (notably: GPIO pin state is not observable under this QEMU +//! machine, so this board does not expose a GPIO capsule; LEDs are +//! implemented against the separate, genuinely-emulated FPGAIO block). + +#![no_std] +#![no_main] + +use kernel::capabilities; +use kernel::component::Component; +use kernel::debug::PanicResources; +use kernel::platform::chip::Chip; +use kernel::platform::{KernelResources, SyscallDriverLookup}; +use kernel::utilities::single_thread_value::SingleThreadValue; +use kernel::{create_capability, static_init}; + +pub mod io; + +const NUM_PROCS: usize = 4; + +type ChipHw = qemu_arm_mps2_chip::chip::QemuArmMps2Chip< + 'static, + cortexm3::CortexM3, + qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, +>; +type ProcessPrinterInUse = capsules_system::process_printer::ProcessPrinterText; +type SchedulerInUse = components::sched::round_robin::RoundRobinComponentType; + +static PANIC_RESOURCES: SingleThreadValue> = + SingleThreadValue::new(); + +kernel::stack_size! {0x2000} + +struct QemuArmMps2An385 { + console: &'static capsules_core::console::Console<'static>, + scheduler: &'static SchedulerInUse, + systick: cortexm3::systick::SysTick, + led: &'static capsules_core::led::LedDriver< + 'static, + qemu_arm_mps2_chip::led::Led<'static>, + { qemu_arm_mps2_chip::led::NUM_LEDS as usize }, + >, + alarm: &'static capsules_core::alarm::AlarmDriver< + 'static, + capsules_core::virtualizers::virtual_alarm::VirtualMuxAlarm< + 'static, + qemu_arm_mps2_chip::timer::Timer<'static>, + >, + >, +} + +impl SyscallDriverLookup for QemuArmMps2An385 { + fn with_driver(&self, driver_num: usize, f: F) -> R + where + F: FnOnce(Option<&dyn kernel::syscall::SyscallDriver>) -> R, + { + match driver_num { + capsules_core::console::DRIVER_NUM => f(Some(self.console)), + capsules_core::led::DRIVER_NUM => f(Some(self.led)), + capsules_core::alarm::DRIVER_NUM => f(Some(self.alarm)), + _ => f(None), + } + } +} + +impl KernelResources for QemuArmMps2An385 { + type SyscallDriverLookup = Self; + type SyscallFilter = (); + type ProcessFault = (); + type Scheduler = SchedulerInUse; + type SchedulerTimer = cortexm3::systick::SysTick; + type WatchDog = (); + type ContextSwitchCallback = (); + + fn syscall_driver_lookup(&self) -> &Self::SyscallDriverLookup { + self + } + fn syscall_filter(&self) -> &Self::SyscallFilter { + &() + } + fn process_fault(&self) -> &Self::ProcessFault { + &() + } + fn scheduler(&self) -> &Self::Scheduler { + self.scheduler + } + fn scheduler_timer(&self) -> &Self::SchedulerTimer { + &self.systick + } + fn watchdog(&self) -> &Self::WatchDog { + &() + } + fn context_switch_callback(&self) -> &Self::ContextSwitchCallback { + &() + } +} + +#[inline(never)] +unsafe fn start() -> ( + &'static kernel::Kernel, + &'static QemuArmMps2An385, + &'static ChipHw, +) { + ChipHw::init(); + + kernel::deferred_call::initialize_deferred_call_state::< + ::ThreadIdProvider, + >(); + + let _ = PANIC_RESOURCES + .bind_to_thread::<::ThreadIdProvider>( + PanicResources::new(), + ); + + let peripherals = static_init!( + qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, + qemu_arm_mps2_chip::Mps2DefaultPeripherals::new() + ); + + let processes = components::process_array::ProcessArrayComponent::new() + .finalize(components::process_array_component_static!(NUM_PROCS)); + let board_kernel = static_init!(kernel::Kernel, kernel::Kernel::new(processes.as_slice())); + + let chip = static_init!(ChipHw, ChipHw::new(peripherals)); + + let uart_mux = components::console::UartMuxComponent::new(&peripherals.uart0, 115200) + .finalize(components::uart_mux_component_static!()); + + let console = components::console::ConsoleComponent::new( + board_kernel, + capsules_core::console::DRIVER_NUM, + uart_mux, + create_capability!(capabilities::MemoryAllocationCapability), + ) + .finalize(components::console_component_static!()); + + components::debug_writer::DebugWriterComponent::new::< + ::ThreadIdProvider, + >( + uart_mux, + create_capability!(capabilities::SetDebugWriterCapability), + ) + .finalize(components::debug_writer_component_static!()); + + let alarm_mux = components::alarm::AlarmMuxComponent::new(&peripherals.timer0).finalize( + components::alarm_mux_component_static!(qemu_arm_mps2_chip::timer::Timer), + ); + + let alarm = components::alarm::AlarmDriverComponent::new( + board_kernel, + capsules_core::alarm::DRIVER_NUM, + alarm_mux, + create_capability!(capabilities::MemoryAllocationCapability), + ) + .finalize(components::alarm_component_static!( + qemu_arm_mps2_chip::timer::Timer + )); + + kernel::create_typed_capability!(process_console_cap, ProcessConsoleCap: + kernel::capabilities::ProcessManagementCapability, + kernel::capabilities::ProcessStartCapability + ); + let process_console = components::process_console::ProcessConsoleComponent::new( + board_kernel, + uart_mux, + alarm_mux, + components::process_printer::ProcessPrinterTextComponent::new() + .finalize(components::process_printer_text_component_static!()), + None, + process_console_cap, + ) + .finalize(components::process_console_component_static!( + qemu_arm_mps2_chip::timer::Timer, + ProcessConsoleCap + )); + let _ = process_console.start(); + + let led = components::led::LedsComponent::new().finalize(components::led_component_static!( + qemu_arm_mps2_chip::led::Led<'static>, + peripherals.fpgaio.led(0), + peripherals.fpgaio.led(1), + )); + + let scheduler = components::sched::round_robin::RoundRobinComponent::new(processes) + .finalize(components::round_robin_component_static!(NUM_PROCS)); + + let platform = static_init!( + QemuArmMps2An385, + QemuArmMps2An385 { + console, + scheduler, + systick: cortexm3::systick::SysTick::new(), + led, + alarm, + } + ); + + extern "C" { + /// Beginning of the ROM region containing app images. + static _sapps: u8; + /// End of the ROM region containing app images. + static _eapps: u8; + /// Beginning of the RAM region for app memory. + static mut _sappmem: u8; + /// End of the RAM region for app memory. + static _eappmem: u8; + } + + let process_management_capability = + create_capability!(capabilities::ProcessManagementCapability); + kernel::process::load_processes( + board_kernel, + chip, + core::slice::from_raw_parts( + core::ptr::addr_of!(_sapps), + core::ptr::addr_of!(_eapps) as usize - core::ptr::addr_of!(_sapps) as usize, + ), + core::slice::from_raw_parts_mut( + core::ptr::addr_of_mut!(_sappmem), + core::ptr::addr_of!(_eappmem) as usize - core::ptr::addr_of!(_sappmem) as usize, + ), + &capsules_system::process_policies::PanicFaultPolicy {}, + &process_management_capability, + ) + .unwrap_or_else(|err| { + kernel::debug!("Error loading processes!"); + kernel::debug!("{:?}", err); + }); + + (board_kernel, platform, chip) +} + +/// Main function called after RAM initialized. +#[no_mangle] +pub unsafe fn main() { + let (board_kernel, platform, chip) = start(); + + kernel::debug!("QEMU MPS2 AN385 (Cortex-M3) initialization complete."); + kernel::debug!("Entering main loop."); + + let main_loop_capability = create_capability!(capabilities::MainLoopCapability); + board_kernel.kernel_loop::( + platform, + chip, + None, + &main_loop_capability, + ); +} diff --git a/boards/qemu_arm_mps2_an386/.cargo/config.toml b/boards/qemu_arm_mps2_an386/.cargo/config.toml new file mode 100644 index 00000000000..c217a3ded70 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/.cargo/config.toml @@ -0,0 +1,11 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2026. + +include = [ + "../../cargo/tock_flags.toml", + "../../cargo/unstable_flags.toml", +] + +[build] +target = "thumbv7em-none-eabi" diff --git a/boards/qemu_arm_mps2_an386/Cargo.toml b/boards/qemu_arm_mps2_an386/Cargo.toml new file mode 100644 index 00000000000..dc8f751e09c --- /dev/null +++ b/boards/qemu_arm_mps2_an386/Cargo.toml @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2026. + +[package] +name = "qemu_arm_mps2_an386" +version.workspace = true +authors.workspace = true +build = "../build.rs" +edition.workspace = true + +[dependencies] +components = { path = "../components" } +cortexm = { path = "../../arch/cortex-m" } +cortexm4 = { path = "../../arch/cortex-m4" } +kernel = { path = "../../kernel" } +qemu_arm_mps2_chip = { path = "../../chips/qemu_arm_mps2_chip", features = ["cortex-m4"] } + +capsules-core = { path = "../../capsules/core" } +capsules-extra = { path = "../../capsules/extra" } +capsules-system = { path = "../../capsules/system" } + +[build-dependencies] +tock_build_scripts = { path = "../build_scripts" } + +[lints] +workspace = true diff --git a/boards/qemu_arm_mps2_an386/Makefile b/boards/qemu_arm_mps2_an386/Makefile new file mode 100644 index 00000000000..72bd629afa9 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/Makefile @@ -0,0 +1,30 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2026. + +# Makefile for building the Tock kernel for the qemu-system-arm `mps2-an386` +# (Cortex-M4) platform / machine type. + +include ../Makefile.common + +QEMU_CMD := qemu-system-arm + +# Peripherals attached by default: +# - CMSDK APB UART0 (attached to stdio) +QEMU_BASE_CMDLINE := \ + $(QEMU_CMD) \ + -machine mps2-an386 \ + -nographic + +# Run the kernel inside a qemu-system-arm "mps2-an386" machine simulation. +.PHONY: run +run: $(TOCK_ROOT_DIRECTORY)target/$(TARGET)/release/$(PLATFORM).elf + @echo + @echo -e "Running $$($(QEMU_CMD) --version | head -n1) with\n"\ + " - kernel $<" + @echo "To exit type C-a x" + @echo + $(QEMU_BASE_CMDLINE) -kernel $< + +.PHONY: qemu +qemu: run diff --git a/boards/qemu_arm_mps2_an386/README.md b/boards/qemu_arm_mps2_an386/README.md new file mode 100644 index 00000000000..58f15183d80 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/README.md @@ -0,0 +1,37 @@ +QEMU ARM MPS2 AN386 (Cortex-M4) Platform +========================================= + +This board crate targets QEMU's `mps2-an386` machine: the Cortex-M4 image of +ARM's "MPS2" Cortex-M System Design Kit (CMSDK) reference platform. It is +identical to `qemu_arm_mps2_an385` in every respect except the CPU core +(an385/an386 share the same `hw/arm/mps2.c` machine init and peripheral +map in QEMU, and this board shares the same `qemu_arm_mps2_chip` crate) — +see that board's README for the full peripheral list and the GPIO +limitation shared by both. + +This board uses the soft-float `cortexm4` architecture crate (not +`cortexm4f`), matching the convention of every other Tock Cortex-M4 board +in-tree, even on FPU-capable silicon. + +Running QEMU +------------ + +- **`run`**: Start Tock on an emulated QEMU board: + + ``` + $ make run + [...] + text data bss dec hex filename + 57388 0 13356 70744 11458 target/thumbv7em-none-eabi/release/qemu_arm_mps2_an386 + + Running QEMU emulator version 10.2.1 with + - kernel target/thumbv7em-none-eabi/release/qemu_arm_mps2_an386.elf + To exit type C-a x + + QEMU MPS2 AN386 (Cortex-M4) initialization complete. + Entering main loop. + tock$ + ``` + +See `qemu_arm_mps2_an385/README.md` for the memory layout (identical +addresses on both machines) and app-loading details. diff --git a/boards/qemu_arm_mps2_an386/chip_layout.ld b/boards/qemu_arm_mps2_an386/chip_layout.ld new file mode 100644 index 00000000000..83f95a67a59 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/chip_layout.ld @@ -0,0 +1,26 @@ +/* Licensed under the Apache License, Version 2.0 or the MIT License. */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ +/* Copyright Tock Contributors 2026. */ + +/* Memory layout for the QEMU ARM MPS2 AN385 (Cortex-M3) machine. + * + * QEMU maps code/flash (SSRAM1) at 0x0, up to a hard 4 MiB cap + * (`armv7m_load_kernel(..., 0, 0x400000)` in `hw/arm/mps2.c`), and RAM at + * 0x21000000, backed by a fixed 16 MiB (`default_ram_size` in the same + * file; QEMU errors if `-m` is overridden). We only claim a modest slice + * of each; QEMU backs whatever the linker script declares regardless of + * the machine's full capacity. + * + * rom = 256KB (kernel) + * prog = 256KB (apps) + * ram = 128KB + */ + +MEMORY +{ + rom (rx) : ORIGIN = 0x00000000, LENGTH = 0x00040000 + prog (rx) : ORIGIN = 0x00040000, LENGTH = 0x00040000 + ram (rwx) : ORIGIN = 0x21000000, LENGTH = 0x00020000 +} + +PAGE_SIZE = 2K; diff --git a/boards/qemu_arm_mps2_an386/layout.ld b/boards/qemu_arm_mps2_an386/layout.ld new file mode 100644 index 00000000000..76b8c289841 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/layout.ld @@ -0,0 +1,6 @@ +/* Licensed under the Apache License, Version 2.0 or the MIT License. */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ +/* Copyright Tock Contributors 2026. */ + +INCLUDE ./chip_layout.ld +INCLUDE tock_kernel_layout.ld diff --git a/boards/qemu_arm_mps2_an386/src/io.rs b/boards/qemu_arm_mps2_an386/src/io.rs new file mode 100644 index 00000000000..177bb09226c --- /dev/null +++ b/boards/qemu_arm_mps2_an386/src/io.rs @@ -0,0 +1,30 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +use core::panic::PanicInfo; + +use kernel::debug; +use kernel::hil::uart; + +/// Panic handler. +#[panic_handler] +pub unsafe fn panic_fmt(info: &PanicInfo) -> ! { + debug::panic_print::( + qemu_arm_mps2_chip::uart::UartPanicWriterConfig { + base: qemu_arm_mps2_chip::uart::UART0_BASE, + params: uart::Parameters { + baud_rate: 115200, + stop_bits: uart::StopBits::One, + parity: uart::Parity::None, + hw_flow_control: false, + width: uart::Width::Eight, + }, + }, + info, + &cortexm4::support::nop, + crate::PANIC_RESOURCES.get(), + ); + + loop {} +} diff --git a/boards/qemu_arm_mps2_an386/src/main.rs b/boards/qemu_arm_mps2_an386/src/main.rs new file mode 100644 index 00000000000..be528229ae5 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/src/main.rs @@ -0,0 +1,256 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! Tock kernel for the QEMU ARM MPS2 AN386 (Cortex-M4) machine. +//! +//! This is a purely virtual platform: ARM's own CMSDK reference design, as +//! emulated by QEMU, not a real vendor chip. See `chips/qemu_arm_mps2_chip` +//! for the peripheral drivers and `README.md` for what is and is not +//! emulated (notably: GPIO pin state is not observable under this QEMU +//! machine, so this board does not expose a GPIO capsule; LEDs are +//! implemented against the separate, genuinely-emulated FPGAIO block). + +#![no_std] +#![no_main] + +use kernel::capabilities; +use kernel::component::Component; +use kernel::debug::PanicResources; +use kernel::platform::chip::Chip; +use kernel::platform::{KernelResources, SyscallDriverLookup}; +use kernel::utilities::single_thread_value::SingleThreadValue; +use kernel::{create_capability, static_init}; + +pub mod io; + +const NUM_PROCS: usize = 4; + +type ChipHw = qemu_arm_mps2_chip::chip::QemuArmMps2Chip< + 'static, + cortexm4::CortexM4, + qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, +>; +type ProcessPrinterInUse = capsules_system::process_printer::ProcessPrinterText; +type SchedulerInUse = components::sched::round_robin::RoundRobinComponentType; + +static PANIC_RESOURCES: SingleThreadValue> = + SingleThreadValue::new(); + +kernel::stack_size! {0x2000} + +struct QemuArmMps2An386 { + console: &'static capsules_core::console::Console<'static>, + scheduler: &'static SchedulerInUse, + systick: cortexm4::systick::SysTick, + led: &'static capsules_core::led::LedDriver< + 'static, + qemu_arm_mps2_chip::led::Led<'static>, + { qemu_arm_mps2_chip::led::NUM_LEDS as usize }, + >, + alarm: &'static capsules_core::alarm::AlarmDriver< + 'static, + capsules_core::virtualizers::virtual_alarm::VirtualMuxAlarm< + 'static, + qemu_arm_mps2_chip::timer::Timer<'static>, + >, + >, +} + +impl SyscallDriverLookup for QemuArmMps2An386 { + fn with_driver(&self, driver_num: usize, f: F) -> R + where + F: FnOnce(Option<&dyn kernel::syscall::SyscallDriver>) -> R, + { + match driver_num { + capsules_core::console::DRIVER_NUM => f(Some(self.console)), + capsules_core::led::DRIVER_NUM => f(Some(self.led)), + capsules_core::alarm::DRIVER_NUM => f(Some(self.alarm)), + _ => f(None), + } + } +} + +impl KernelResources for QemuArmMps2An386 { + type SyscallDriverLookup = Self; + type SyscallFilter = (); + type ProcessFault = (); + type Scheduler = SchedulerInUse; + type SchedulerTimer = cortexm4::systick::SysTick; + type WatchDog = (); + type ContextSwitchCallback = (); + + fn syscall_driver_lookup(&self) -> &Self::SyscallDriverLookup { + self + } + fn syscall_filter(&self) -> &Self::SyscallFilter { + &() + } + fn process_fault(&self) -> &Self::ProcessFault { + &() + } + fn scheduler(&self) -> &Self::Scheduler { + self.scheduler + } + fn scheduler_timer(&self) -> &Self::SchedulerTimer { + &self.systick + } + fn watchdog(&self) -> &Self::WatchDog { + &() + } + fn context_switch_callback(&self) -> &Self::ContextSwitchCallback { + &() + } +} + +#[inline(never)] +unsafe fn start() -> ( + &'static kernel::Kernel, + &'static QemuArmMps2An386, + &'static ChipHw, +) { + ChipHw::init(); + + kernel::deferred_call::initialize_deferred_call_state::< + ::ThreadIdProvider, + >(); + + let _ = PANIC_RESOURCES + .bind_to_thread::<::ThreadIdProvider>( + PanicResources::new(), + ); + + let peripherals = static_init!( + qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, + qemu_arm_mps2_chip::Mps2DefaultPeripherals::new() + ); + + let processes = components::process_array::ProcessArrayComponent::new() + .finalize(components::process_array_component_static!(NUM_PROCS)); + let board_kernel = static_init!(kernel::Kernel, kernel::Kernel::new(processes.as_slice())); + + let chip = static_init!(ChipHw, ChipHw::new(peripherals)); + + let uart_mux = components::console::UartMuxComponent::new(&peripherals.uart0, 115200) + .finalize(components::uart_mux_component_static!()); + + let console = components::console::ConsoleComponent::new( + board_kernel, + capsules_core::console::DRIVER_NUM, + uart_mux, + create_capability!(capabilities::MemoryAllocationCapability), + ) + .finalize(components::console_component_static!()); + + components::debug_writer::DebugWriterComponent::new::< + ::ThreadIdProvider, + >( + uart_mux, + create_capability!(capabilities::SetDebugWriterCapability), + ) + .finalize(components::debug_writer_component_static!()); + + let alarm_mux = components::alarm::AlarmMuxComponent::new(&peripherals.timer0).finalize( + components::alarm_mux_component_static!(qemu_arm_mps2_chip::timer::Timer), + ); + + let alarm = components::alarm::AlarmDriverComponent::new( + board_kernel, + capsules_core::alarm::DRIVER_NUM, + alarm_mux, + create_capability!(capabilities::MemoryAllocationCapability), + ) + .finalize(components::alarm_component_static!( + qemu_arm_mps2_chip::timer::Timer + )); + + kernel::create_typed_capability!(process_console_cap, ProcessConsoleCap: + kernel::capabilities::ProcessManagementCapability, + kernel::capabilities::ProcessStartCapability + ); + let process_console = components::process_console::ProcessConsoleComponent::new( + board_kernel, + uart_mux, + alarm_mux, + components::process_printer::ProcessPrinterTextComponent::new() + .finalize(components::process_printer_text_component_static!()), + None, + process_console_cap, + ) + .finalize(components::process_console_component_static!( + qemu_arm_mps2_chip::timer::Timer, + ProcessConsoleCap + )); + let _ = process_console.start(); + + let led = components::led::LedsComponent::new().finalize(components::led_component_static!( + qemu_arm_mps2_chip::led::Led<'static>, + peripherals.fpgaio.led(0), + peripherals.fpgaio.led(1), + )); + + let scheduler = components::sched::round_robin::RoundRobinComponent::new(processes) + .finalize(components::round_robin_component_static!(NUM_PROCS)); + + let platform = static_init!( + QemuArmMps2An386, + QemuArmMps2An386 { + console, + scheduler, + systick: cortexm4::systick::SysTick::new(), + led, + alarm, + } + ); + + extern "C" { + /// Beginning of the ROM region containing app images. + static _sapps: u8; + /// End of the ROM region containing app images. + static _eapps: u8; + /// Beginning of the RAM region for app memory. + static mut _sappmem: u8; + /// End of the RAM region for app memory. + static _eappmem: u8; + } + + let process_management_capability = + create_capability!(capabilities::ProcessManagementCapability); + kernel::process::load_processes( + board_kernel, + chip, + core::slice::from_raw_parts( + core::ptr::addr_of!(_sapps), + core::ptr::addr_of!(_eapps) as usize - core::ptr::addr_of!(_sapps) as usize, + ), + core::slice::from_raw_parts_mut( + core::ptr::addr_of_mut!(_sappmem), + core::ptr::addr_of!(_eappmem) as usize - core::ptr::addr_of!(_sappmem) as usize, + ), + &capsules_system::process_policies::PanicFaultPolicy {}, + &process_management_capability, + ) + .unwrap_or_else(|err| { + kernel::debug!("Error loading processes!"); + kernel::debug!("{:?}", err); + }); + + (board_kernel, platform, chip) +} + +/// Main function called after RAM initialized. +#[no_mangle] +pub unsafe fn main() { + let (board_kernel, platform, chip) = start(); + + kernel::debug!("QEMU MPS2 AN386 (Cortex-M4) initialization complete."); + kernel::debug!("Entering main loop."); + + let main_loop_capability = create_capability!(capabilities::MainLoopCapability); + board_kernel.kernel_loop::( + platform, + chip, + None, + &main_loop_capability, + ); +} diff --git a/chips/qemu_arm_mps2_chip/Cargo.toml b/chips/qemu_arm_mps2_chip/Cargo.toml new file mode 100644 index 00000000000..872d9ff3b6f --- /dev/null +++ b/chips/qemu_arm_mps2_chip/Cargo.toml @@ -0,0 +1,30 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2026. + +[package] +name = "qemu_arm_mps2_chip" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[dependencies] +cortexm = { path = "../../arch/cortex-m" } +cortexm3 = { path = "../../arch/cortex-m3", optional = true } +cortexm4 = { path = "../../arch/cortex-m4", optional = true } +kernel = { path = "../../kernel" } + +[features] +# Selects which concrete vector table (`vectors_m3`/`vectors_m4`) this chip +# crate provides. Kept out of the generic `chip::QemuArmMps2Chip` (which +# stays generic over the CortexMVariant so both boards can share it): the +# vector table's `_estack` reference must be a function-typed `extern "C"` +# declaration, fully resolved within this crate, so it never ends up +# monomorphized into a board crate that also (via +# `components::process_console`) declares `_estack` as a data symbol — +# mixing the two in one compilation unit trips LTO's symbol merging. +cortex-m3 = ["dep:cortexm3"] +cortex-m4 = ["dep:cortexm4"] + +[lints] +workspace = true diff --git a/chips/qemu_arm_mps2_chip/src/chip.rs b/chips/qemu_arm_mps2_chip/src/chip.rs new file mode 100644 index 00000000000..31666c17822 --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/chip.rs @@ -0,0 +1,99 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! Top-level chip definition for the ARM MPS2 AN385/AN386 FPGA images. +//! +//! an385 (Cortex-M3) and an386 (Cortex-M4) share an identical peripheral +//! map (`hw/arm/mps2.c`'s `mps2_common_init`), differing only in CPU core, +//! so this is generic over the [`cortexm::CortexMVariant`] the board +//! chooses. + +use core::fmt::Write; + +use cortexm::CortexMVariant; +use kernel::platform::chip::InterruptService; +use kernel::utilities::StaticRef; + +const MPU_BASE_ADDRESS: StaticRef = + unsafe { StaticRef::new(0xE000_ED90 as *const cortexm::mpu::MpuRegisters) }; + +pub type Mps2Mpu = cortexm::mpu::MPU<8, 32>; + +pub struct QemuArmMps2Chip<'a, V: CortexMVariant, I: InterruptService + 'a> { + mpu: Mps2Mpu, + userspace_kernel_boundary: cortexm::syscall::SysCall, + interrupt_service: &'a I, +} + +impl<'a, V: CortexMVariant, I: InterruptService + 'a> QemuArmMps2Chip<'a, V, I> { + /// # Safety + /// + /// Must only be called once, as it takes ownership of the MPU and + /// syscall-boundary hardware state. + pub unsafe fn new(interrupt_service: &'a I) -> Self { + Self { + mpu: unsafe { Mps2Mpu::new(MPU_BASE_ADDRESS) }, + userspace_kernel_boundary: unsafe { cortexm::syscall::SysCall::new() }, + interrupt_service, + } + } +} + +impl<'a, V: CortexMVariant, I: InterruptService + 'a> kernel::platform::chip::Chip + for QemuArmMps2Chip<'a, V, I> +{ + type MPU = Mps2Mpu; + type UserspaceKernelBoundary = cortexm::syscall::SysCall; + type ThreadIdProvider = cortexm::thread_id::CortexMThreadIdProvider; + + fn init() { + // This board has no bootloader relocating the vector table, and no + // documented silicon errata to work around (this is a QEMU-only + // FPGA reference image, not real silicon), so there is nothing to + // do beyond unmasking interrupts at the NVIC. + cortexm::nvic::enable_all(); + } + + fn mpu(&self) -> &Self::MPU { + &self.mpu + } + + fn userspace_kernel_boundary(&self) -> &Self::UserspaceKernelBoundary { + &self.userspace_kernel_boundary + } + + fn service_pending_interrupts(&self) { + while let Some(interrupt) = cortexm::nvic::next_pending() { + if !self.interrupt_service.service_interrupt(interrupt) { + panic!("unhandled interrupt {}", interrupt); + } + let n = cortexm::nvic::Nvic::new(interrupt); + n.clear_pending(); + n.enable(); + } + } + + fn has_pending_interrupts(&self) -> bool { + cortexm::nvic::has_pending() + } + + fn sleep(&self) { + unsafe { + cortexm::support::wfi(); + } + } + + fn with_interrupts_disabled(&self, f: F) -> R + where + F: FnOnce() -> R, + { + cortexm::support::with_interrupts_disabled(f) + } + + unsafe fn print_state(_this: Option<&Self>, write: &mut dyn Write) { + unsafe { + V::print_cortexm_state(write); + } + } +} diff --git a/chips/qemu_arm_mps2_chip/src/interrupts.rs b/chips/qemu_arm_mps2_chip/src/interrupts.rs new file mode 100644 index 00000000000..130c63dd004 --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/interrupts.rs @@ -0,0 +1,24 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! NVIC external interrupt numbers for the ARM MPS2 AN385/AN386 FPGA images. +//! +//! Taken from QEMU's `hw/arm/mps2.c` (`mps2_common_init`), which is shared +//! by both FPGA images. Only the peripherals this chip crate actually +//! drives are listed; the MPS2 image wires up several more (SPI, I2C, +//! Ethernet) that are out of scope here. + +pub const UART0_RX: u32 = 0; +pub const UART0_TX: u32 = 1; +pub const UART1_RX: u32 = 2; +pub const UART1_TX: u32 = 3; +pub const UART2_RX: u32 = 4; +pub const UART2_TX: u32 = 5; +pub const TIMER0: u32 = 8; +pub const TIMER1: u32 = 9; +pub const DUALTIMER: u32 = 10; +pub const UART3_RX: u32 = 18; +pub const UART3_TX: u32 = 19; +pub const UART4_RX: u32 = 20; +pub const UART4_TX: u32 = 21; diff --git a/chips/qemu_arm_mps2_chip/src/led.rs b/chips/qemu_arm_mps2_chip/src/led.rs new file mode 100644 index 00000000000..bd8486ca166 --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/led.rs @@ -0,0 +1,78 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! LEDs on the MPS2 AN385/AN386 FPGA images, via the "FPGAIO" register +//! block. +//! +//! This is *not* implemented on top of the CMSDK AHB GPIO peripheral: QEMU +//! models the four CMSDK GPIO banks as inert stubs (writes are discarded, +//! reads always return 0) on every MPS2/MPS2-TZ machine, so pin state +//! changes made through that peripheral are never observable in emulation. +//! `FPGAIO`'s `LED0` register, by contrast, is fully emulated in QEMU +//! (`hw/misc/mps2-fpgaio.c`) and drives an actual `LEDState` per bit, so +//! this is the only way to get genuinely observable LED behavior under +//! this QEMU machine. + +use kernel::hil; +use kernel::utilities::StaticRef; +use kernel::utilities::registers::ReadWrite; +use kernel::utilities::registers::interfaces::{Readable, Writeable}; + +pub const FPGAIO_BASE: StaticRef = + unsafe { StaticRef::new(0x4002_8000 as *const FpgaioRegisters) }; + +/// Number of LEDs QEMU wires up to `LED0` for the an385/an386 machines +/// (the `mps2-fpgaio` device's `num-leds` property default). +pub const NUM_LEDS: u32 = 2; + +#[repr(C)] +pub struct FpgaioRegisters { + led0: ReadWrite, +} + +pub struct Fpgaio { + registers: StaticRef, +} + +impl Fpgaio { + pub const fn new(registers: StaticRef) -> Self { + Fpgaio { registers } + } + + /// Returns a [`hil::led::Led`] handle for LED `index` (`0..NUM_LEDS`). + pub fn led(&self, index: u32) -> Led<'_> { + Led { + fpgaio: self, + mask: 1 << index, + } + } +} + +pub struct Led<'a> { + fpgaio: &'a Fpgaio, + mask: u32, +} + +impl hil::led::Led for Led<'_> { + fn init(&self) {} + + fn on(&self) { + let v = self.fpgaio.registers.led0.get() | self.mask; + self.fpgaio.registers.led0.set(v); + } + + fn off(&self) { + let v = self.fpgaio.registers.led0.get() & !self.mask; + self.fpgaio.registers.led0.set(v); + } + + fn toggle(&self) { + let v = self.fpgaio.registers.led0.get() ^ self.mask; + self.fpgaio.registers.led0.set(v); + } + + fn read(&self) -> bool { + self.fpgaio.registers.led0.get() & self.mask != 0 + } +} diff --git a/chips/qemu_arm_mps2_chip/src/lib.rs b/chips/qemu_arm_mps2_chip/src/lib.rs new file mode 100644 index 00000000000..4db5c411a57 --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/lib.rs @@ -0,0 +1,62 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! Chip support for the ARM MPS2 AN385/AN386 FPGA images under QEMU. + +#![no_std] + +pub mod chip; +pub mod interrupts; +pub mod led; +pub mod timer; +pub mod uart; + +#[cfg(feature = "cortex-m3")] +pub mod vectors_m3; +#[cfg(feature = "cortex-m4")] +pub mod vectors_m4; + +use kernel::platform::chip::InterruptService; + +/// The MPS2 AN385/AN386 machine's fixed system clock, in Hz (`SYSCLK_FRQ` +/// in QEMU's `hw/arm/mps2.c`), which every CMSDK peripheral's PCLK is +/// driven from. +pub const SYSCLK_FRQ: u32 = 25_000_000; + +/// Instantiates the peripherals this chip crate drives. +/// +/// Only UART0 and Timer0 are wired up as the console and alarm backing; +/// UART1-4 and Timer1 exist on the real memory map but are unused here. +pub struct Mps2DefaultPeripherals<'a> { + pub uart0: uart::Uart<'a>, + pub timer0: timer::Timer<'a>, + pub fpgaio: led::Fpgaio, +} + +impl Mps2DefaultPeripherals<'_> { + pub fn new() -> Self { + Self { + uart0: uart::Uart::new(uart::UART0_BASE), + timer0: timer::Timer::new(timer::TIMER0_BASE), + fpgaio: led::Fpgaio::new(led::FPGAIO_BASE), + } + } +} + +impl Default for Mps2DefaultPeripherals<'_> { + fn default() -> Self { + Self::new() + } +} + +impl InterruptService for Mps2DefaultPeripherals<'_> { + fn service_interrupt(&self, interrupt: u32) -> bool { + match interrupt { + interrupts::UART0_RX | interrupts::UART0_TX => self.uart0.handle_interrupt(), + interrupts::TIMER0 => self.timer0.handle_interrupt(), + _ => return false, + } + true + } +} diff --git a/chips/qemu_arm_mps2_chip/src/timer.rs b/chips/qemu_arm_mps2_chip/src/timer.rs new file mode 100644 index 00000000000..725ad98f080 --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/timer.rs @@ -0,0 +1,182 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! ARM CMSDK APB Timer, as found on the MPS2 AN385/AN386 FPGA images. +//! +//! This is a plain 32-bit down-counter with a reload register: it counts +//! down from `VALUE` to 0 at `PCLK`, then (if enabled) reloads `VALUE` from +//! `RELOAD` and continues, raising an interrupt on every 1-to-0 transition. +//! There is no separate free-running counter or compare register. +//! +//! To expose this as a [`hil::time::Alarm`] we keep `RELOAD` fixed at +//! `u32::MAX` so the hardware free-runs and `now()` is simply +//! `u32::MAX - VALUE` (matching [`Ticks32`]'s own wraparound at 2**32). +//! Arming an alarm shortens the *current* countdown by writing `VALUE` +//! directly (which, per the hardware, does not disturb `RELOAD`), so the +//! timer keeps free-running normally once the shortened countdown expires. +//! Because both a deliberately-armed deadline and a routine free-run wrap +//! look identical to the hardware (both are just "VALUE hit 0"), every +//! interrupt resynchronizes our tracked epoch the same way, and only +//! invokes the alarm callback if an alarm was actually pending. + +use core::cell::Cell; + +use kernel::hil::time::{Alarm, AlarmClient, Frequency, Ticks, Ticks32, Time}; +use kernel::utilities::StaticRef; +use kernel::utilities::registers::interfaces::{Readable, Writeable}; +use kernel::utilities::registers::{ReadWrite, register_bitfields}; + +pub const TIMER0_BASE: StaticRef = + unsafe { StaticRef::new(0x4000_0000 as *const TimerRegisters) }; +pub const TIMER1_BASE: StaticRef = + unsafe { StaticRef::new(0x4000_1000 as *const TimerRegisters) }; + +#[repr(C)] +pub struct TimerRegisters { + ctrl: ReadWrite, + value: ReadWrite, + reload: ReadWrite, + intstatus: ReadWrite, +} + +register_bitfields![u32, + CTRL [ + En OFFSET(0) NUMBITS(1) [], + IrqEn OFFSET(3) NUMBITS(1) [], + ], + VALUE [ + Value OFFSET(0) NUMBITS(32) [], + ], + INTSTATUS [ + Irq OFFSET(0) NUMBITS(1) [], + ], +]; + +pub struct Freq25MHz; +impl Frequency for Freq25MHz { + fn frequency() -> u32 { + crate::SYSCLK_FRQ + } +} + +pub struct Timer<'a> { + registers: StaticRef, + client: kernel::utilities::cells::OptionalCell<&'a dyn AlarmClient>, + /// Absolute tick value as of the last time we reconciled it with the + /// live hardware `VALUE`. + synced_now: Cell, + /// The raw `VALUE` the hardware held at `synced_now`. + synced_value: Cell, + /// The most recently requested alarm target, valid regardless of + /// whether it is still pending (per the `Alarm::get_alarm` contract). + target: Cell, + /// Whether the next "VALUE hit 0" interrupt corresponds to an + /// actual armed alarm, as opposed to a routine free-run wrap. + armed: Cell, +} + +impl<'a> Timer<'a> { + pub fn new(registers: StaticRef) -> Timer<'a> { + registers.ctrl.set(0); + registers.reload.write(VALUE::Value.val(u32::MAX)); + registers.value.write(VALUE::Value.val(u32::MAX)); + registers.ctrl.write(CTRL::En::SET + CTRL::IrqEn::SET); + + Timer { + registers, + client: kernel::utilities::cells::OptionalCell::empty(), + synced_now: Cell::new(0), + synced_value: Cell::new(u32::MAX), + target: Cell::new(0), + armed: Cell::new(false), + } + } + + fn live_now(&self) -> u32 { + let value = self.registers.value.read(VALUE::Value); + // VALUE only ever counts down between resyncs (any reload happens + // via `handle_interrupt`, which itself resyncs), so this + // subtraction is always of a smaller-or-equal current value from + // the last known one. + self.synced_now + .get() + .wrapping_add(self.synced_value.get().wrapping_sub(value)) + } + + pub fn handle_interrupt(&self) { + self.registers.intstatus.write(INTSTATUS::Irq::SET); + + // Whatever VALUE was set to before hitting 0, the hardware has now + // reloaded it from RELOAD (u32::MAX), and that reload happened + // exactly `synced_value` ticks after `synced_now`. + self.synced_now + .set(self.synced_now.get().wrapping_add(self.synced_value.get())); + self.synced_value.set(u32::MAX); + + if self.armed.take() { + self.client.map(|client| client.alarm()); + } + } +} + +impl Time for Timer<'_> { + type Frequency = Freq25MHz; + type Ticks = Ticks32; + + fn now(&self) -> Ticks32 { + Ticks32::from(self.live_now()) + } +} + +impl<'a> Alarm<'a> for Timer<'a> { + fn set_alarm_client(&self, client: &'a dyn AlarmClient) { + self.client.set(client); + } + + fn set_alarm(&self, reference: Ticks32, dt: Ticks32) { + let now = self.now(); + let target = reference.wrapping_add(dt); + self.target.set(target.into_usize() as u32); + + let elapsed = now.wrapping_sub(reference); + let remaining = if elapsed >= dt { + self.minimum_dt() + } else { + let r = dt.wrapping_sub(elapsed); + if r.into_usize() < self.minimum_dt().into_usize() { + self.minimum_dt() + } else { + r + } + }; + + // Resync to "now" before shortening the countdown: the write to + // VALUE below becomes the new reference point for `live_now`. + self.synced_now.set(now.into_usize() as u32); + let remaining_raw = remaining.into_usize() as u32; + self.synced_value.set(remaining_raw); + self.armed.set(true); + self.registers.value.write(VALUE::Value.val(remaining_raw)); + } + + fn get_alarm(&self) -> Ticks32 { + Ticks32::from(self.target.get()) + } + + fn disarm(&self) -> Result<(), kernel::ErrorCode> { + // The hardware will still raise an interrupt when the shortened + // countdown reaches 0, but with `armed` cleared that interrupt is + // treated as an ordinary free-run wrap and no callback fires. + self.armed.set(false); + Ok(()) + } + + fn is_armed(&self) -> bool { + self.armed.get() + } + + fn minimum_dt(&self) -> Ticks32 { + Ticks32::from(10) + } +} diff --git a/chips/qemu_arm_mps2_chip/src/uart.rs b/chips/qemu_arm_mps2_chip/src/uart.rs new file mode 100644 index 00000000000..60fca4b1c4e --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/uart.rs @@ -0,0 +1,339 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! ARM CMSDK APB UART, as found on the MPS2 AN385/AN386 FPGA images. +//! +//! Documented in the Cortex-M System Design Kit Technical Reference Manual +//! (ARM DDI0479C). Unlike a 16550, this device has no FIFO: it holds a +//! single byte in each direction, signaled by the TXFULL/RXFULL bits in +//! `STATE`. + +use core::cell::Cell; + +use kernel::ErrorCode; +use kernel::hil; +use kernel::utilities::StaticRef; +use kernel::utilities::cells::{OptionalCell, TakeCell}; +use kernel::utilities::io_write::IoWrite; +use kernel::utilities::registers::interfaces::{ReadWriteable, Readable, Writeable}; +use kernel::utilities::registers::{ReadWrite, register_bitfields}; + +use crate::SYSCLK_FRQ; + +pub const UART0_BASE: StaticRef = + unsafe { StaticRef::new(0x4000_4000 as *const UartRegisters) }; +pub const UART1_BASE: StaticRef = + unsafe { StaticRef::new(0x4000_5000 as *const UartRegisters) }; +pub const UART2_BASE: StaticRef = + unsafe { StaticRef::new(0x4000_6000 as *const UartRegisters) }; +pub const UART3_BASE: StaticRef = + unsafe { StaticRef::new(0x4000_7000 as *const UartRegisters) }; +pub const UART4_BASE: StaticRef = + unsafe { StaticRef::new(0x4000_9000 as *const UartRegisters) }; + +#[repr(C)] +pub struct UartRegisters { + data: ReadWrite, + state: ReadWrite, + ctrl: ReadWrite, + intstatus: ReadWrite, + bauddiv: ReadWrite, +} + +register_bitfields![u32, + DATA [ + Data OFFSET(0) NUMBITS(8) [], + ], + STATE [ + TxFull OFFSET(0) NUMBITS(1) [], + RxFull OFFSET(1) NUMBITS(1) [], + TxOverrun OFFSET(2) NUMBITS(1) [], + RxOverrun OFFSET(3) NUMBITS(1) [], + ], + CTRL [ + TxEn OFFSET(0) NUMBITS(1) [], + RxEn OFFSET(1) NUMBITS(1) [], + TxIntEn OFFSET(2) NUMBITS(1) [], + RxIntEn OFFSET(3) NUMBITS(1) [], + TxOverrunIntEn OFFSET(4) NUMBITS(1) [], + RxOverrunIntEn OFFSET(5) NUMBITS(1) [], + ], + INTSTATUS [ + Tx OFFSET(0) NUMBITS(1) [], + Rx OFFSET(1) NUMBITS(1) [], + TxOverrun OFFSET(2) NUMBITS(1) [], + RxOverrun OFFSET(3) NUMBITS(1) [], + ], + BAUDDIV [ + Div OFFSET(0) NUMBITS(20) [], + ], +]; + +pub struct Uart<'a> { + registers: StaticRef, + tx_client: OptionalCell<&'a dyn hil::uart::TransmitClient>, + rx_client: OptionalCell<&'a dyn hil::uart::ReceiveClient>, + tx_buffer: TakeCell<'static, [u8]>, + tx_len: Cell, + tx_index: Cell, + rx_buffer: TakeCell<'static, [u8]>, + rx_len: Cell, + rx_index: Cell, +} + +impl<'a> Uart<'a> { + pub const fn new(registers: StaticRef) -> Uart<'a> { + Uart { + registers, + tx_client: OptionalCell::empty(), + rx_client: OptionalCell::empty(), + tx_buffer: TakeCell::empty(), + tx_len: Cell::new(0), + tx_index: Cell::new(0), + rx_buffer: TakeCell::empty(), + rx_len: Cell::new(0), + rx_index: Cell::new(0), + } + } + + /// Disable the device and clear any pending interrupt state. Safe to + /// call at any time; used both at construction and by the panic writer. + fn reset(&self) { + self.registers.ctrl.set(0); + self.registers.intstatus.write( + INTSTATUS::Tx::SET + + INTSTATUS::Rx::SET + + INTSTATUS::TxOverrun::SET + + INTSTATUS::RxOverrun::SET, + ); + } + + pub fn handle_interrupt(&self) { + let intstatus = self.registers.intstatus.extract(); + + if intstatus.is_set(INTSTATUS::Tx) { + self.registers.intstatus.write(INTSTATUS::Tx::SET); + self.transmit_continue(); + } + if intstatus.is_set(INTSTATUS::Rx) { + self.registers.intstatus.write(INTSTATUS::Rx::SET); + self.receive_continue(); + } + // Overrun conditions: clear them so the interrupt doesn't retrigger. + // There is no HIL-level overrun reporting for this simple device. + if intstatus.is_set(INTSTATUS::TxOverrun) { + self.registers.state.write(STATE::TxOverrun::SET); + self.registers.intstatus.write(INTSTATUS::TxOverrun::SET); + } + if intstatus.is_set(INTSTATUS::RxOverrun) { + self.registers.state.write(STATE::RxOverrun::SET); + self.registers.intstatus.write(INTSTATUS::RxOverrun::SET); + } + } + + fn transmit_continue(&self) { + let Some(tx_data) = self.tx_buffer.take() else { + // Spurious: no transmission in progress (e.g. panic writer used + // the device synchronously while an async transfer was live). + return; + }; + + let mut index = self.tx_index.get(); + if index < self.tx_len.get() && !self.registers.state.is_set(STATE::TxFull) { + self.registers + .data + .write(DATA::Data.val(tx_data[index] as u32)); + index += 1; + } + + if index < self.tx_len.get() { + self.tx_index.set(index); + self.tx_buffer.replace(tx_data); + } else { + self.registers.ctrl.modify(CTRL::TxIntEn::CLEAR); + self.tx_client + .map(move |client| client.transmitted_buffer(tx_data, self.tx_len.get(), Ok(()))); + } + } + + fn receive_continue(&self) { + let Some(rx_buffer) = self.rx_buffer.take() else { + return; + }; + + let len = self.rx_len.get(); + let mut index = self.rx_index.get(); + if index < len && self.registers.state.is_set(STATE::RxFull) { + rx_buffer[index] = self.registers.data.read(DATA::Data) as u8; + index += 1; + } + + if index == len { + self.registers.ctrl.modify(CTRL::RxIntEn::CLEAR); + self.rx_client.map(move |client| { + client.received_buffer(rx_buffer, len, Ok(()), hil::uart::Error::None) + }); + } else { + self.rx_index.set(index); + self.rx_buffer.replace(rx_buffer); + } + } +} + +impl hil::uart::Configure for Uart<'_> { + fn configure(&self, params: hil::uart::Parameters) -> Result<(), ErrorCode> { + // The hardware is fixed at 8 data bits, no parity, one stop bit, no + // flow control; it cannot represent anything else. + if params.width != hil::uart::Width::Eight + || params.parity != hil::uart::Parity::None + || params.stop_bits != hil::uart::StopBits::One + || params.hw_flow_control + { + return Err(ErrorCode::NOSUPPORT); + } + + let bauddiv = SYSCLK_FRQ / params.baud_rate; + if !(16..=SYSCLK_FRQ).contains(&bauddiv) { + return Err(ErrorCode::INVAL); + } + self.registers.bauddiv.write(BAUDDIV::Div.val(bauddiv)); + self.registers + .ctrl + .modify(CTRL::TxEn::SET + CTRL::RxEn::SET); + + Ok(()) + } +} + +impl<'a> hil::uart::Transmit<'a> for Uart<'a> { + fn set_transmit_client(&self, client: &'a dyn hil::uart::TransmitClient) { + self.tx_client.set(client); + } + + fn transmit_buffer( + &self, + tx_data: &'static mut [u8], + tx_len: usize, + ) -> Result<(), (ErrorCode, &'static mut [u8])> { + if tx_len > tx_data.len() { + return Err((ErrorCode::SIZE, tx_data)); + } + if tx_len == 0 { + return Err((ErrorCode::INVAL, tx_data)); + } + if self.tx_buffer.is_some() { + return Err((ErrorCode::BUSY, tx_data)); + } + + self.registers.ctrl.modify(CTRL::TxIntEn::SET); + + let mut index = 0; + if !self.registers.state.is_set(STATE::TxFull) { + self.registers.data.write(DATA::Data.val(tx_data[0] as u32)); + index = 1; + } + + self.tx_len.set(tx_len); + self.tx_index.set(index); + self.tx_buffer.replace(tx_data); + + Ok(()) + } + + fn transmit_abort(&self) -> Result<(), ErrorCode> { + Err(ErrorCode::FAIL) + } + + fn transmit_word(&self, _word: u32) -> Result<(), ErrorCode> { + Err(ErrorCode::FAIL) + } +} + +impl<'a> hil::uart::Receive<'a> for Uart<'a> { + fn set_receive_client(&self, client: &'a dyn hil::uart::ReceiveClient) { + self.rx_client.set(client); + } + + fn receive_buffer( + &self, + rx_buffer: &'static mut [u8], + rx_len: usize, + ) -> Result<(), (ErrorCode, &'static mut [u8])> { + if rx_len > rx_buffer.len() { + return Err((ErrorCode::SIZE, rx_buffer)); + } + if self.rx_buffer.is_some() { + return Err((ErrorCode::BUSY, rx_buffer)); + } + + self.rx_buffer.replace(rx_buffer); + self.rx_len.set(rx_len); + self.rx_index.set(0); + self.registers.ctrl.modify(CTRL::RxIntEn::SET); + + Ok(()) + } + + fn receive_abort(&self) -> Result<(), ErrorCode> { + Err(ErrorCode::FAIL) + } + + fn receive_word(&self) -> Result<(), ErrorCode> { + Err(ErrorCode::FAIL) + } +} + +/// A synchronous, polling writer for panic messages. +/// +/// This bypasses all interrupt-driven state above and is only ever used +/// from the panic handler. +pub struct UartPanicWriter<'a> { + inner: Uart<'a>, +} + +impl UartPanicWriter<'_> { + fn transmit_sync(&self, bytes: &[u8]) { + self.inner.registers.ctrl.modify(CTRL::TxIntEn::CLEAR); + for byte in bytes { + while self.inner.registers.state.is_set(STATE::TxFull) {} + self.inner + .registers + .data + .write(DATA::Data.val(*byte as u32)); + } + while self.inner.registers.state.is_set(STATE::TxFull) {} + } +} + +impl IoWrite for UartPanicWriter<'_> { + fn write(&mut self, buf: &[u8]) -> usize { + self.transmit_sync(buf); + buf.len() + } +} + +impl core::fmt::Write for UartPanicWriter<'_> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.write(s.as_bytes()); + Ok(()) + } +} + +pub struct UartPanicWriterConfig { + pub base: StaticRef, + pub params: hil::uart::Parameters, +} + +impl kernel::platform::chip::PanicWriter for UartPanicWriter<'_> { + type Config = UartPanicWriterConfig; + + unsafe fn create_panic_writer(config: Self::Config) -> impl IoWrite + core::fmt::Write { + use hil::uart::Configure as _; + + let inner = Uart::new(config.base); + inner.reset(); + let _ = inner.configure(config.params); + UartPanicWriter { inner } + } +} diff --git a/chips/qemu_arm_mps2_chip/src/vectors_m3.rs b/chips/qemu_arm_mps2_chip/src/vectors_m3.rs new file mode 100644 index 00000000000..a5ec54c488e --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/vectors_m3.rs @@ -0,0 +1,61 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! ARM Cortex-M vector table for the MPS2 AN385 (Cortex-M3) machine. +//! +//! There is no bootloader relocating the vector table on this QEMU-only +//! FPGA image: it is loaded and executed directly from address 0, which is +//! exactly where this table is placed by the linker script's `.vectors` +//! section. There are no documented silicon errata to apply, unlike real +//! hardware chip crates (this is a synthetic reference platform, not real +//! silicon). +//! +//! This is deliberately a concrete (non-generic) module, unlike +//! [`crate::chip::QemuArmMps2Chip`]: see the `cortex-m3`/`cortex-m4` +//! feature doc comment in this crate's `Cargo.toml` for why. + +use cortexm3::{CortexM3, CortexMVariant, initialize_ram_jump_to_main, unhandled_interrupt}; + +extern "C" { + // _estack is not really a function, but it makes the types work. + // You should never actually invoke it!! + fn _estack(); +} + +#[cfg_attr( + all(target_arch = "arm", target_os = "none"), + link_section = ".vectors" +)] +#[cfg_attr(all(target_arch = "arm", target_os = "none"), used)] +/// ARM Cortex-M Vector Table +pub static BASE_VECTORS: [unsafe extern "C" fn(); 16] = [ + _estack, // Stack Pointer + initialize_ram_jump_to_main, // Reset Handler + unhandled_interrupt, // NMI + CortexM3::HARD_FAULT_HANDLER, // Hard Fault + unhandled_interrupt, // Memory Management Fault + unhandled_interrupt, // Bus Fault + unhandled_interrupt, // Usage Fault + unhandled_interrupt, // Reserved + unhandled_interrupt, // Reserved + unhandled_interrupt, // Reserved + unhandled_interrupt, // Reserved + CortexM3::SVC_HANDLER, // SVCall + unhandled_interrupt, // Reserved for Debug + unhandled_interrupt, // Reserved + unhandled_interrupt, // PendSv + CortexM3::SYSTICK_HANDLER, // SysTick +]; + +/// Number of NVIC external interrupt lines. +/// +/// The an385 machine's NVIC is configured with +/// `qdev_prop_set_uint32(armv7m, "num-irq", 32)` in `hw/arm/mps2.c`; only a +/// handful are wired to real devices, but the vector table must cover the +/// full range. +const NUM_IRQS: usize = 32; + +#[cfg_attr(all(target_arch = "arm", target_os = "none"), link_section = ".irqs")] +#[cfg_attr(all(target_arch = "arm", target_os = "none"), used)] +pub static IRQS: [unsafe extern "C" fn(); NUM_IRQS] = [CortexM3::GENERIC_ISR; NUM_IRQS]; diff --git a/chips/qemu_arm_mps2_chip/src/vectors_m4.rs b/chips/qemu_arm_mps2_chip/src/vectors_m4.rs new file mode 100644 index 00000000000..652a8339a74 --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/vectors_m4.rs @@ -0,0 +1,53 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! ARM Cortex-M vector table for the MPS2 AN386 (Cortex-M4) machine. +//! +//! See `vectors_m3.rs` for the full rationale; this is the identical +//! table for the an386 image, which differs from an385 only in CPU core. + +use cortexm4::{CortexM4, CortexMVariant, initialize_ram_jump_to_main, unhandled_interrupt}; + +extern "C" { + // _estack is not really a function, but it makes the types work. + // You should never actually invoke it!! + fn _estack(); +} + +#[cfg_attr( + all(target_arch = "arm", target_os = "none"), + link_section = ".vectors" +)] +#[cfg_attr(all(target_arch = "arm", target_os = "none"), used)] +/// ARM Cortex-M Vector Table +pub static BASE_VECTORS: [unsafe extern "C" fn(); 16] = [ + _estack, // Stack Pointer + initialize_ram_jump_to_main, // Reset Handler + unhandled_interrupt, // NMI + CortexM4::HARD_FAULT_HANDLER, // Hard Fault + unhandled_interrupt, // Memory Management Fault + unhandled_interrupt, // Bus Fault + unhandled_interrupt, // Usage Fault + unhandled_interrupt, // Reserved + unhandled_interrupt, // Reserved + unhandled_interrupt, // Reserved + unhandled_interrupt, // Reserved + CortexM4::SVC_HANDLER, // SVCall + unhandled_interrupt, // Reserved for Debug + unhandled_interrupt, // Reserved + unhandled_interrupt, // PendSv + CortexM4::SYSTICK_HANDLER, // SysTick +]; + +/// Number of NVIC external interrupt lines. +/// +/// The an386 machine's NVIC is configured with +/// `qdev_prop_set_uint32(armv7m, "num-irq", 32)` in `hw/arm/mps2.c`; only a +/// handful are wired to real devices, but the vector table must cover the +/// full range. +const NUM_IRQS: usize = 32; + +#[cfg_attr(all(target_arch = "arm", target_os = "none"), link_section = ".irqs")] +#[cfg_attr(all(target_arch = "arm", target_os = "none"), used)] +pub static IRQS: [unsafe extern "C" fn(); NUM_IRQS] = [CortexM4::GENERIC_ISR; NUM_IRQS]; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 51a530987fe..651a015c959 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -6,6 +6,7 @@ channel = "nightly-2026-07-21" components = ["miri", "llvm-tools", "rust-src", "rustfmt", "clippy", "rust-analyzer"] targets = ["thumbv6m-none-eabi", + "thumbv7m-none-eabi", "thumbv7em-none-eabi", "thumbv7em-none-eabihf", "thumbv8m.main-none-eabi", diff --git a/tools/ci/qemu-runner/src/main.rs b/tools/ci/qemu-runner/src/main.rs index 1d1b4c59954..d25e01a0c8e 100644 --- a/tools/ci/qemu-runner/src/main.rs +++ b/tools/ci/qemu-runner/src/main.rs @@ -39,6 +39,58 @@ fn hifive1() -> Result<(), Error> { Ok(()) } +fn mps2_an385() -> Result<(), Error> { + // First, build the board if needed + // n.b. rexpect's `exp_eof` does not actually block main thread, so use + // the standard Rust process library mechanism instead. + let mut build = Command::new("make") + .arg("-C") + .arg("../../../boards/qemu_arm_mps2_an385") + .spawn() + .expect("failed to spawn build"); + assert!(build.wait().unwrap().success()); + + let mut p = spawn( + "make qemu -C ../../../boards/qemu_arm_mps2_an385", + Some(10_000), + )?; + + p.exp_string("QEMU MPS2 AN385 (Cortex-M3) initialization complete.")?; + p.exp_string("Entering main loop.")?; + + // Test completed, kill QEMU + kill_qemu(&mut p)?; + + p.exp_string("QEMU: Terminated")?; + Ok(()) +} + +fn mps2_an386() -> Result<(), Error> { + // First, build the board if needed + // n.b. rexpect's `exp_eof` does not actually block main thread, so use + // the standard Rust process library mechanism instead. + let mut build = Command::new("make") + .arg("-C") + .arg("../../../boards/qemu_arm_mps2_an386") + .spawn() + .expect("failed to spawn build"); + assert!(build.wait().unwrap().success()); + + let mut p = spawn( + "make qemu -C ../../../boards/qemu_arm_mps2_an386", + Some(10_000), + )?; + + p.exp_string("QEMU MPS2 AN386 (Cortex-M4) initialization complete.")?; + p.exp_string("Entering main loop.")?; + + // Test completed, kill QEMU + kill_qemu(&mut p)?; + + p.exp_string("QEMU: Terminated")?; + Ok(()) +} + fn earlgrey_cw310() -> Result<(), Error> { // First, build the board if needed // n.b. rexpect's `exp_eof` does not actually block main thread, so use @@ -78,6 +130,14 @@ fn main() { hifive1().unwrap_or_else(|e| panic!("hifive1 job failed with {}", e)); println!("hifive1 SUCCESS."); println!(""); + println!("Running mps2_an385 tests..."); + mps2_an385().unwrap_or_else(|e| panic!("mps2_an385 job failed with {}", e)); + println!("mps2_an385 SUCCESS."); + println!(""); + println!("Running mps2_an386 tests..."); + mps2_an386().unwrap_or_else(|e| panic!("mps2_an386 job failed with {}", e)); + println!("mps2_an386 SUCCESS."); + println!(""); println!("Running earlgrey_cw310 tests..."); earlgrey_cw310().unwrap_or_else(|e| panic!("earlgrey_cw310 job failed with {}", e)); println!("earlgrey_cw310 SUCCESS."); From 6c174502b84b4a8dec222ea777864d9c7e633a7d Mon Sep 17 00:00:00 2001 From: ppannuto-claude Date: Tue, 25 Aug 2026 23:08:25 -0700 Subject: [PATCH 2/6] Add run-app target; verify c_hello + blink together on both boards Adds a run-app Makefile target (mirroring qemu_rv32_virt's) to both boards, loading app data via -device loader at the prog flash base. Uses the .bin kernel image rather than .elf: QEMU refuses to load two overlapping ROM blobs, and the kernel ELF's own 4-byte .apps placeholder otherwise collides with the injected app data at the same address. Verified on both an385 and an386 by building libtock-c's c_hello and blink examples and loading them at the same time: both processes load, schedule, and run correctly (c_hello prints via a real console syscall, blink runs continuously), which is what actually exercises the syscall dispatch, UserspaceKernelBoundary/switch_to_user, and per-process MPU setup that console/process-console alone never reach (those are kernel-internal and don't cross the syscall boundary). Also confirmed blink's LED toggling for real by reading the FPGAIO LED0 register live through the QEMU monitor. Co-Authored-By: Claude Sonnet 5 --- boards/qemu_arm_mps2_an385/Makefile | 22 ++++++++++++++++ boards/qemu_arm_mps2_an385/README.md | 38 ++++++++++++++++++++++++++++ boards/qemu_arm_mps2_an386/Makefile | 22 ++++++++++++++++ boards/qemu_arm_mps2_an386/README.md | 6 ++++- 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/boards/qemu_arm_mps2_an385/Makefile b/boards/qemu_arm_mps2_an385/Makefile index 357a515cc2c..51674a4c244 100644 --- a/boards/qemu_arm_mps2_an385/Makefile +++ b/boards/qemu_arm_mps2_an385/Makefile @@ -9,6 +9,9 @@ include ../Makefile.common QEMU_CMD := qemu-system-arm +# Base address of the "prog" (app) flash region; must match chip_layout.ld. +APP_ADDRESS := 0x00040000 + # Peripherals attached by default: # - CMSDK APB UART0 (attached to stdio) QEMU_BASE_CMDLINE := \ @@ -28,3 +31,22 @@ run: $(TOCK_ROOT_DIRECTORY)target/$(TARGET)/release/$(PLATFORM).elf .PHONY: qemu qemu: run + +# Same as `run`, but load an app (or several apps concatenated together, +# e.g. via `cat app1.tbf app2.tbf > apps.bin` — see README.md for the flash +# alignment requirement when combining more than one) at $(APP_ADDRESS). +# +# Uses the `.bin` kernel image (with the placeholder `.apps` section +# stripped by the standard $(OBJCOPY_FLAGS)) rather than the `.elf`: QEMU +# refuses to load two overlapping ROM blobs, and the kernel `.elf`'s own +# 4-byte `.apps` placeholder otherwise collides with $(APP) at the same +# address. +.PHONY: run-app +run-app: $(TOCK_ROOT_DIRECTORY)target/$(TARGET)/release/$(PLATFORM).bin + @echo + @echo -e "Running $$($(QEMU_CMD) --version | head -n1) with\n"\ + " - kernel $<\n"\ + " - app(s) $(APP)" + @echo "To exit type C-a x" + @echo + $(QEMU_BASE_CMDLINE) -kernel $< -device loader,file=$(APP),addr=$(APP_ADDRESS) diff --git a/boards/qemu_arm_mps2_an385/README.md b/boards/qemu_arm_mps2_an385/README.md index 6d75c131c6a..a15a00b0952 100644 --- a/boards/qemu_arm_mps2_an385/README.md +++ b/boards/qemu_arm_mps2_an385/README.md @@ -67,3 +67,41 @@ QEMU's hard 4 MiB cap for code at address 0 (`armv7m_load_kernel(..., 0, 0x400000)` in `hw/arm/mps2.c`); RAM is a modest slice of the 16 MiB QEMU always backs at 0x21000000 regardless of what a board's linker script claims. + +Running an application +----------------------- + +- **`run-app`**: Start Tock with one or more apps loaded at + `APP_ADDRESS` (0x00040000): + + ``` + $ make run-app APP=$PATH_TO_APP.tbf + ``` + + Verified end-to-end with `libtock-c`'s `c_hello` and `blink` examples, + built for `TOCK_TARGETS=cortex-m3` and loaded **at the same time**: + + ``` + $ cd $LIBTOCK_C/examples/c_hello && TOCK_TARGETS=cortex-m3 make + $ cd $LIBTOCK_C/examples/blink && TOCK_TARGETS=cortex-m3 make + $ cat $LIBTOCK_C/examples/blink/build/cortex-m3/cortex-m3.tbf \ + $LIBTOCK_C/examples/c_hello/build/cortex-m3/cortex-m3.tbf \ + > apps.bin + $ make run-app APP=$PWD/apps.bin + [...] + QEMU MPS2 AN385 (Cortex-M3) initialization complete. + Entering main loop. + tock$ list + Hello World! + list + PID ShortID Name Quanta Syscalls Restarts Grants State + 0 Unique blink 183303 7 0 1/ 2 Running + 1 Unique c_hello 131473 8 0 0/ 2 Terminated + tock$ + ``` + + `blink`'s LED toggling was confirmed for real by reading the `FPGAIO` + `LED0` register directly (`0x40028000`) through the QEMU monitor + (`C-a c` to switch from the serial console, then `xp/1xw 0x40028000`) + while it ran, observing the value change between `0x00000000` and + `0x00000002`. diff --git a/boards/qemu_arm_mps2_an386/Makefile b/boards/qemu_arm_mps2_an386/Makefile index 72bd629afa9..b0961e336e7 100644 --- a/boards/qemu_arm_mps2_an386/Makefile +++ b/boards/qemu_arm_mps2_an386/Makefile @@ -9,6 +9,9 @@ include ../Makefile.common QEMU_CMD := qemu-system-arm +# Base address of the "prog" (app) flash region; must match chip_layout.ld. +APP_ADDRESS := 0x00040000 + # Peripherals attached by default: # - CMSDK APB UART0 (attached to stdio) QEMU_BASE_CMDLINE := \ @@ -28,3 +31,22 @@ run: $(TOCK_ROOT_DIRECTORY)target/$(TARGET)/release/$(PLATFORM).elf .PHONY: qemu qemu: run + +# Same as `run`, but load an app (or several apps concatenated together, +# e.g. via `cat app1.tbf app2.tbf > apps.bin` — see README.md for the flash +# alignment requirement when combining more than one) at $(APP_ADDRESS). +# +# Uses the `.bin` kernel image (with the placeholder `.apps` section +# stripped by the standard $(OBJCOPY_FLAGS)) rather than the `.elf`: QEMU +# refuses to load two overlapping ROM blobs, and the kernel `.elf`'s own +# 4-byte `.apps` placeholder otherwise collides with $(APP) at the same +# address. +.PHONY: run-app +run-app: $(TOCK_ROOT_DIRECTORY)target/$(TARGET)/release/$(PLATFORM).bin + @echo + @echo -e "Running $$($(QEMU_CMD) --version | head -n1) with\n"\ + " - kernel $<\n"\ + " - app(s) $(APP)" + @echo "To exit type C-a x" + @echo + $(QEMU_BASE_CMDLINE) -kernel $< -device loader,file=$(APP),addr=$(APP_ADDRESS) diff --git a/boards/qemu_arm_mps2_an386/README.md b/boards/qemu_arm_mps2_an386/README.md index 58f15183d80..75666ea506b 100644 --- a/boards/qemu_arm_mps2_an386/README.md +++ b/boards/qemu_arm_mps2_an386/README.md @@ -33,5 +33,9 @@ Running QEMU tock$ ``` +- **`run-app`**: same as `qemu_arm_mps2_an385`'s (`make run-app + APP=$PATH_TO_APP.tbf`), also verified end-to-end with `c_hello` and + `blink` (built with `TOCK_TARGETS=cortex-m4`) loaded at the same time. + See `qemu_arm_mps2_an385/README.md` for the memory layout (identical -addresses on both machines) and app-loading details. +addresses on both machines). From 380ae7809a17f58397013ff62ce574ef7a1fd1dd Mon Sep 17 00:00:00 2001 From: ppannuto-claude Date: Tue, 25 Aug 2026 23:56:45 -0700 Subject: [PATCH 3/6] Add SPI (PL022) support for qemu_arm_mps2_an385/an386 Drives one of the machine's five PL022 controllers (the "Shield0" instance at 0x40026000, IRQ 24) as an hil::spi::SpiMaster, modeled on chips/rp2040/src/spi.rs, which already drives the same PrimeCell IP block. This is the first SPI support on any Tock QEMU board. Two real constraints from this QEMU model, not implementation shortcuts: - None of the five PL022 instances have an SSI slave device attached in QEMU (hw/arm/mps2.c creates bare controllers, no ssi_create_peripheral), so a non-loopback transfer just reads back whatever the empty bus's default is. The driver always enables CR1.LBM (loopback) in init() -- the only way to get a genuine, deterministic transfer under this model. - No functional GPIO to toggle for chip select (see led.rs's existing GPIO-is-a-stub docs), so ChipSelect is a zero-sized placeholder per the user's direction, rather than the GPIO-pin-based chip select every other Tock SpiMaster wiring uses. Verified with a new loopback test app (write a known pattern, read it back, memcmp, print SPI PASS/FAIL) loaded alongside c_hello and blink on both boards -- confirming three concurrently-scheduled processes are all correctly serviced through this chip crate's UART, Timer, and SPI drivers at once. libtock-c's existing wip/spi/* examples predate the current libtock_spi_controller_* API and don't build against it, so this needed a new app rather than reviving one of those. Along the way, discovered that Tock's own capsules_core::spi_controller capsule has never implemented the "set chip select" command on any board (a hard-coded NOSUPPORT, "TODO: do nothing, for now") -- not a bug in this board, documented in the README so it isn't mistaken for one. Co-Authored-By: Claude Sonnet 5 --- boards/qemu_arm_mps2_an385/README.md | 40 ++- boards/qemu_arm_mps2_an385/src/main.rs | 24 ++ boards/qemu_arm_mps2_an386/README.md | 5 +- boards/qemu_arm_mps2_an386/src/main.rs | 24 ++ chips/qemu_arm_mps2_chip/src/interrupts.rs | 8 +- chips/qemu_arm_mps2_chip/src/lib.rs | 10 +- chips/qemu_arm_mps2_chip/src/spi.rs | 395 +++++++++++++++++++++ 7 files changed, 498 insertions(+), 8 deletions(-) create mode 100644 chips/qemu_arm_mps2_chip/src/spi.rs diff --git a/boards/qemu_arm_mps2_an385/README.md b/boards/qemu_arm_mps2_an385/README.md index a15a00b0952..896599eb35a 100644 --- a/boards/qemu_arm_mps2_an385/README.md +++ b/boards/qemu_arm_mps2_an385/README.md @@ -15,6 +15,8 @@ Currently supported peripherals: - One CMSDK APB Timer, backing the kernel's `Alarm`/`Time` HIL. - The `FPGAIO` block's `LED0` register, exposing the machine's two simulated LEDs. +- One PL022 SPI controller (the "Shield0" instance), run in hardware + loopback mode — see the note below. Not supported, and not planned for this machine specifically: @@ -26,8 +28,20 @@ Not supported, and not planned for this machine specifically: under this QEMU machine, so this board does not implement a GPIO capsule. LEDs are wired to the separate, genuinely-emulated `FPGAIO` register instead (see `chips/qemu_arm_mps2_chip/src/led.rs`). -- SPI, I2C, and the machine's LAN9118 Ethernet controller: present on the - memory map but not driven by this chip crate. +- I2C and the machine's LAN9118 Ethernet controller: present on the memory + map but not driven by this chip crate. + +**SPI note**: none of the machine's five PL022 instances have an SSI slave +device attached in QEMU, so a non-loopback transfer just reads back +whatever the empty bus's default is, not meaningful data. The driver +therefore always enables `CR1.LBM` (loopback) — see +`chips/qemu_arm_mps2_chip/src/spi.rs`'s module docs. Chip select is a +zero-sized placeholder for the same reason GPIO is unavailable: there's no +functional GPIO pin to toggle for it, and no real device to select in the +first place. Separately, Tock's own `capsules_core::spi_controller` capsule +has never implemented the "set chip select" command on any board (it's a +hard-coded `NOSUPPORT`, unrelated to this board) — see the comment in +`capsules/core/src/spi_controller.rs`. See also `qemu_arm_mps2_an386`, the Cortex-M4 sibling of this board: same peripheral map (an385/an386 differ only in CPU core), sharing this same @@ -105,3 +119,25 @@ Running an application (`C-a c` to switch from the serial console, then `xp/1xw 0x40028000`) while it ran, observing the value change between `0x00000000` and `0x00000002`. + + SPI was verified the same way, alongside `c_hello` and `blink`, with a + small loopback test app (write a known pattern, read it back over + `libtock_spi_controller_read_write`, `memcmp` the result, print + `SPI PASS`/`SPI FAIL`) — not currently part of `libtock-c`, since none of + its existing SPI examples build against the current + `libtock/peripherals/spi_controller.h` API. Loading it alongside the + other two confirmed three concurrently-scheduled processes all get + correctly serviced through this chip crate's UART, Timer, and SPI + drivers at once: + + ``` + tock$ list + Hello World! + SPI PASS + list + PID ShortID Name Quanta Syscalls Restarts Grants State + 0 Unique blink 470841 171 0 1/ 3 Running + 1 Unique spi_loopback_test 272451 18 0 0/ 3 Terminated + 2 Unique c_hello 238836 8 0 0/ 3 Terminated + tock$ + ``` diff --git a/boards/qemu_arm_mps2_an385/src/main.rs b/boards/qemu_arm_mps2_an385/src/main.rs index 594494907ff..5487d6e3081 100644 --- a/boards/qemu_arm_mps2_an385/src/main.rs +++ b/boards/qemu_arm_mps2_an385/src/main.rs @@ -55,6 +55,13 @@ struct QemuArmMps2An385 { qemu_arm_mps2_chip::timer::Timer<'static>, >, >, + spi: &'static capsules_core::spi_controller::Spi< + 'static, + capsules_core::virtualizers::virtual_spi::VirtualSpiMasterDevice< + 'static, + qemu_arm_mps2_chip::spi::Spi<'static>, + >, + >, } impl SyscallDriverLookup for QemuArmMps2An385 { @@ -66,6 +73,7 @@ impl SyscallDriverLookup for QemuArmMps2An385 { capsules_core::console::DRIVER_NUM => f(Some(self.console)), capsules_core::led::DRIVER_NUM => f(Some(self.led)), capsules_core::alarm::DRIVER_NUM => f(Some(self.alarm)), + capsules_core::spi_controller::DRIVER_NUM => f(Some(self.spi)), _ => f(None), } } @@ -183,6 +191,21 @@ unsafe fn start() -> ( )); let _ = process_console.start(); + let spi_mux = components::spi::SpiMuxComponent::new(&peripherals.spi_shield0).finalize( + components::spi_mux_component_static!(qemu_arm_mps2_chip::spi::Spi), + ); + + let spi = components::spi::SpiSyscallComponent::new( + board_kernel, + spi_mux, + qemu_arm_mps2_chip::spi::ChipSelect, + capsules_core::spi_controller::DRIVER_NUM, + create_capability!(capabilities::MemoryAllocationCapability), + ) + .finalize(components::spi_syscall_component_static!( + qemu_arm_mps2_chip::spi::Spi + )); + let led = components::led::LedsComponent::new().finalize(components::led_component_static!( qemu_arm_mps2_chip::led::Led<'static>, peripherals.fpgaio.led(0), @@ -200,6 +223,7 @@ unsafe fn start() -> ( systick: cortexm3::systick::SysTick::new(), led, alarm, + spi, } ); diff --git a/boards/qemu_arm_mps2_an386/README.md b/boards/qemu_arm_mps2_an386/README.md index 75666ea506b..2267975dac7 100644 --- a/boards/qemu_arm_mps2_an386/README.md +++ b/boards/qemu_arm_mps2_an386/README.md @@ -34,8 +34,9 @@ Running QEMU ``` - **`run-app`**: same as `qemu_arm_mps2_an385`'s (`make run-app - APP=$PATH_TO_APP.tbf`), also verified end-to-end with `c_hello` and - `blink` (built with `TOCK_TARGETS=cortex-m4`) loaded at the same time. + APP=$PATH_TO_APP.tbf`), also verified end-to-end with `c_hello`, `blink`, + and a SPI loopback test app (built with `TOCK_TARGETS=cortex-m4`) loaded + at the same time — including `SPI PASS` from the loopback test. See `qemu_arm_mps2_an385/README.md` for the memory layout (identical addresses on both machines). diff --git a/boards/qemu_arm_mps2_an386/src/main.rs b/boards/qemu_arm_mps2_an386/src/main.rs index be528229ae5..f8f1b70498b 100644 --- a/boards/qemu_arm_mps2_an386/src/main.rs +++ b/boards/qemu_arm_mps2_an386/src/main.rs @@ -55,6 +55,13 @@ struct QemuArmMps2An386 { qemu_arm_mps2_chip::timer::Timer<'static>, >, >, + spi: &'static capsules_core::spi_controller::Spi< + 'static, + capsules_core::virtualizers::virtual_spi::VirtualSpiMasterDevice< + 'static, + qemu_arm_mps2_chip::spi::Spi<'static>, + >, + >, } impl SyscallDriverLookup for QemuArmMps2An386 { @@ -66,6 +73,7 @@ impl SyscallDriverLookup for QemuArmMps2An386 { capsules_core::console::DRIVER_NUM => f(Some(self.console)), capsules_core::led::DRIVER_NUM => f(Some(self.led)), capsules_core::alarm::DRIVER_NUM => f(Some(self.alarm)), + capsules_core::spi_controller::DRIVER_NUM => f(Some(self.spi)), _ => f(None), } } @@ -183,6 +191,21 @@ unsafe fn start() -> ( )); let _ = process_console.start(); + let spi_mux = components::spi::SpiMuxComponent::new(&peripherals.spi_shield0).finalize( + components::spi_mux_component_static!(qemu_arm_mps2_chip::spi::Spi), + ); + + let spi = components::spi::SpiSyscallComponent::new( + board_kernel, + spi_mux, + qemu_arm_mps2_chip::spi::ChipSelect, + capsules_core::spi_controller::DRIVER_NUM, + create_capability!(capabilities::MemoryAllocationCapability), + ) + .finalize(components::spi_syscall_component_static!( + qemu_arm_mps2_chip::spi::Spi + )); + let led = components::led::LedsComponent::new().finalize(components::led_component_static!( qemu_arm_mps2_chip::led::Led<'static>, peripherals.fpgaio.led(0), @@ -200,6 +223,7 @@ unsafe fn start() -> ( systick: cortexm4::systick::SysTick::new(), led, alarm, + spi, } ); diff --git a/chips/qemu_arm_mps2_chip/src/interrupts.rs b/chips/qemu_arm_mps2_chip/src/interrupts.rs index 130c63dd004..fcbbfc0bc1e 100644 --- a/chips/qemu_arm_mps2_chip/src/interrupts.rs +++ b/chips/qemu_arm_mps2_chip/src/interrupts.rs @@ -6,8 +6,9 @@ //! //! Taken from QEMU's `hw/arm/mps2.c` (`mps2_common_init`), which is shared //! by both FPGA images. Only the peripherals this chip crate actually -//! drives are listed; the MPS2 image wires up several more (SPI, I2C, -//! Ethernet) that are out of scope here. +//! drives are listed; the MPS2 image wires up several more (I2C, Ethernet) +//! that are out of scope here. The watchdog is NMI-driven, not a normal +//! NVIC line, so it has no entry here. pub const UART0_RX: u32 = 0; pub const UART0_TX: u32 = 1; @@ -18,6 +19,9 @@ pub const UART2_TX: u32 = 5; pub const TIMER0: u32 = 8; pub const TIMER1: u32 = 9; pub const DUALTIMER: u32 = 10; +/// Shared by the Shield0 and Shield1 PL022 instances via an OR-gate; only +/// Shield0 is driven by this chip crate. +pub const SPI_SHIELD: u32 = 24; pub const UART3_RX: u32 = 18; pub const UART3_TX: u32 = 19; pub const UART4_RX: u32 = 20; diff --git a/chips/qemu_arm_mps2_chip/src/lib.rs b/chips/qemu_arm_mps2_chip/src/lib.rs index 4db5c411a57..1e3e77c7baa 100644 --- a/chips/qemu_arm_mps2_chip/src/lib.rs +++ b/chips/qemu_arm_mps2_chip/src/lib.rs @@ -9,6 +9,7 @@ pub mod chip; pub mod interrupts; pub mod led; +pub mod spi; pub mod timer; pub mod uart; @@ -26,12 +27,15 @@ pub const SYSCLK_FRQ: u32 = 25_000_000; /// Instantiates the peripherals this chip crate drives. /// -/// Only UART0 and Timer0 are wired up as the console and alarm backing; -/// UART1-4 and Timer1 exist on the real memory map but are unused here. +/// Only UART0, Timer0, and the "Shield0" PL022 are wired up (console/alarm +/// backing, and the syscall-facing SPI controller); UART1-4, Timer1, and +/// the other four PL022 instances exist on the real memory map but are +/// unused here. pub struct Mps2DefaultPeripherals<'a> { pub uart0: uart::Uart<'a>, pub timer0: timer::Timer<'a>, pub fpgaio: led::Fpgaio, + pub spi_shield0: spi::Spi<'a>, } impl Mps2DefaultPeripherals<'_> { @@ -40,6 +44,7 @@ impl Mps2DefaultPeripherals<'_> { uart0: uart::Uart::new(uart::UART0_BASE), timer0: timer::Timer::new(timer::TIMER0_BASE), fpgaio: led::Fpgaio::new(led::FPGAIO_BASE), + spi_shield0: spi::Spi::new(spi::SPI_SHIELD0_BASE), } } } @@ -55,6 +60,7 @@ impl InterruptService for Mps2DefaultPeripherals<'_> { match interrupt { interrupts::UART0_RX | interrupts::UART0_TX => self.uart0.handle_interrupt(), interrupts::TIMER0 => self.timer0.handle_interrupt(), + interrupts::SPI_SHIELD => self.spi_shield0.handle_interrupt(), _ => return false, } true diff --git a/chips/qemu_arm_mps2_chip/src/spi.rs b/chips/qemu_arm_mps2_chip/src/spi.rs new file mode 100644 index 00000000000..32e625215e4 --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/spi.rs @@ -0,0 +1,395 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! ARM PL022 (PrimeCell SSP) SPI controller, as found on the MPS2 +//! AN385/AN386 FPGA images. +//! +//! Of the five PL022 instances on this machine, only the "Shield0" one +//! (`0x40026000`) is driven here. +//! +//! Two real limitations of this QEMU model shape this driver: +//! +//! - **No SSI slave device is attached to any of the five PL022 instances** +//! in QEMU (`hw/arm/mps2.c` creates bare `TYPE_PL022` controllers with no +//! `ssi_create_peripheral`), so a non-loopback transfer just reads back +//! whatever QEMU's empty-bus default is, not meaningful data. This driver +//! therefore always enables `CR1.LBM` (loopback) in [`Spi::init`] — the +//! only way to get a genuine, deterministic transfer under this model. +//! - **No functional chip select.** Real `SpiMaster` implementations +//! elsewhere toggle a GPIO pin for chip select, but GPIO is a QEMU stub +//! on this machine (see `led.rs`'s module docs). [`ChipSelect`] is a +//! zero-sized placeholder that satisfies the trait without touching any +//! hardware. +//! +//! QEMU's PL022 also does not model SPI clock timing at all: `CR0`'s clock +//! format bits and `CPSR`'s prescaler are accepted and stored, but have no +//! effect on transfer behavior (transfers are synchronous and immediate in +//! the emulation). + +use core::cell::Cell; +use core::cmp; + +use kernel::ErrorCode; +use kernel::hil::spi::{ClockPhase, ClockPolarity, SpiMaster, SpiMasterClient}; +use kernel::utilities::StaticRef; +use kernel::utilities::cells::{MapCell, OptionalCell}; +use kernel::utilities::leasable_buffer::SubSliceMut; +use kernel::utilities::registers::interfaces::{ReadWriteable, Readable, Writeable}; +use kernel::utilities::registers::{ReadOnly, ReadWrite, register_bitfields, register_structs}; + +use crate::SYSCLK_FRQ; + +pub const SPI_SHIELD0_BASE: StaticRef = + unsafe { StaticRef::new(0x4002_6000 as *const SpiRegisters) }; + +register_structs! { + pub SpiRegisters { + (0x000 => cr0: ReadWrite), + (0x004 => cr1: ReadWrite), + (0x008 => dr: ReadWrite), + (0x00c => sr: ReadOnly), + (0x010 => cpsr: ReadWrite), + (0x014 => imsc: ReadWrite), + (0x018 => ris: ReadOnly), + (0x01c => mis: ReadOnly), + (0x020 => icr: ReadWrite), + (0x024 => dmacr: ReadWrite), + (0x028 => _reserved0), + (0x1000 => @END), + } +} + +register_bitfields![u32, + CR0 [ + Scr OFFSET(8) NUMBITS(8) [], + Sph OFFSET(7) NUMBITS(1) [], + Spo OFFSET(6) NUMBITS(1) [], + Frf OFFSET(4) NUMBITS(2) [ + Motorola = 0b00, + ], + Dss OFFSET(0) NUMBITS(4) [ + Data8Bit = 0b0111, + ], + ], + CR1 [ + Sod OFFSET(3) NUMBITS(1) [], + Ms OFFSET(2) NUMBITS(1) [], + Sse OFFSET(1) NUMBITS(1) [], + Lbm OFFSET(0) NUMBITS(1) [], + ], + DR [ + Data OFFSET(0) NUMBITS(16) [], + ], + SR [ + Bsy OFFSET(4) NUMBITS(1) [], + Rff OFFSET(3) NUMBITS(1) [], + Rne OFFSET(2) NUMBITS(1) [], + Tnf OFFSET(1) NUMBITS(1) [], + Tfe OFFSET(0) NUMBITS(1) [], + ], + CPSR [ + Cpsdvsr OFFSET(0) NUMBITS(8) [], + ], + IMSC [ + Txim OFFSET(3) NUMBITS(1) [], + Rxim OFFSET(2) NUMBITS(1) [], + Rtim OFFSET(1) NUMBITS(1) [], + Rorim OFFSET(0) NUMBITS(1) [], + ], + RIS [ + Txris OFFSET(3) NUMBITS(1) [], + Rxris OFFSET(2) NUMBITS(1) [], + ], + MIS [ + Txmis OFFSET(3) NUMBITS(1) [], + Rxmis OFFSET(2) NUMBITS(1) [], + ], + ICR [ + Rtic OFFSET(1) NUMBITS(1) [], + Roric OFFSET(0) NUMBITS(1) [], + ], + DMACR [ + Txdmae OFFSET(1) NUMBITS(1) [], + Rxdmae OFFSET(0) NUMBITS(1) [], + ], +]; + +const SPI_IDLE: u8 = 0b000; +const SPI_WRITE_IN_PROGRESS: u8 = 0b001; +const SPI_READ_IN_PROGRESS: u8 = 0b010; +const SPI_IN_PROGRESS: u8 = 0b100; + +/// Placeholder chip-select: this board has no functional GPIO to toggle a +/// real one, and none of the PL022 instances on this QEMU machine have a +/// slave device attached to select in the first place. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ChipSelect; + +pub struct Spi<'a> { + registers: StaticRef, + client: OptionalCell<&'a dyn SpiMasterClient>, + + tx_buffer: MapCell>, + tx_position: Cell, + rx_buffer: MapCell>, + rx_position: Cell, + len: Cell, + + transfer_state: Cell, +} + +impl<'a> Spi<'a> { + pub const fn new(registers: StaticRef) -> Spi<'a> { + Spi { + registers, + client: OptionalCell::empty(), + tx_buffer: MapCell::empty(), + tx_position: Cell::new(0), + rx_buffer: MapCell::empty(), + rx_position: Cell::new(0), + len: Cell::new(0), + transfer_state: Cell::new(SPI_IDLE), + } + } + + fn enable(&self) { + self.registers.cr1.modify(CR1::Sse::SET); + } + + fn disable(&self) { + self.registers.cr1.modify(CR1::Sse::CLEAR); + } + + pub fn handle_interrupt(&self) { + if self.registers.sr.is_set(SR::Tfe) { + if self.tx_buffer.is_some() { + while self.registers.sr.is_set(SR::Tnf) && self.tx_position.get() < self.len.get() { + self.tx_buffer.map(|buf| { + self.registers + .dr + .write(DR::Data.val(buf[self.tx_position.get()] as u32)); + self.tx_position.set(self.tx_position.get() + 1); + }); + } + if self.tx_position.get() >= self.len.get() { + self.transfer_state + .set(self.transfer_state.get() & !SPI_WRITE_IN_PROGRESS); + } + } else { + self.registers.imsc.modify(IMSC::Txim::CLEAR); + } + } + + while self.registers.sr.is_set(SR::Rne) { + let byte = self.registers.dr.read(DR::Data) as u8; + if self.rx_buffer.is_some() && self.rx_position.get() < self.len.get() { + self.rx_buffer.map(|buf| { + buf[self.rx_position.get()] = byte; + }); + self.rx_position.set(self.rx_position.get() + 1); + } + } + if self.rx_position.get() >= self.len.get() { + self.transfer_state + .set(self.transfer_state.get() & !SPI_READ_IN_PROGRESS); + } + + if self.transfer_state.get() == SPI_IN_PROGRESS + && self.registers.sr.is_set(SR::Tfe) + && !self.registers.sr.is_set(SR::Bsy) + { + self.client.map(|client| { + self.registers.imsc.modify(IMSC::Txim::CLEAR); + self.registers.imsc.modify(IMSC::Rxim::CLEAR); + self.disable(); + self.transfer_state.set(SPI_IDLE); + self.tx_buffer.take().map(|buf| { + client.read_write_done(buf, self.rx_buffer.take(), Ok(self.len.get())) + }); + }); + } + } +} + +impl<'a> SpiMaster<'a> for Spi<'a> { + type ChipSelect = ChipSelect; + + fn set_client(&self, client: &'a dyn SpiMasterClient) { + self.client.set(client); + } + + fn init(&self) -> Result<(), ErrorCode> { + self.registers.cr0.modify(CR0::Dss::Data8Bit); + self.registers.cr0.modify(CR0::Frf::Motorola); + self.registers.cr0.modify(CR0::Spo::CLEAR); + self.registers.cr0.modify(CR0::Sph::CLEAR); + // See the module docs: no SSI slave is attached under QEMU, so + // loopback is the only way to get a real transfer. + self.registers.cr1.modify(CR1::Lbm::SET); + // Master mode (Cr1::Ms::CLEAR); slave mode isn't implemented in + // QEMU's PL022 model anyway. + self.registers.cr1.modify(CR1::Ms::CLEAR); + self.set_rate(1_000_000)?; + Ok(()) + } + + fn is_busy(&self) -> bool { + self.transfer_state.get() != SPI_IDLE + } + + fn read_write_bytes( + &self, + write_buffer: SubSliceMut<'static, u8>, + read_buffer: Option>, + ) -> Result< + (), + ( + ErrorCode, + SubSliceMut<'static, u8>, + Option>, + ), + > { + if self.is_busy() { + return Err((ErrorCode::BUSY, write_buffer, read_buffer)); + } + + let len = match read_buffer.as_ref() { + Some(rb) => cmp::min(write_buffer.len(), rb.len()), + None => write_buffer.len(), + }; + if len == 0 { + return Err((ErrorCode::INVAL, write_buffer, read_buffer)); + } + + self.enable(); + self.registers.imsc.modify(IMSC::Txim::CLEAR); + self.registers.imsc.modify(IMSC::Rxim::CLEAR); + + self.len.set(len); + let mut state = SPI_IN_PROGRESS | SPI_WRITE_IN_PROGRESS; + + self.tx_position.set(0); + self.tx_buffer.replace(write_buffer); + self.registers.imsc.modify(IMSC::Txim::SET); + + if let Some(rb) = read_buffer { + state |= SPI_READ_IN_PROGRESS; + self.rx_position.set(0); + self.rx_buffer.replace(rb); + self.registers.imsc.modify(IMSC::Rxim::SET); + } + + self.transfer_state.set(state); + Ok(()) + } + + fn write_byte(&self, val: u8) -> Result<(), ErrorCode> { + if self.is_busy() { + return Err(ErrorCode::BUSY); + } + while !self.registers.sr.is_set(SR::Tnf) {} + self.registers.dr.write(DR::Data.val(val as u32)); + Ok(()) + } + + fn read_byte(&self) -> Result { + self.read_write_byte(0) + } + + fn read_write_byte(&self, val: u8) -> Result { + if self.is_busy() { + return Err(ErrorCode::BUSY); + } + self.enable(); + self.write_byte(val)?; + while !self.registers.sr.is_set(SR::Rne) {} + let byte = self.registers.dr.read(DR::Data) as u8; + self.disable(); + Ok(byte) + } + + fn specify_chip_select(&self, _cs: Self::ChipSelect) -> Result<(), ErrorCode> { + Ok(()) + } + + fn set_rate(&self, rate: u32) -> Result { + // QEMU's PL022 does not model timing at all, so this only affects + // what get_rate() reports back, not actual transfer speed. The + // divider math (CPSR * (SCR+1)) mirrors the real PL022 hardware + // formula so real-hardware callers get a sane answer too. + if rate == 0 || rate > SYSCLK_FRQ { + return Err(ErrorCode::INVAL); + } + + let mut prescale = 0u32; + let mut postdiv = 0u32; + for p in (2..254).step_by(2) { + if (SYSCLK_FRQ as u64) < ((p + 2) as u64 * 256 * rate as u64) { + prescale = p; + break; + } + } + for p in (2..256).rev() { + if prescale > 0 && (SYSCLK_FRQ / (prescale * (p - 1))) > rate { + postdiv = p; + break; + } + } + + if prescale == 0 || postdiv == 0 { + return Err(ErrorCode::INVAL); + } + + self.registers.cpsr.write(CPSR::Cpsdvsr.val(prescale)); + self.registers.cr0.modify(CR0::Scr.val(postdiv - 1)); + Ok(SYSCLK_FRQ / (prescale * postdiv)) + } + + fn get_rate(&self) -> u32 { + let prescale = self.registers.cpsr.read(CPSR::Cpsdvsr).max(1); + let postdiv = self.registers.cr0.read(CR0::Scr) + 1; + SYSCLK_FRQ / (prescale * postdiv) + } + + fn set_polarity(&self, polarity: ClockPolarity) -> Result<(), ErrorCode> { + if self.is_busy() { + return Err(ErrorCode::BUSY); + } + match polarity { + ClockPolarity::IdleHigh => self.registers.cr0.modify(CR0::Spo::SET), + ClockPolarity::IdleLow => self.registers.cr0.modify(CR0::Spo::CLEAR), + } + Ok(()) + } + + fn get_polarity(&self) -> ClockPolarity { + if self.registers.cr0.is_set(CR0::Spo) { + ClockPolarity::IdleHigh + } else { + ClockPolarity::IdleLow + } + } + + fn set_phase(&self, phase: ClockPhase) -> Result<(), ErrorCode> { + if self.is_busy() { + return Err(ErrorCode::BUSY); + } + match phase { + ClockPhase::SampleTrailing => self.registers.cr0.modify(CR0::Sph::SET), + ClockPhase::SampleLeading => self.registers.cr0.modify(CR0::Sph::CLEAR), + } + Ok(()) + } + + fn get_phase(&self) -> ClockPhase { + if self.registers.cr0.is_set(CR0::Sph) { + ClockPhase::SampleTrailing + } else { + ClockPhase::SampleLeading + } + } + + fn hold_low(&self) {} + fn release_low(&self) {} +} From 3b692b89480756003250afbf74c4496c2608a67f Mon Sep 17 00:00:00 2001 From: ppannuto-claude Date: Wed, 26 Aug 2026 00:12:04 -0700 Subject: [PATCH 4/6] Add Watchdog support for qemu_arm_mps2_an385/an386 Drives the CMSDK APB Watchdog (SP805-style, base 0x40008000) as the kernel's WatchDog resource. Unlike GPIO, this is a real, non-stub QEMU peripheral: it genuinely counts down and genuinely resets the machine. Its interrupt is wired to NMI, not a normal NVIC line, so unlike the other peripherals in this chip crate it's never dispatched through InterruptService -- this board's vector table already maps NMI to unhandled_interrupt, so a missed kick surfaces as a panic first. Verified in two parts, since a well-behaved userspace app can't legitimately hang the kernel to trigger the real failure mode: - Negative test: ran c_hello, blink, and spi_loopback_test together for ~18s (~9x the ~2s reload period) and confirmed the boot banner printed exactly once -- no spurious reset under real interrupt/ scheduling load. - Positive test (one-off, not shipped): temporarily commented out the tickle() call in kernel/src/kernel.rs. Discovered along the way that idle sleep/wake cycles alone also incidentally reload the watchdog (setting INTEN high after being disabled reloads from WDOGLOAD -- a real SP805 hardware property, not a QEMU quirk), so suspend()/ resume() had to be neutralized too to actually provoke an expiry. Result: NMI fired on schedule (panicked with "Unhandled Interrupt. ISR 2 is active."), and since the panic loop doesn't kick the dog either, QEMU genuinely reset the machine shortly after, repeating in a clean cycle. Both changes reverted immediately after, confirmed via an identical post-revert binary hash. Co-Authored-By: Claude Sonnet 5 --- boards/qemu_arm_mps2_an385/README.md | 31 ++++++ boards/qemu_arm_mps2_an385/src/main.rs | 6 +- boards/qemu_arm_mps2_an386/README.md | 7 +- boards/qemu_arm_mps2_an386/src/main.rs | 6 +- chips/qemu_arm_mps2_chip/src/lib.rs | 3 + chips/qemu_arm_mps2_chip/src/watchdog.rs | 117 +++++++++++++++++++++++ 6 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 chips/qemu_arm_mps2_chip/src/watchdog.rs diff --git a/boards/qemu_arm_mps2_an385/README.md b/boards/qemu_arm_mps2_an385/README.md index 896599eb35a..5706657424d 100644 --- a/boards/qemu_arm_mps2_an385/README.md +++ b/boards/qemu_arm_mps2_an385/README.md @@ -17,6 +17,10 @@ Currently supported peripherals: simulated LEDs. - One PL022 SPI controller (the "Shield0" instance), run in hardware loopback mode — see the note below. +- The CMSDK APB Watchdog, backing the kernel's `WatchDog` resource — see + `chips/qemu_arm_mps2_chip/src/watchdog.rs`'s module docs. Unlike GPIO, + this is a real, non-stub peripheral: it genuinely counts down and + genuinely resets the machine. Not supported, and not planned for this machine specifically: @@ -141,3 +145,30 @@ Running an application 2 Unique c_hello 238836 8 0 0/ 3 Terminated tock$ ``` + +Watchdog +-------- + +`kernel::platform::watchdog::WatchDog::tickle()` is called once per +scheduling decision (i.e. very frequently), so a well-behaved kernel never +comes close to the ~2 second reload margin. That means the only way to +prove the watchdog actually works is to fake a hang, which a normal +userspace app can't legitimately do — process isolation exists precisely +so an app can't hang the kernel. So this was checked in two parts: + +- **Negative test (the userspace-facing one)**: ran `c_hello`, `blink`, + and `spi_loopback_test` together for ~18 seconds (about 9x the reload + period) and confirmed the boot banner printed exactly once — no spurious + reset, proving `tickle()` integration doesn't false-positive under real + interrupt/scheduling load. +- **Positive test (kernel-side, one-off, not shipped)**: temporarily + commented out the `tickle()` call in `kernel/src/kernel.rs` (and, since + every idle sleep/wake cycle *also* reloads the watchdog as an + unavoidable property of this hardware, temporarily neutralized + `suspend()`/`resume()` in `watchdog.rs` too) and reran. Result: NMI + fired almost exactly on schedule (`panicked at arch/cortex-m/src/lib.rs: + ...: Unhandled Interrupt. ISR 2 is active.`), and since the panic loop + doesn't kick the dog either, QEMU genuinely reset the machine — the boot + banner reprinted — roughly one reload period later, repeating in a + clean cycle. Both changes were reverted immediately after (confirmed via + an identical post-revert binary hash before moving on). diff --git a/boards/qemu_arm_mps2_an385/src/main.rs b/boards/qemu_arm_mps2_an385/src/main.rs index 5487d6e3081..17dd8a2f4c6 100644 --- a/boards/qemu_arm_mps2_an385/src/main.rs +++ b/boards/qemu_arm_mps2_an385/src/main.rs @@ -62,6 +62,7 @@ struct QemuArmMps2An385 { qemu_arm_mps2_chip::spi::Spi<'static>, >, >, + watchdog: &'static qemu_arm_mps2_chip::watchdog::Watchdog, } impl SyscallDriverLookup for QemuArmMps2An385 { @@ -85,7 +86,7 @@ impl KernelResources for QemuArmMps2An385 { type ProcessFault = (); type Scheduler = SchedulerInUse; type SchedulerTimer = cortexm3::systick::SysTick; - type WatchDog = (); + type WatchDog = qemu_arm_mps2_chip::watchdog::Watchdog; type ContextSwitchCallback = (); fn syscall_driver_lookup(&self) -> &Self::SyscallDriverLookup { @@ -104,7 +105,7 @@ impl KernelResources for QemuArmMps2An385 { &self.systick } fn watchdog(&self) -> &Self::WatchDog { - &() + self.watchdog } fn context_switch_callback(&self) -> &Self::ContextSwitchCallback { &() @@ -224,6 +225,7 @@ unsafe fn start() -> ( led, alarm, spi, + watchdog: &peripherals.watchdog, } ); diff --git a/boards/qemu_arm_mps2_an386/README.md b/boards/qemu_arm_mps2_an386/README.md index 2267975dac7..0a87acec63e 100644 --- a/boards/qemu_arm_mps2_an386/README.md +++ b/boards/qemu_arm_mps2_an386/README.md @@ -36,7 +36,10 @@ Running QEMU - **`run-app`**: same as `qemu_arm_mps2_an385`'s (`make run-app APP=$PATH_TO_APP.tbf`), also verified end-to-end with `c_hello`, `blink`, and a SPI loopback test app (built with `TOCK_TARGETS=cortex-m4`) loaded - at the same time — including `SPI PASS` from the loopback test. + at the same time — including `SPI PASS` from the loopback test, and the + same negative watchdog check (no spurious reset over ~9 reload periods). See `qemu_arm_mps2_an385/README.md` for the memory layout (identical -addresses on both machines). +addresses on both machines) and the watchdog positive-test methodology +(only run once, on an385, since the peripheral and kernel logic are +identical on both boards). diff --git a/boards/qemu_arm_mps2_an386/src/main.rs b/boards/qemu_arm_mps2_an386/src/main.rs index f8f1b70498b..c19f76980a7 100644 --- a/boards/qemu_arm_mps2_an386/src/main.rs +++ b/boards/qemu_arm_mps2_an386/src/main.rs @@ -62,6 +62,7 @@ struct QemuArmMps2An386 { qemu_arm_mps2_chip::spi::Spi<'static>, >, >, + watchdog: &'static qemu_arm_mps2_chip::watchdog::Watchdog, } impl SyscallDriverLookup for QemuArmMps2An386 { @@ -85,7 +86,7 @@ impl KernelResources for QemuArmMps2An386 { type ProcessFault = (); type Scheduler = SchedulerInUse; type SchedulerTimer = cortexm4::systick::SysTick; - type WatchDog = (); + type WatchDog = qemu_arm_mps2_chip::watchdog::Watchdog; type ContextSwitchCallback = (); fn syscall_driver_lookup(&self) -> &Self::SyscallDriverLookup { @@ -104,7 +105,7 @@ impl KernelResources for QemuArmMps2An386 { &self.systick } fn watchdog(&self) -> &Self::WatchDog { - &() + self.watchdog } fn context_switch_callback(&self) -> &Self::ContextSwitchCallback { &() @@ -224,6 +225,7 @@ unsafe fn start() -> ( led, alarm, spi, + watchdog: &peripherals.watchdog, } ); diff --git a/chips/qemu_arm_mps2_chip/src/lib.rs b/chips/qemu_arm_mps2_chip/src/lib.rs index 1e3e77c7baa..88f568a5a38 100644 --- a/chips/qemu_arm_mps2_chip/src/lib.rs +++ b/chips/qemu_arm_mps2_chip/src/lib.rs @@ -12,6 +12,7 @@ pub mod led; pub mod spi; pub mod timer; pub mod uart; +pub mod watchdog; #[cfg(feature = "cortex-m3")] pub mod vectors_m3; @@ -36,6 +37,7 @@ pub struct Mps2DefaultPeripherals<'a> { pub timer0: timer::Timer<'a>, pub fpgaio: led::Fpgaio, pub spi_shield0: spi::Spi<'a>, + pub watchdog: watchdog::Watchdog, } impl Mps2DefaultPeripherals<'_> { @@ -45,6 +47,7 @@ impl Mps2DefaultPeripherals<'_> { timer0: timer::Timer::new(timer::TIMER0_BASE), fpgaio: led::Fpgaio::new(led::FPGAIO_BASE), spi_shield0: spi::Spi::new(spi::SPI_SHIELD0_BASE), + watchdog: watchdog::Watchdog::new(watchdog::WATCHDOG_BASE), } } } diff --git a/chips/qemu_arm_mps2_chip/src/watchdog.rs b/chips/qemu_arm_mps2_chip/src/watchdog.rs new file mode 100644 index 00000000000..f6420de65c2 --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/watchdog.rs @@ -0,0 +1,117 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! ARM CMSDK APB Watchdog (SP805-style), as found on the MPS2 AN385/AN386 +//! FPGA images. +//! +//! Unlike GPIO, this is a real, non-stub QEMU peripheral: it genuinely +//! counts down and genuinely resets the machine. The interrupt line is +//! wired to NMI, not a normal NVIC line (`hw/arm/mps2.c`), so it is not +//! dispatched through [`kernel::platform::chip::InterruptService`] the way +//! the other peripherals in this crate are — this driver only ever pokes +//! registers, it never installs an NMI handler of its own. +//! +//! Hardware behavior: the first countdown-to-zero with `INTEN` set raises +//! the (non-maskable) interrupt; if nobody kicks the watchdog +//! (`WDOGINTCLR`) before the *second* countdown-to-zero with `RESEN` also +//! set, QEMU performs an actual system reset +//! (`watchdog_perform_action()`), independent of whatever the guest's NMI +//! handler does. This board's vector table maps NMI to `unhandled_interrupt` +//! (a panic), so in practice that panic — not the hardware reset — is what +//! happens on a first missed kick; the reset only fires if something +//! prevents that panic from halting execution first. + +use kernel::platform::watchdog::WatchDog; +use kernel::utilities::StaticRef; +use kernel::utilities::registers::interfaces::{ReadWriteable, Writeable}; +use kernel::utilities::registers::{ReadWrite, register_bitfields, register_structs}; + +use crate::SYSCLK_FRQ; + +pub const WATCHDOG_BASE: StaticRef = + unsafe { StaticRef::new(0x4000_8000 as *const WatchdogRegisters) }; + +/// Unlock value documented in the CMSDK Watchdog TRM; writing anything else +/// to `WDOGLOCK` re-locks the device. +const WDOG_UNLOCK_VALUE: u32 = 0x1ACC_E551; + +/// Reload value giving a generous margin. +/// +/// `tickle()` is called once per `kernel_loop_operation` iteration (i.e. on +/// every scheduling decision), so only a genuine kernel hang could ever +/// miss enough kicks to matter. +const WDOG_RELOAD_TICKS: u32 = SYSCLK_FRQ * 2; + +register_structs! { + pub WatchdogRegisters { + (0x000 => wdogload: ReadWrite), + (0x004 => wdogvalue: ReadWrite), + (0x008 => wdogcontrol: ReadWrite), + (0x00c => wdogintclr: ReadWrite), + (0x010 => wdogris: ReadWrite), + (0x014 => wdogmis: ReadWrite), + (0x018 => _reserved0), + (0xc00 => wdoglock: ReadWrite), + (0xc04 => @END), + } +} + +register_bitfields![u32, + WDOGCONTROL [ + ResEn OFFSET(1) NUMBITS(1) [], + IntEn OFFSET(0) NUMBITS(1) [], + ], +]; + +pub struct Watchdog { + registers: StaticRef, +} + +impl Watchdog { + pub const fn new(registers: StaticRef) -> Self { + Watchdog { registers } + } + + fn unlock(&self) { + self.registers.wdoglock.set(WDOG_UNLOCK_VALUE); + } +} + +impl WatchDog for Watchdog { + fn setup(&self) { + self.unlock(); + // Writing WDOGLOAD also reloads the live counter. + self.registers.wdogload.set(WDOG_RELOAD_TICKS); + self.registers + .wdogcontrol + .write(WDOGCONTROL::IntEn::SET + WDOGCONTROL::ResEn::SET); + } + + fn tickle(&self) { + // Any value clears the pending interrupt and reloads from + // WDOGLOAD. + self.registers.wdogintclr.set(1); + } + + fn suspend(&self) { + self.registers.wdogcontrol.modify(WDOGCONTROL::IntEn::CLEAR); + } + + fn resume(&self) { + // Setting IntEn high after being disabled reloads from WDOGLOAD, + // so this resumes with a full margin rather than wherever the + // counter happened to be left -- an inherent property of this + // hardware (a real SP805 characteristic, not a QEMU quirk), not a + // choice made here. One consequence, confirmed while testing: + // every idle sleep/wake cycle incidentally re-arms the watchdog + // with a fresh margin, same as tickle() does. That's harmless for + // catching real hangs -- a kernel that's truly stuck either never + // reaches sleep() at all (a tight loop) or is stuck inside an + // interrupt handler with interrupts disabled (no sleep/wake churn + // either way) -- but it does mean this alone, without also + // disabling tickle(), isn't a sufficient way to manually provoke + // an expiry for testing. + self.registers.wdogcontrol.modify(WDOGCONTROL::IntEn::SET); + } +} From 9020faafff90f470f1b18c547da7b539ede758db Mon Sep 17 00:00:00 2001 From: ppannuto-claude Date: Wed, 26 Aug 2026 13:40:33 -0700 Subject: [PATCH 5/6] Address review feedback: dedupe board setup, add semihost exit - Extract the two boards' shared setup (syscall driver lookup, KernelResources, capsule/process init) into a new qemu_arm_mps2_lib crate, generic over cortexm::CortexMVariant. Each board's main.rs shrinks to just the concrete CortexM3/CortexM4 type argument, plus a static_init!() and a #[panic_handler] that structurally can't be generic (a static can't reference an enclosing generic function's own type parameter). - Add cortexm::support::semihost_command(), since no ARM semihosting primitive existed in-tree; call it from each board's panic handler and pass -semihosting to QEMU, so a panic now makes qemu-system-arm exit on its own instead of hanging until killed. - Trim both READMEs down to reference documentation: move LED observability into the peripheral list itself, drop verification transcripts and the watchdog test narrative (that belongs in commit history, not a permanent README), and fix an385/an386 chip_layout.ld comments (an386's had copy-pasted "AN385"). - Minor doc-comment cleanups in spi.rs/watchdog.rs. Co-Authored-By: Claude Sonnet 5 --- Cargo.lock | 25 +- Cargo.toml | 1 + arch/cortex-m/src/support.rs | 47 ++++ boards/qemu_arm_mps2_an385/Cargo.toml | 7 +- boards/qemu_arm_mps2_an385/Makefile | 3 +- boards/qemu_arm_mps2_an385/README.md | 105 +------- boards/qemu_arm_mps2_an385/chip_layout.ld | 9 +- boards/qemu_arm_mps2_an385/src/io.rs | 22 +- boards/qemu_arm_mps2_an385/src/main.rs | 273 ++------------------ boards/qemu_arm_mps2_an386/Cargo.toml | 7 +- boards/qemu_arm_mps2_an386/Makefile | 3 +- boards/qemu_arm_mps2_an386/README.md | 9 +- boards/qemu_arm_mps2_an386/chip_layout.ld | 11 +- boards/qemu_arm_mps2_an386/src/io.rs | 22 +- boards/qemu_arm_mps2_an386/src/main.rs | 273 ++------------------ boards/qemu_arm_mps2_lib/Cargo.toml | 22 ++ boards/qemu_arm_mps2_lib/src/lib.rs | 300 ++++++++++++++++++++++ chips/qemu_arm_mps2_chip/src/spi.rs | 2 +- chips/qemu_arm_mps2_chip/src/watchdog.rs | 17 +- 19 files changed, 514 insertions(+), 644 deletions(-) create mode 100644 boards/qemu_arm_mps2_lib/Cargo.toml create mode 100644 boards/qemu_arm_mps2_lib/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 0f3e2f97514..c5e0c301ebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1278,14 +1278,10 @@ dependencies = [ name = "qemu_arm_mps2_an385" version = "0.2.3-dev" dependencies = [ - "capsules-core", - "capsules-extra", - "capsules-system", - "components", - "cortexm", "cortexm3", "kernel", "qemu_arm_mps2_chip", + "qemu_arm_mps2_lib", "tock_build_scripts", ] @@ -1293,14 +1289,10 @@ dependencies = [ name = "qemu_arm_mps2_an386" version = "0.2.3-dev" dependencies = [ - "capsules-core", - "capsules-extra", - "capsules-system", - "components", - "cortexm", "cortexm4", "kernel", "qemu_arm_mps2_chip", + "qemu_arm_mps2_lib", "tock_build_scripts", ] @@ -1314,6 +1306,19 @@ dependencies = [ "kernel", ] +[[package]] +name = "qemu_arm_mps2_lib" +version = "0.2.3-dev" +dependencies = [ + "capsules-core", + "capsules-extra", + "capsules-system", + "components", + "cortexm", + "kernel", + "qemu_arm_mps2_chip", +] + [[package]] name = "qemu_i486_q35" version = "0.2.3-dev" diff --git a/Cargo.toml b/Cargo.toml index 3efa64e022e..5a7811e5f53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ members = [ "boards/nano33ble_rev2", "boards/qemu_arm_mps2_an385", "boards/qemu_arm_mps2_an386", + "boards/qemu_arm_mps2_lib", "boards/qemu_i486_q35", "boards/qemu_rv32_virt", "boards/qemu_rv64_virt", diff --git a/arch/cortex-m/src/support.rs b/arch/cortex-m/src/support.rs index 407d9bef9dd..07a703b6218 100644 --- a/arch/cortex-m/src/support.rs +++ b/arch/cortex-m/src/support.rs @@ -184,3 +184,50 @@ pub fn is_interrupt_context() -> bool { pub fn is_interrupt_context() -> bool { unimplemented!() } + +/// Issue an ARM semihosting call. +/// +/// `operation` is the semihosting operation number (e.g. `0x18` for +/// `SYS_EXIT`) and `parameter` is its operation-specific argument. Only +/// meaningful when running under a semihosting host (e.g. QEMU started with +/// `-semihosting`, or an attached debug probe); otherwise the `bkpt` +/// instruction traps with no host to service it. +#[cfg(any(doc, all(target_arch = "arm", target_os = "none")))] +#[inline(always)] +pub unsafe fn semihost_command(operation: u32, parameter: u32) -> u32 { + use core::arch::asm; + let result; + + // # Safety + // + // - INPUTS: r0/r1 are set to `operation`/`parameter`, per the ABI ARM + // semihosting defines (ARM's "Semihosting for AArch32 and AArch64" + // specification). + // - OUTPUTS: r0 is overwritten with the semihosting call's result. + // - Options set: + // - nostack: This does not use the stack. + // - Options not set: + // - nomem: not guaranteed in general -- some operations (e.g. + // `SYS_WRITEC`) dereference `parameter` as a pointer. + // - pure, readonly: not applicable, as above. + // - preserves_flags: not documented by the semihosting spec. + // - noreturn: we do fall through (there may be no host to service + // this call at all, e.g. real hardware with no debugger attached). + // - att_syntax: not on arm. + // - raw: not required. + unsafe { + asm!( + "bkpt #0xAB", + inout("r0") operation => result, + in("r1") parameter, + options(nostack), + ); + } + result +} + +/// Mock implementation for tests on Travis-CI. +#[cfg(not(any(doc, all(target_arch = "arm", target_os = "none"))))] +pub unsafe fn semihost_command(_operation: u32, _parameter: u32) -> u32 { + unimplemented!() +} diff --git a/boards/qemu_arm_mps2_an385/Cargo.toml b/boards/qemu_arm_mps2_an385/Cargo.toml index 99e8a3aebe2..cc98cad5f94 100644 --- a/boards/qemu_arm_mps2_an385/Cargo.toml +++ b/boards/qemu_arm_mps2_an385/Cargo.toml @@ -10,15 +10,10 @@ build = "../build.rs" edition.workspace = true [dependencies] -components = { path = "../components" } -cortexm = { path = "../../arch/cortex-m" } cortexm3 = { path = "../../arch/cortex-m3" } kernel = { path = "../../kernel" } qemu_arm_mps2_chip = { path = "../../chips/qemu_arm_mps2_chip", features = ["cortex-m3"] } - -capsules-core = { path = "../../capsules/core" } -capsules-extra = { path = "../../capsules/extra" } -capsules-system = { path = "../../capsules/system" } +qemu_arm_mps2_lib = { path = "../qemu_arm_mps2_lib" } [build-dependencies] tock_build_scripts = { path = "../build_scripts" } diff --git a/boards/qemu_arm_mps2_an385/Makefile b/boards/qemu_arm_mps2_an385/Makefile index 51674a4c244..f257144bee0 100644 --- a/boards/qemu_arm_mps2_an385/Makefile +++ b/boards/qemu_arm_mps2_an385/Makefile @@ -17,7 +17,8 @@ APP_ADDRESS := 0x00040000 QEMU_BASE_CMDLINE := \ $(QEMU_CMD) \ -machine mps2-an385 \ - -nographic + -nographic \ + -semihosting # Run the kernel inside a qemu-system-arm "mps2-an385" machine simulation. .PHONY: run diff --git a/boards/qemu_arm_mps2_an385/README.md b/boards/qemu_arm_mps2_an385/README.md index 5706657424d..bba9d93b17f 100644 --- a/boards/qemu_arm_mps2_an385/README.md +++ b/boards/qemu_arm_mps2_an385/README.md @@ -14,13 +14,13 @@ Currently supported peripherals: console/debug UART. - One CMSDK APB Timer, backing the kernel's `Alarm`/`Time` HIL. - The `FPGAIO` block's `LED0` register, exposing the machine's two - simulated LEDs. + simulated LEDs. There's no display on this `-nographic` machine to show + them; observe their state by reading the register directly through the + QEMU monitor (`C-a c` to switch from the serial console, then `xp/1xw + 0x40028000`). - One PL022 SPI controller (the "Shield0" instance), run in hardware loopback mode — see the note below. -- The CMSDK APB Watchdog, backing the kernel's `WatchDog` resource — see - `chips/qemu_arm_mps2_chip/src/watchdog.rs`'s module docs. Unlike GPIO, - this is a real, non-stub peripheral: it genuinely counts down and - genuinely resets the machine. +- The CMSDK APB Watchdog, backing the kernel's `WatchDog` resource. Not supported, and not planned for this machine specifically: @@ -42,14 +42,7 @@ therefore always enables `CR1.LBM` (loopback) — see `chips/qemu_arm_mps2_chip/src/spi.rs`'s module docs. Chip select is a zero-sized placeholder for the same reason GPIO is unavailable: there's no functional GPIO pin to toggle for it, and no real device to select in the -first place. Separately, Tock's own `capsules_core::spi_controller` capsule -has never implemented the "set chip select" command on any board (it's a -hard-coded `NOSUPPORT`, unrelated to this board) — see the comment in -`capsules/core/src/spi_controller.rs`. - -See also `qemu_arm_mps2_an386`, the Cortex-M4 sibling of this board: same -peripheral map (an385/an386 differ only in CPU core), sharing this same -`qemu_arm_mps2_chip` crate. +first place. Running QEMU ------------ @@ -80,11 +73,7 @@ device on this machine to show regardless). With the default linker script, this board loads processes from flash=0x00040000-0x0007FFFF into ram=0x21004000-0x2101FFFF (RAM above the -kernel's own static allocations). Kernel and app flash are both well within -QEMU's hard 4 MiB cap for code at address 0 (`armv7m_load_kernel(..., 0, -0x400000)` in `hw/arm/mps2.c`); RAM is a modest slice of the 16 MiB QEMU -always backs at 0x21000000 regardless of what a board's linker script -claims. +kernel's own static allocations). Running an application ----------------------- @@ -96,79 +85,7 @@ Running an application $ make run-app APP=$PATH_TO_APP.tbf ``` - Verified end-to-end with `libtock-c`'s `c_hello` and `blink` examples, - built for `TOCK_TARGETS=cortex-m3` and loaded **at the same time**: - - ``` - $ cd $LIBTOCK_C/examples/c_hello && TOCK_TARGETS=cortex-m3 make - $ cd $LIBTOCK_C/examples/blink && TOCK_TARGETS=cortex-m3 make - $ cat $LIBTOCK_C/examples/blink/build/cortex-m3/cortex-m3.tbf \ - $LIBTOCK_C/examples/c_hello/build/cortex-m3/cortex-m3.tbf \ - > apps.bin - $ make run-app APP=$PWD/apps.bin - [...] - QEMU MPS2 AN385 (Cortex-M3) initialization complete. - Entering main loop. - tock$ list - Hello World! - list - PID ShortID Name Quanta Syscalls Restarts Grants State - 0 Unique blink 183303 7 0 1/ 2 Running - 1 Unique c_hello 131473 8 0 0/ 2 Terminated - tock$ - ``` - - `blink`'s LED toggling was confirmed for real by reading the `FPGAIO` - `LED0` register directly (`0x40028000`) through the QEMU monitor - (`C-a c` to switch from the serial console, then `xp/1xw 0x40028000`) - while it ran, observing the value change between `0x00000000` and - `0x00000002`. - - SPI was verified the same way, alongside `c_hello` and `blink`, with a - small loopback test app (write a known pattern, read it back over - `libtock_spi_controller_read_write`, `memcmp` the result, print - `SPI PASS`/`SPI FAIL`) — not currently part of `libtock-c`, since none of - its existing SPI examples build against the current - `libtock/peripherals/spi_controller.h` API. Loading it alongside the - other two confirmed three concurrently-scheduled processes all get - correctly serviced through this chip crate's UART, Timer, and SPI - drivers at once: - - ``` - tock$ list - Hello World! - SPI PASS - list - PID ShortID Name Quanta Syscalls Restarts Grants State - 0 Unique blink 470841 171 0 1/ 3 Running - 1 Unique spi_loopback_test 272451 18 0 0/ 3 Terminated - 2 Unique c_hello 238836 8 0 0/ 3 Terminated - tock$ - ``` - -Watchdog --------- - -`kernel::platform::watchdog::WatchDog::tickle()` is called once per -scheduling decision (i.e. very frequently), so a well-behaved kernel never -comes close to the ~2 second reload margin. That means the only way to -prove the watchdog actually works is to fake a hang, which a normal -userspace app can't legitimately do — process isolation exists precisely -so an app can't hang the kernel. So this was checked in two parts: - -- **Negative test (the userspace-facing one)**: ran `c_hello`, `blink`, - and `spi_loopback_test` together for ~18 seconds (about 9x the reload - period) and confirmed the boot banner printed exactly once — no spurious - reset, proving `tickle()` integration doesn't false-positive under real - interrupt/scheduling load. -- **Positive test (kernel-side, one-off, not shipped)**: temporarily - commented out the `tickle()` call in `kernel/src/kernel.rs` (and, since - every idle sleep/wake cycle *also* reloads the watchdog as an - unavoidable property of this hardware, temporarily neutralized - `suspend()`/`resume()` in `watchdog.rs` too) and reran. Result: NMI - fired almost exactly on schedule (`panicked at arch/cortex-m/src/lib.rs: - ...: Unhandled Interrupt. ISR 2 is active.`), and since the panic loop - doesn't kick the dog either, QEMU genuinely reset the machine — the boot - banner reprinted — roughly one reload period later, repeating in a - clean cycle. Both changes were reverted immediately after (confirmed via - an identical post-revert binary hash before moving on). + To load more than one app at once, concatenate their `.tbf` files (e.g. + `cat app1.tbf app2.tbf > apps.bin`) largest-first: `elf2tab` pads each + `.tbf` to a power-of-two size for MPU alignment, and the loader assumes + that ordering. diff --git a/boards/qemu_arm_mps2_an385/chip_layout.ld b/boards/qemu_arm_mps2_an385/chip_layout.ld index 83f95a67a59..1d86b019e9f 100644 --- a/boards/qemu_arm_mps2_an385/chip_layout.ld +++ b/boards/qemu_arm_mps2_an385/chip_layout.ld @@ -7,9 +7,12 @@ * QEMU maps code/flash (SSRAM1) at 0x0, up to a hard 4 MiB cap * (`armv7m_load_kernel(..., 0, 0x400000)` in `hw/arm/mps2.c`), and RAM at * 0x21000000, backed by a fixed 16 MiB (`default_ram_size` in the same - * file; QEMU errors if `-m` is overridden). We only claim a modest slice - * of each; QEMU backs whatever the linker script declares regardless of - * the machine's full capacity. + * file; QEMU errors if `-m` is overridden). + * + * We only expose a more limited portion of the SRAM, to better reflect + * the constraints of typical chips. However, QEMU's emulation does not + * enforce the limited range here (i.e., it will always emulate the full 16 MiB + * of SRAM and allow accesses outside the range we specify here). * * rom = 256KB (kernel) * prog = 256KB (apps) diff --git a/boards/qemu_arm_mps2_an385/src/io.rs b/boards/qemu_arm_mps2_an385/src/io.rs index 42c45f2a2a3..3c8f05e8f3f 100644 --- a/boards/qemu_arm_mps2_an385/src/io.rs +++ b/boards/qemu_arm_mps2_an385/src/io.rs @@ -5,7 +5,20 @@ use core::panic::PanicInfo; use kernel::debug; +use kernel::debug::PanicResources; use kernel::hil::uart; +use kernel::utilities::single_thread_value::SingleThreadValue; + +/// Board-owned panic-time resources. +/// +/// This can't live in `qemu_arm_mps2_lib` since a `static` can't be generic +/// over the `CortexMVariant` the way `qemu_arm_mps2_lib::start()` is. +pub(crate) static PANIC_RESOURCES: SingleThreadValue< + PanicResources< + qemu_arm_mps2_lib::ChipHw, + qemu_arm_mps2_lib::ProcessPrinterInUse, + >, +> = SingleThreadValue::new(); /// Panic handler. #[panic_handler] @@ -23,8 +36,15 @@ pub unsafe fn panic_fmt(info: &PanicInfo) -> ! { }, info, &cortexm3::support::nop, - crate::PANIC_RESOURCES.get(), + PANIC_RESOURCES.get(), ); + // The system is no longer in a well-defined state. Ask QEMU (started + // with `-semihosting`) to exit; SYS_EXIT (0x18) with + // ADP_Stopped_ApplicationExit reports the target exited abnormally. + // Only takes effect under a semihosting host -- on real hardware, or + // QEMU without `-semihosting`, this falls through to the loop below. + cortexm3::support::semihost_command(0x18, 0x20026); + loop {} } diff --git a/boards/qemu_arm_mps2_an385/src/main.rs b/boards/qemu_arm_mps2_an385/src/main.rs index 17dd8a2f4c6..9b70290acd6 100644 --- a/boards/qemu_arm_mps2_an385/src/main.rs +++ b/boards/qemu_arm_mps2_an385/src/main.rs @@ -10,273 +10,44 @@ //! emulated (notably: GPIO pin state is not observable under this QEMU //! machine, so this board does not expose a GPIO capsule; LEDs are //! implemented against the separate, genuinely-emulated FPGAIO block). +//! +//! This board and `qemu_arm_mps2_an386` are identical other than their CPU +//! core; all the shared setup lives in `qemu_arm_mps2_lib`. #![no_std] #![no_main] use kernel::capabilities; -use kernel::component::Component; -use kernel::debug::PanicResources; -use kernel::platform::chip::Chip; -use kernel::platform::{KernelResources, SyscallDriverLookup}; -use kernel::utilities::single_thread_value::SingleThreadValue; -use kernel::{create_capability, static_init}; +use kernel::create_capability; +use kernel::static_init; pub mod io; -const NUM_PROCS: usize = 4; - -type ChipHw = qemu_arm_mps2_chip::chip::QemuArmMps2Chip< - 'static, - cortexm3::CortexM3, - qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, ->; -type ProcessPrinterInUse = capsules_system::process_printer::ProcessPrinterText; -type SchedulerInUse = components::sched::round_robin::RoundRobinComponentType; - -static PANIC_RESOURCES: SingleThreadValue> = - SingleThreadValue::new(); - kernel::stack_size! {0x2000} -struct QemuArmMps2An385 { - console: &'static capsules_core::console::Console<'static>, - scheduler: &'static SchedulerInUse, - systick: cortexm3::systick::SysTick, - led: &'static capsules_core::led::LedDriver< - 'static, - qemu_arm_mps2_chip::led::Led<'static>, - { qemu_arm_mps2_chip::led::NUM_LEDS as usize }, - >, - alarm: &'static capsules_core::alarm::AlarmDriver< - 'static, - capsules_core::virtualizers::virtual_alarm::VirtualMuxAlarm< - 'static, - qemu_arm_mps2_chip::timer::Timer<'static>, - >, - >, - spi: &'static capsules_core::spi_controller::Spi< - 'static, - capsules_core::virtualizers::virtual_spi::VirtualSpiMasterDevice< - 'static, - qemu_arm_mps2_chip::spi::Spi<'static>, - >, - >, - watchdog: &'static qemu_arm_mps2_chip::watchdog::Watchdog, -} - -impl SyscallDriverLookup for QemuArmMps2An385 { - fn with_driver(&self, driver_num: usize, f: F) -> R - where - F: FnOnce(Option<&dyn kernel::syscall::SyscallDriver>) -> R, - { - match driver_num { - capsules_core::console::DRIVER_NUM => f(Some(self.console)), - capsules_core::led::DRIVER_NUM => f(Some(self.led)), - capsules_core::alarm::DRIVER_NUM => f(Some(self.alarm)), - capsules_core::spi_controller::DRIVER_NUM => f(Some(self.spi)), - _ => f(None), - } - } -} - -impl KernelResources for QemuArmMps2An385 { - type SyscallDriverLookup = Self; - type SyscallFilter = (); - type ProcessFault = (); - type Scheduler = SchedulerInUse; - type SchedulerTimer = cortexm3::systick::SysTick; - type WatchDog = qemu_arm_mps2_chip::watchdog::Watchdog; - type ContextSwitchCallback = (); - - fn syscall_driver_lookup(&self) -> &Self::SyscallDriverLookup { - self - } - fn syscall_filter(&self) -> &Self::SyscallFilter { - &() - } - fn process_fault(&self) -> &Self::ProcessFault { - &() - } - fn scheduler(&self) -> &Self::Scheduler { - self.scheduler - } - fn scheduler_timer(&self) -> &Self::SchedulerTimer { - &self.systick - } - fn watchdog(&self) -> &Self::WatchDog { - self.watchdog - } - fn context_switch_callback(&self) -> &Self::ContextSwitchCallback { - &() - } -} - -#[inline(never)] -unsafe fn start() -> ( - &'static kernel::Kernel, - &'static QemuArmMps2An385, - &'static ChipHw, -) { - ChipHw::init(); - - kernel::deferred_call::initialize_deferred_call_state::< - ::ThreadIdProvider, - >(); - - let _ = PANIC_RESOURCES - .bind_to_thread::<::ThreadIdProvider>( - PanicResources::new(), - ); - - let peripherals = static_init!( - qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, - qemu_arm_mps2_chip::Mps2DefaultPeripherals::new() - ); - - let processes = components::process_array::ProcessArrayComponent::new() - .finalize(components::process_array_component_static!(NUM_PROCS)); - let board_kernel = static_init!(kernel::Kernel, kernel::Kernel::new(processes.as_slice())); - - let chip = static_init!(ChipHw, ChipHw::new(peripherals)); - - let uart_mux = components::console::UartMuxComponent::new(&peripherals.uart0, 115200) - .finalize(components::uart_mux_component_static!()); - - let console = components::console::ConsoleComponent::new( - board_kernel, - capsules_core::console::DRIVER_NUM, - uart_mux, - create_capability!(capabilities::MemoryAllocationCapability), - ) - .finalize(components::console_component_static!()); - - components::debug_writer::DebugWriterComponent::new::< - ::ThreadIdProvider, - >( - uart_mux, - create_capability!(capabilities::SetDebugWriterCapability), - ) - .finalize(components::debug_writer_component_static!()); - - let alarm_mux = components::alarm::AlarmMuxComponent::new(&peripherals.timer0).finalize( - components::alarm_mux_component_static!(qemu_arm_mps2_chip::timer::Timer), - ); - - let alarm = components::alarm::AlarmDriverComponent::new( - board_kernel, - capsules_core::alarm::DRIVER_NUM, - alarm_mux, - create_capability!(capabilities::MemoryAllocationCapability), - ) - .finalize(components::alarm_component_static!( - qemu_arm_mps2_chip::timer::Timer - )); - - kernel::create_typed_capability!(process_console_cap, ProcessConsoleCap: - kernel::capabilities::ProcessManagementCapability, - kernel::capabilities::ProcessStartCapability - ); - let process_console = components::process_console::ProcessConsoleComponent::new( - board_kernel, - uart_mux, - alarm_mux, - components::process_printer::ProcessPrinterTextComponent::new() - .finalize(components::process_printer_text_component_static!()), - None, - process_console_cap, - ) - .finalize(components::process_console_component_static!( - qemu_arm_mps2_chip::timer::Timer, - ProcessConsoleCap - )); - let _ = process_console.start(); - - let spi_mux = components::spi::SpiMuxComponent::new(&peripherals.spi_shield0).finalize( - components::spi_mux_component_static!(qemu_arm_mps2_chip::spi::Spi), - ); - - let spi = components::spi::SpiSyscallComponent::new( - board_kernel, - spi_mux, - qemu_arm_mps2_chip::spi::ChipSelect, - capsules_core::spi_controller::DRIVER_NUM, - create_capability!(capabilities::MemoryAllocationCapability), - ) - .finalize(components::spi_syscall_component_static!( - qemu_arm_mps2_chip::spi::Spi - )); - - let led = components::led::LedsComponent::new().finalize(components::led_component_static!( - qemu_arm_mps2_chip::led::Led<'static>, - peripherals.fpgaio.led(0), - peripherals.fpgaio.led(1), - )); - - let scheduler = components::sched::round_robin::RoundRobinComponent::new(processes) - .finalize(components::round_robin_component_static!(NUM_PROCS)); - - let platform = static_init!( - QemuArmMps2An385, - QemuArmMps2An385 { - console, - scheduler, - systick: cortexm3::systick::SysTick::new(), - led, - alarm, - spi, - watchdog: &peripherals.watchdog, - } - ); - - extern "C" { - /// Beginning of the ROM region containing app images. - static _sapps: u8; - /// End of the ROM region containing app images. - static _eapps: u8; - /// Beginning of the RAM region for app memory. - static mut _sappmem: u8; - /// End of the RAM region for app memory. - static _eappmem: u8; - } - - let process_management_capability = - create_capability!(capabilities::ProcessManagementCapability); - kernel::process::load_processes( - board_kernel, - chip, - core::slice::from_raw_parts( - core::ptr::addr_of!(_sapps), - core::ptr::addr_of!(_eapps) as usize - core::ptr::addr_of!(_sapps) as usize, - ), - core::slice::from_raw_parts_mut( - core::ptr::addr_of_mut!(_sappmem), - core::ptr::addr_of!(_eappmem) as usize - core::ptr::addr_of!(_sappmem) as usize, - ), - &capsules_system::process_policies::PanicFaultPolicy {}, - &process_management_capability, - ) - .unwrap_or_else(|err| { - kernel::debug!("Error loading processes!"); - kernel::debug!("{:?}", err); - }); - - (board_kernel, platform, chip) -} - /// Main function called after RAM initialized. #[no_mangle] pub unsafe fn main() { - let (board_kernel, platform, chip) = start(); + let early = qemu_arm_mps2_lib::early_init::(&io::PANIC_RESOURCES); + + // Must be allocated here, not inside `qemu_arm_mps2_lib`: a `static` + // can't reference a generic function's own type parameter, so this one + // `static_init!()` needs a concrete, non-generic call site. See + // `qemu_arm_mps2_lib::early_init()`'s docs. + let chip = static_init!( + qemu_arm_mps2_lib::ChipHw, + qemu_arm_mps2_lib::ChipHw::::new(early.peripherals) + ); + + let (board_kernel, platform, chip) = qemu_arm_mps2_lib::finish_start(early, chip); kernel::debug!("QEMU MPS2 AN385 (Cortex-M3) initialization complete."); kernel::debug!("Entering main loop."); let main_loop_capability = create_capability!(capabilities::MainLoopCapability); - board_kernel.kernel_loop::( - platform, - chip, - None, - &main_loop_capability, - ); + board_kernel.kernel_loop::< + qemu_arm_mps2_lib::Platform, + qemu_arm_mps2_lib::ChipHw, + { qemu_arm_mps2_lib::NUM_PROCS as u8 }, + >(platform, chip, None, &main_loop_capability); } diff --git a/boards/qemu_arm_mps2_an386/Cargo.toml b/boards/qemu_arm_mps2_an386/Cargo.toml index dc8f751e09c..bf8e17d4889 100644 --- a/boards/qemu_arm_mps2_an386/Cargo.toml +++ b/boards/qemu_arm_mps2_an386/Cargo.toml @@ -10,15 +10,10 @@ build = "../build.rs" edition.workspace = true [dependencies] -components = { path = "../components" } -cortexm = { path = "../../arch/cortex-m" } cortexm4 = { path = "../../arch/cortex-m4" } kernel = { path = "../../kernel" } qemu_arm_mps2_chip = { path = "../../chips/qemu_arm_mps2_chip", features = ["cortex-m4"] } - -capsules-core = { path = "../../capsules/core" } -capsules-extra = { path = "../../capsules/extra" } -capsules-system = { path = "../../capsules/system" } +qemu_arm_mps2_lib = { path = "../qemu_arm_mps2_lib" } [build-dependencies] tock_build_scripts = { path = "../build_scripts" } diff --git a/boards/qemu_arm_mps2_an386/Makefile b/boards/qemu_arm_mps2_an386/Makefile index b0961e336e7..2fc37749e34 100644 --- a/boards/qemu_arm_mps2_an386/Makefile +++ b/boards/qemu_arm_mps2_an386/Makefile @@ -17,7 +17,8 @@ APP_ADDRESS := 0x00040000 QEMU_BASE_CMDLINE := \ $(QEMU_CMD) \ -machine mps2-an386 \ - -nographic + -nographic \ + -semihosting # Run the kernel inside a qemu-system-arm "mps2-an386" machine simulation. .PHONY: run diff --git a/boards/qemu_arm_mps2_an386/README.md b/boards/qemu_arm_mps2_an386/README.md index 0a87acec63e..4e7d652e653 100644 --- a/boards/qemu_arm_mps2_an386/README.md +++ b/boards/qemu_arm_mps2_an386/README.md @@ -34,12 +34,7 @@ Running QEMU ``` - **`run-app`**: same as `qemu_arm_mps2_an385`'s (`make run-app - APP=$PATH_TO_APP.tbf`), also verified end-to-end with `c_hello`, `blink`, - and a SPI loopback test app (built with `TOCK_TARGETS=cortex-m4`) loaded - at the same time — including `SPI PASS` from the loopback test, and the - same negative watchdog check (no spurious reset over ~9 reload periods). + APP=$PATH_TO_APP.tbf`). See `qemu_arm_mps2_an385/README.md` for the memory layout (identical -addresses on both machines) and the watchdog positive-test methodology -(only run once, on an385, since the peripheral and kernel logic are -identical on both boards). +addresses on both machines). diff --git a/boards/qemu_arm_mps2_an386/chip_layout.ld b/boards/qemu_arm_mps2_an386/chip_layout.ld index 83f95a67a59..1078fb45e7d 100644 --- a/boards/qemu_arm_mps2_an386/chip_layout.ld +++ b/boards/qemu_arm_mps2_an386/chip_layout.ld @@ -2,14 +2,17 @@ /* SPDX-License-Identifier: Apache-2.0 OR MIT */ /* Copyright Tock Contributors 2026. */ -/* Memory layout for the QEMU ARM MPS2 AN385 (Cortex-M3) machine. +/* Memory layout for the QEMU ARM MPS2 AN386 (Cortex-M4) machine. * * QEMU maps code/flash (SSRAM1) at 0x0, up to a hard 4 MiB cap * (`armv7m_load_kernel(..., 0, 0x400000)` in `hw/arm/mps2.c`), and RAM at * 0x21000000, backed by a fixed 16 MiB (`default_ram_size` in the same - * file; QEMU errors if `-m` is overridden). We only claim a modest slice - * of each; QEMU backs whatever the linker script declares regardless of - * the machine's full capacity. + * file; QEMU errors if `-m` is overridden). + * + * We only expose a more limited portion of the SRAM, to better reflect + * the constraints of typical chips. However, QEMU's emulation does not + * enforce the limited range here (i.e., it will always emulate the full 16 MiB + * of SRAM and allow accesses outside the range we specify here). * * rom = 256KB (kernel) * prog = 256KB (apps) diff --git a/boards/qemu_arm_mps2_an386/src/io.rs b/boards/qemu_arm_mps2_an386/src/io.rs index 177bb09226c..4bf61e5fed2 100644 --- a/boards/qemu_arm_mps2_an386/src/io.rs +++ b/boards/qemu_arm_mps2_an386/src/io.rs @@ -5,7 +5,20 @@ use core::panic::PanicInfo; use kernel::debug; +use kernel::debug::PanicResources; use kernel::hil::uart; +use kernel::utilities::single_thread_value::SingleThreadValue; + +/// Board-owned panic-time resources. +/// +/// This can't live in `qemu_arm_mps2_lib` since a `static` can't be generic +/// over the `CortexMVariant` the way `qemu_arm_mps2_lib::start()` is. +pub(crate) static PANIC_RESOURCES: SingleThreadValue< + PanicResources< + qemu_arm_mps2_lib::ChipHw, + qemu_arm_mps2_lib::ProcessPrinterInUse, + >, +> = SingleThreadValue::new(); /// Panic handler. #[panic_handler] @@ -23,8 +36,15 @@ pub unsafe fn panic_fmt(info: &PanicInfo) -> ! { }, info, &cortexm4::support::nop, - crate::PANIC_RESOURCES.get(), + PANIC_RESOURCES.get(), ); + // The system is no longer in a well-defined state. Ask QEMU (started + // with `-semihosting`) to exit; SYS_EXIT (0x18) with + // ADP_Stopped_ApplicationExit reports the target exited abnormally. + // Only takes effect under a semihosting host -- on real hardware, or + // QEMU without `-semihosting`, this falls through to the loop below. + cortexm4::support::semihost_command(0x18, 0x20026); + loop {} } diff --git a/boards/qemu_arm_mps2_an386/src/main.rs b/boards/qemu_arm_mps2_an386/src/main.rs index c19f76980a7..f1b226722bc 100644 --- a/boards/qemu_arm_mps2_an386/src/main.rs +++ b/boards/qemu_arm_mps2_an386/src/main.rs @@ -10,273 +10,44 @@ //! emulated (notably: GPIO pin state is not observable under this QEMU //! machine, so this board does not expose a GPIO capsule; LEDs are //! implemented against the separate, genuinely-emulated FPGAIO block). +//! +//! This board and `qemu_arm_mps2_an385` are identical other than their CPU +//! core; all the shared setup lives in `qemu_arm_mps2_lib`. #![no_std] #![no_main] use kernel::capabilities; -use kernel::component::Component; -use kernel::debug::PanicResources; -use kernel::platform::chip::Chip; -use kernel::platform::{KernelResources, SyscallDriverLookup}; -use kernel::utilities::single_thread_value::SingleThreadValue; -use kernel::{create_capability, static_init}; +use kernel::create_capability; +use kernel::static_init; pub mod io; -const NUM_PROCS: usize = 4; - -type ChipHw = qemu_arm_mps2_chip::chip::QemuArmMps2Chip< - 'static, - cortexm4::CortexM4, - qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, ->; -type ProcessPrinterInUse = capsules_system::process_printer::ProcessPrinterText; -type SchedulerInUse = components::sched::round_robin::RoundRobinComponentType; - -static PANIC_RESOURCES: SingleThreadValue> = - SingleThreadValue::new(); - kernel::stack_size! {0x2000} -struct QemuArmMps2An386 { - console: &'static capsules_core::console::Console<'static>, - scheduler: &'static SchedulerInUse, - systick: cortexm4::systick::SysTick, - led: &'static capsules_core::led::LedDriver< - 'static, - qemu_arm_mps2_chip::led::Led<'static>, - { qemu_arm_mps2_chip::led::NUM_LEDS as usize }, - >, - alarm: &'static capsules_core::alarm::AlarmDriver< - 'static, - capsules_core::virtualizers::virtual_alarm::VirtualMuxAlarm< - 'static, - qemu_arm_mps2_chip::timer::Timer<'static>, - >, - >, - spi: &'static capsules_core::spi_controller::Spi< - 'static, - capsules_core::virtualizers::virtual_spi::VirtualSpiMasterDevice< - 'static, - qemu_arm_mps2_chip::spi::Spi<'static>, - >, - >, - watchdog: &'static qemu_arm_mps2_chip::watchdog::Watchdog, -} - -impl SyscallDriverLookup for QemuArmMps2An386 { - fn with_driver(&self, driver_num: usize, f: F) -> R - where - F: FnOnce(Option<&dyn kernel::syscall::SyscallDriver>) -> R, - { - match driver_num { - capsules_core::console::DRIVER_NUM => f(Some(self.console)), - capsules_core::led::DRIVER_NUM => f(Some(self.led)), - capsules_core::alarm::DRIVER_NUM => f(Some(self.alarm)), - capsules_core::spi_controller::DRIVER_NUM => f(Some(self.spi)), - _ => f(None), - } - } -} - -impl KernelResources for QemuArmMps2An386 { - type SyscallDriverLookup = Self; - type SyscallFilter = (); - type ProcessFault = (); - type Scheduler = SchedulerInUse; - type SchedulerTimer = cortexm4::systick::SysTick; - type WatchDog = qemu_arm_mps2_chip::watchdog::Watchdog; - type ContextSwitchCallback = (); - - fn syscall_driver_lookup(&self) -> &Self::SyscallDriverLookup { - self - } - fn syscall_filter(&self) -> &Self::SyscallFilter { - &() - } - fn process_fault(&self) -> &Self::ProcessFault { - &() - } - fn scheduler(&self) -> &Self::Scheduler { - self.scheduler - } - fn scheduler_timer(&self) -> &Self::SchedulerTimer { - &self.systick - } - fn watchdog(&self) -> &Self::WatchDog { - self.watchdog - } - fn context_switch_callback(&self) -> &Self::ContextSwitchCallback { - &() - } -} - -#[inline(never)] -unsafe fn start() -> ( - &'static kernel::Kernel, - &'static QemuArmMps2An386, - &'static ChipHw, -) { - ChipHw::init(); - - kernel::deferred_call::initialize_deferred_call_state::< - ::ThreadIdProvider, - >(); - - let _ = PANIC_RESOURCES - .bind_to_thread::<::ThreadIdProvider>( - PanicResources::new(), - ); - - let peripherals = static_init!( - qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, - qemu_arm_mps2_chip::Mps2DefaultPeripherals::new() - ); - - let processes = components::process_array::ProcessArrayComponent::new() - .finalize(components::process_array_component_static!(NUM_PROCS)); - let board_kernel = static_init!(kernel::Kernel, kernel::Kernel::new(processes.as_slice())); - - let chip = static_init!(ChipHw, ChipHw::new(peripherals)); - - let uart_mux = components::console::UartMuxComponent::new(&peripherals.uart0, 115200) - .finalize(components::uart_mux_component_static!()); - - let console = components::console::ConsoleComponent::new( - board_kernel, - capsules_core::console::DRIVER_NUM, - uart_mux, - create_capability!(capabilities::MemoryAllocationCapability), - ) - .finalize(components::console_component_static!()); - - components::debug_writer::DebugWriterComponent::new::< - ::ThreadIdProvider, - >( - uart_mux, - create_capability!(capabilities::SetDebugWriterCapability), - ) - .finalize(components::debug_writer_component_static!()); - - let alarm_mux = components::alarm::AlarmMuxComponent::new(&peripherals.timer0).finalize( - components::alarm_mux_component_static!(qemu_arm_mps2_chip::timer::Timer), - ); - - let alarm = components::alarm::AlarmDriverComponent::new( - board_kernel, - capsules_core::alarm::DRIVER_NUM, - alarm_mux, - create_capability!(capabilities::MemoryAllocationCapability), - ) - .finalize(components::alarm_component_static!( - qemu_arm_mps2_chip::timer::Timer - )); - - kernel::create_typed_capability!(process_console_cap, ProcessConsoleCap: - kernel::capabilities::ProcessManagementCapability, - kernel::capabilities::ProcessStartCapability - ); - let process_console = components::process_console::ProcessConsoleComponent::new( - board_kernel, - uart_mux, - alarm_mux, - components::process_printer::ProcessPrinterTextComponent::new() - .finalize(components::process_printer_text_component_static!()), - None, - process_console_cap, - ) - .finalize(components::process_console_component_static!( - qemu_arm_mps2_chip::timer::Timer, - ProcessConsoleCap - )); - let _ = process_console.start(); - - let spi_mux = components::spi::SpiMuxComponent::new(&peripherals.spi_shield0).finalize( - components::spi_mux_component_static!(qemu_arm_mps2_chip::spi::Spi), - ); - - let spi = components::spi::SpiSyscallComponent::new( - board_kernel, - spi_mux, - qemu_arm_mps2_chip::spi::ChipSelect, - capsules_core::spi_controller::DRIVER_NUM, - create_capability!(capabilities::MemoryAllocationCapability), - ) - .finalize(components::spi_syscall_component_static!( - qemu_arm_mps2_chip::spi::Spi - )); - - let led = components::led::LedsComponent::new().finalize(components::led_component_static!( - qemu_arm_mps2_chip::led::Led<'static>, - peripherals.fpgaio.led(0), - peripherals.fpgaio.led(1), - )); - - let scheduler = components::sched::round_robin::RoundRobinComponent::new(processes) - .finalize(components::round_robin_component_static!(NUM_PROCS)); - - let platform = static_init!( - QemuArmMps2An386, - QemuArmMps2An386 { - console, - scheduler, - systick: cortexm4::systick::SysTick::new(), - led, - alarm, - spi, - watchdog: &peripherals.watchdog, - } - ); - - extern "C" { - /// Beginning of the ROM region containing app images. - static _sapps: u8; - /// End of the ROM region containing app images. - static _eapps: u8; - /// Beginning of the RAM region for app memory. - static mut _sappmem: u8; - /// End of the RAM region for app memory. - static _eappmem: u8; - } - - let process_management_capability = - create_capability!(capabilities::ProcessManagementCapability); - kernel::process::load_processes( - board_kernel, - chip, - core::slice::from_raw_parts( - core::ptr::addr_of!(_sapps), - core::ptr::addr_of!(_eapps) as usize - core::ptr::addr_of!(_sapps) as usize, - ), - core::slice::from_raw_parts_mut( - core::ptr::addr_of_mut!(_sappmem), - core::ptr::addr_of!(_eappmem) as usize - core::ptr::addr_of!(_sappmem) as usize, - ), - &capsules_system::process_policies::PanicFaultPolicy {}, - &process_management_capability, - ) - .unwrap_or_else(|err| { - kernel::debug!("Error loading processes!"); - kernel::debug!("{:?}", err); - }); - - (board_kernel, platform, chip) -} - /// Main function called after RAM initialized. #[no_mangle] pub unsafe fn main() { - let (board_kernel, platform, chip) = start(); + let early = qemu_arm_mps2_lib::early_init::(&io::PANIC_RESOURCES); + + // Must be allocated here, not inside `qemu_arm_mps2_lib`: a `static` + // can't reference a generic function's own type parameter, so this one + // `static_init!()` needs a concrete, non-generic call site. See + // `qemu_arm_mps2_lib::early_init()`'s docs. + let chip = static_init!( + qemu_arm_mps2_lib::ChipHw, + qemu_arm_mps2_lib::ChipHw::::new(early.peripherals) + ); + + let (board_kernel, platform, chip) = qemu_arm_mps2_lib::finish_start(early, chip); kernel::debug!("QEMU MPS2 AN386 (Cortex-M4) initialization complete."); kernel::debug!("Entering main loop."); let main_loop_capability = create_capability!(capabilities::MainLoopCapability); - board_kernel.kernel_loop::( - platform, - chip, - None, - &main_loop_capability, - ); + board_kernel.kernel_loop::< + qemu_arm_mps2_lib::Platform, + qemu_arm_mps2_lib::ChipHw, + { qemu_arm_mps2_lib::NUM_PROCS as u8 }, + >(platform, chip, None, &main_loop_capability); } diff --git a/boards/qemu_arm_mps2_lib/Cargo.toml b/boards/qemu_arm_mps2_lib/Cargo.toml new file mode 100644 index 00000000000..bfd36ac66ca --- /dev/null +++ b/boards/qemu_arm_mps2_lib/Cargo.toml @@ -0,0 +1,22 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2026. + +[package] +name = "qemu_arm_mps2_lib" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[dependencies] +components = { path = "../components" } +cortexm = { path = "../../arch/cortex-m" } +kernel = { path = "../../kernel" } +qemu_arm_mps2_chip = { path = "../../chips/qemu_arm_mps2_chip" } + +capsules-core = { path = "../../capsules/core" } +capsules-extra = { path = "../../capsules/extra" } +capsules-system = { path = "../../capsules/system" } + +[lints] +workspace = true diff --git a/boards/qemu_arm_mps2_lib/src/lib.rs b/boards/qemu_arm_mps2_lib/src/lib.rs new file mode 100644 index 00000000000..a8b8d28ef53 --- /dev/null +++ b/boards/qemu_arm_mps2_lib/src/lib.rs @@ -0,0 +1,300 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2026. + +//! Shared kernel setup for the QEMU ARM MPS2 AN385 (Cortex-M3) and AN386 +//! (Cortex-M4) boards. +//! +//! The two boards are identical other than their CPU core, so this crate is +//! generic over the [`CortexMVariant`] and provides everything that +//! genericity allows to be shared: the `Platform` syscall driver lookup, the +//! `ChipHw` type, and the process/capsule setup in [`start()`]. What can't +//! be made generic stays in each board's own `main.rs`/`io.rs`: +//! - The `#[panic_handler]` function and its `PANIC_RESOURCES` static, since +//! a `static` can't be generic and `#[panic_handler]` requires a concrete, +//! non-generic function. +//! - `kernel::stack_size!`, an item-position macro that can't be invoked +//! from inside a generic function body. +//! - The board name used in the boot banner. + +#![no_std] + +use cortexm::CortexMVariant; +use kernel::capabilities; +use kernel::component::Component; +use kernel::debug::PanicResources; +use kernel::platform::chip::Chip; +use kernel::platform::{KernelResources, SyscallDriverLookup}; +use kernel::utilities::single_thread_value::SingleThreadValue; +use kernel::{create_capability, static_init}; + +pub const NUM_PROCS: usize = 4; + +pub type ChipHw = qemu_arm_mps2_chip::chip::QemuArmMps2Chip< + 'static, + C, + qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, +>; +pub type ProcessPrinterInUse = capsules_system::process_printer::ProcessPrinterText; +type SchedulerInUse = components::sched::round_robin::RoundRobinComponentType; + +pub struct Platform { + console: &'static capsules_core::console::Console<'static>, + scheduler: &'static SchedulerInUse, + systick: cortexm::systick::SysTick, + led: &'static capsules_core::led::LedDriver< + 'static, + qemu_arm_mps2_chip::led::Led<'static>, + { qemu_arm_mps2_chip::led::NUM_LEDS as usize }, + >, + alarm: &'static capsules_core::alarm::AlarmDriver< + 'static, + capsules_core::virtualizers::virtual_alarm::VirtualMuxAlarm< + 'static, + qemu_arm_mps2_chip::timer::Timer<'static>, + >, + >, + spi: &'static capsules_core::spi_controller::Spi< + 'static, + capsules_core::virtualizers::virtual_spi::VirtualSpiMasterDevice< + 'static, + qemu_arm_mps2_chip::spi::Spi<'static>, + >, + >, + watchdog: &'static qemu_arm_mps2_chip::watchdog::Watchdog, +} + +impl SyscallDriverLookup for Platform { + fn with_driver(&self, driver_num: usize, f: F) -> R + where + F: FnOnce(Option<&dyn kernel::syscall::SyscallDriver>) -> R, + { + match driver_num { + capsules_core::console::DRIVER_NUM => f(Some(self.console)), + capsules_core::led::DRIVER_NUM => f(Some(self.led)), + capsules_core::alarm::DRIVER_NUM => f(Some(self.alarm)), + capsules_core::spi_controller::DRIVER_NUM => f(Some(self.spi)), + _ => f(None), + } + } +} + +impl KernelResources> for Platform { + type SyscallDriverLookup = Self; + type SyscallFilter = (); + type ProcessFault = (); + type Scheduler = SchedulerInUse; + type SchedulerTimer = cortexm::systick::SysTick; + type WatchDog = qemu_arm_mps2_chip::watchdog::Watchdog; + type ContextSwitchCallback = (); + + fn syscall_driver_lookup(&self) -> &Self::SyscallDriverLookup { + self + } + fn syscall_filter(&self) -> &Self::SyscallFilter { + &() + } + fn process_fault(&self) -> &Self::ProcessFault { + &() + } + fn scheduler(&self) -> &Self::Scheduler { + self.scheduler + } + fn scheduler_timer(&self) -> &Self::SchedulerTimer { + &self.systick + } + fn watchdog(&self) -> &Self::WatchDog { + self.watchdog + } + fn context_switch_callback(&self) -> &Self::ContextSwitchCallback { + &() + } +} + +/// Peripherals and kernel state ready for [`chip()`] and [`finish_start()`]. +pub struct EarlyInit { + pub peripherals: &'static qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, + pub processes: &'static kernel::process::ProcessArray, + pub board_kernel: &'static kernel::Kernel, +} + +/// Runs the CPU-variant init and allocates peripherals/kernel state. +/// +/// Split out from [`finish_start()`] because the `ChipHw` allocation in +/// between the two has to be a `static_init!()` written at a concrete, +/// non-generic call site (a `static` can't reference a generic function's +/// own type parameter — see each board's `main.rs`), so each board calls: +/// `early_init`, then its own `static_init!(ChipHw, ...)`, then +/// `finish_start`. +/// +/// `panic_resources` is board-owned for the same reason but filled in here +/// so both boards only have to declare it, not populate it. +#[inline(never)] +pub unsafe fn early_init( + panic_resources: &'static SingleThreadValue, ProcessPrinterInUse>>, +) -> EarlyInit { + ChipHw::::init(); + + kernel::deferred_call::initialize_deferred_call_state::< as Chip>::ThreadIdProvider>( + ); + + let _ = panic_resources + .bind_to_thread::< as Chip>::ThreadIdProvider>(PanicResources::new()); + + let peripherals = static_init!( + qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, + qemu_arm_mps2_chip::Mps2DefaultPeripherals::new() + ); + + let processes = components::process_array::ProcessArrayComponent::new() + .finalize(components::process_array_component_static!(NUM_PROCS)); + let board_kernel = static_init!(kernel::Kernel, kernel::Kernel::new(processes.as_slice())); + + EarlyInit { + peripherals, + processes, + board_kernel, + } +} + +/// Finishes board setup and starts loading processes. +/// +/// `chip` must have been allocated by the caller via +/// `static_init!(ChipHw, ChipHw::::new(early_init.peripherals))` +/// after [`early_init()`] — see its docs for why. +#[inline(never)] +pub unsafe fn finish_start( + early: EarlyInit, + chip: &'static ChipHw, +) -> ( + &'static kernel::Kernel, + &'static Platform, + &'static ChipHw, +) { + let EarlyInit { + peripherals, + processes, + board_kernel, + } = early; + + let uart_mux = components::console::UartMuxComponent::new(&peripherals.uart0, 115200) + .finalize(components::uart_mux_component_static!()); + + let console = components::console::ConsoleComponent::new( + board_kernel, + capsules_core::console::DRIVER_NUM, + uart_mux, + create_capability!(capabilities::MemoryAllocationCapability), + ) + .finalize(components::console_component_static!()); + + components::debug_writer::DebugWriterComponent::new::< as Chip>::ThreadIdProvider>( + uart_mux, + create_capability!(capabilities::SetDebugWriterCapability), + ) + .finalize(components::debug_writer_component_static!()); + + let alarm_mux = components::alarm::AlarmMuxComponent::new(&peripherals.timer0).finalize( + components::alarm_mux_component_static!(qemu_arm_mps2_chip::timer::Timer), + ); + + let alarm = components::alarm::AlarmDriverComponent::new( + board_kernel, + capsules_core::alarm::DRIVER_NUM, + alarm_mux, + create_capability!(capabilities::MemoryAllocationCapability), + ) + .finalize(components::alarm_component_static!( + qemu_arm_mps2_chip::timer::Timer + )); + + kernel::create_typed_capability!(process_console_cap, ProcessConsoleCap: + kernel::capabilities::ProcessManagementCapability, + kernel::capabilities::ProcessStartCapability + ); + let process_console = components::process_console::ProcessConsoleComponent::new( + board_kernel, + uart_mux, + alarm_mux, + components::process_printer::ProcessPrinterTextComponent::new() + .finalize(components::process_printer_text_component_static!()), + None, + process_console_cap, + ) + .finalize(components::process_console_component_static!( + qemu_arm_mps2_chip::timer::Timer, + ProcessConsoleCap + )); + let _ = process_console.start(); + + let spi_mux = components::spi::SpiMuxComponent::new(&peripherals.spi_shield0).finalize( + components::spi_mux_component_static!(qemu_arm_mps2_chip::spi::Spi), + ); + + let spi = components::spi::SpiSyscallComponent::new( + board_kernel, + spi_mux, + qemu_arm_mps2_chip::spi::ChipSelect, + capsules_core::spi_controller::DRIVER_NUM, + create_capability!(capabilities::MemoryAllocationCapability), + ) + .finalize(components::spi_syscall_component_static!( + qemu_arm_mps2_chip::spi::Spi + )); + + let led = components::led::LedsComponent::new().finalize(components::led_component_static!( + qemu_arm_mps2_chip::led::Led<'static>, + peripherals.fpgaio.led(0), + peripherals.fpgaio.led(1), + )); + + let scheduler = components::sched::round_robin::RoundRobinComponent::new(processes) + .finalize(components::round_robin_component_static!(NUM_PROCS)); + + let platform = static_init!( + Platform, + Platform { + console, + scheduler, + systick: cortexm::systick::SysTick::new(), + led, + alarm, + spi, + watchdog: &peripherals.watchdog, + } + ); + + extern "C" { + /// Beginning of the ROM region containing app images. + static _sapps: u8; + /// End of the ROM region containing app images. + static _eapps: u8; + /// Beginning of the RAM region for app memory. + static mut _sappmem: u8; + /// End of the RAM region for app memory. + static _eappmem: u8; + } + + let process_management_capability = + create_capability!(capabilities::ProcessManagementCapability); + kernel::process::load_processes( + board_kernel, + chip, + core::slice::from_raw_parts( + core::ptr::addr_of!(_sapps), + core::ptr::addr_of!(_eapps) as usize - core::ptr::addr_of!(_sapps) as usize, + ), + core::slice::from_raw_parts_mut( + core::ptr::addr_of_mut!(_sappmem), + core::ptr::addr_of!(_eappmem) as usize - core::ptr::addr_of!(_sappmem) as usize, + ), + &capsules_system::process_policies::PanicFaultPolicy {}, + &process_management_capability, + ) + .unwrap_or_else(|err| { + kernel::debug!("Error loading processes!"); + kernel::debug!("{:?}", err); + }); + + (board_kernel, platform, chip) +} diff --git a/chips/qemu_arm_mps2_chip/src/spi.rs b/chips/qemu_arm_mps2_chip/src/spi.rs index 32e625215e4..bcff2482d68 100644 --- a/chips/qemu_arm_mps2_chip/src/spi.rs +++ b/chips/qemu_arm_mps2_chip/src/spi.rs @@ -8,7 +8,7 @@ //! Of the five PL022 instances on this machine, only the "Shield0" one //! (`0x40026000`) is driven here. //! -//! Two real limitations of this QEMU model shape this driver: +//! Limitations of this QEMU model shape this driver: //! //! - **No SSI slave device is attached to any of the five PL022 instances** //! in QEMU (`hw/arm/mps2.c` creates bare `TYPE_PL022` controllers with no diff --git a/chips/qemu_arm_mps2_chip/src/watchdog.rs b/chips/qemu_arm_mps2_chip/src/watchdog.rs index f6420de65c2..3baa4bcf2ef 100644 --- a/chips/qemu_arm_mps2_chip/src/watchdog.rs +++ b/chips/qemu_arm_mps2_chip/src/watchdog.rs @@ -5,12 +5,12 @@ //! ARM CMSDK APB Watchdog (SP805-style), as found on the MPS2 AN385/AN386 //! FPGA images. //! -//! Unlike GPIO, this is a real, non-stub QEMU peripheral: it genuinely -//! counts down and genuinely resets the machine. The interrupt line is -//! wired to NMI, not a normal NVIC line (`hw/arm/mps2.c`), so it is not -//! dispatched through [`kernel::platform::chip::InterruptService`] the way -//! the other peripherals in this crate are — this driver only ever pokes -//! registers, it never installs an NMI handler of its own. +//! This is a real, non-stub QEMU peripheral: it counts down and resets the +//! machine. The interrupt line is wired to NMI, not a normal NVIC line +//! (`hw/arm/mps2.c`), so it is not dispatched through +//! [`kernel::platform::chip::InterruptService`] the way the other +//! peripherals in this crate are — this driver only ever pokes registers, +//! it never installs an NMI handler of its own. //! //! Hardware behavior: the first countdown-to-zero with `INTEN` set raises //! the (non-maskable) interrupt; if nobody kicks the watchdog @@ -20,7 +20,10 @@ //! handler does. This board's vector table maps NMI to `unhandled_interrupt` //! (a panic), so in practice that panic — not the hardware reset — is what //! happens on a first missed kick; the reset only fires if something -//! prevents that panic from halting execution first. +//! prevents that panic from halting execution first. In practice this +//! happens quickly regardless: the panic handler loops forever without +//! kicking the watchdog, so the second countdown-to-zero (and the reset it +//! triggers) follows shortly after. use kernel::platform::watchdog::WatchDog; use kernel::utilities::StaticRef; From 22b7691a5dd5d6dae447f96243764ad474347b35 Mon Sep 17 00:00:00 2001 From: ppannuto-claude Date: Wed, 26 Aug 2026 15:07:51 -0700 Subject: [PATCH 6/6] Address review feedback: narrow semihost API, add SAFETY comments - Make cortexm::support::semihost_command() module-private and add a narrow pub semihost_terminate() instead -- the general, unrestricted semihosting interface shouldn't be exposed when only one specific operation is actually needed. - Add /// # Safety docs and // SAFETY: comments to semihost_command, semihost_terminate, and qemu_arm_mps2_lib's early_init()/finish_start(), plus their call sites, per Tock's SAFETY comment convention. - Move the shared memory-layout documentation from an385's README (with an386's pointing at it) into qemu_arm_mps2_lib's crate-level doc comment, since that's the crate actually shared between both boards. Co-Authored-By: Claude Sonnet 5 --- arch/cortex-m/src/support.rs | 59 +++++++++++++++++++++----- boards/qemu_arm_mps2_an385/README.md | 5 +-- boards/qemu_arm_mps2_an385/src/io.rs | 14 +++--- boards/qemu_arm_mps2_an385/src/main.rs | 12 +++++- boards/qemu_arm_mps2_an386/README.md | 4 +- boards/qemu_arm_mps2_an386/src/io.rs | 14 +++--- boards/qemu_arm_mps2_an386/src/main.rs | 12 +++++- boards/qemu_arm_mps2_lib/src/lib.rs | 40 ++++++++++++++--- 8 files changed, 122 insertions(+), 38 deletions(-) diff --git a/arch/cortex-m/src/support.rs b/arch/cortex-m/src/support.rs index 07a703b6218..05d9dddcfe6 100644 --- a/arch/cortex-m/src/support.rs +++ b/arch/cortex-m/src/support.rs @@ -188,21 +188,31 @@ pub fn is_interrupt_context() -> bool { /// Issue an ARM semihosting call. /// /// `operation` is the semihosting operation number (e.g. `0x18` for -/// `SYS_EXIT`) and `parameter` is its operation-specific argument. Only -/// meaningful when running under a semihosting host (e.g. QEMU started with -/// `-semihosting`, or an attached debug probe); otherwise the `bkpt` -/// instruction traps with no host to service it. +/// `SYS_EXIT`) and `parameter` is its operation-specific argument. +/// +/// Not exposed outside this module: it's a general, unrestricted semihosting +/// interface, whereas callers should only need specific, narrow operations +/// (e.g. [`semihost_terminate`]) that are safe to expose more broadly. +/// +/// # Safety +/// +/// Only meaningful when running under a semihosting host (e.g. QEMU started +/// with `-semihosting`, or an attached debug probe); otherwise the `bkpt` +/// instruction traps with no host to service it, so the caller must not +/// assume this call takes effect. Depending on `operation`, the host may +/// dereference `parameter` as a pointer (e.g. `SYS_WRITEC`) -- the caller is +/// responsible for passing a value valid for whichever `operation` it +/// selects. #[cfg(any(doc, all(target_arch = "arm", target_os = "none")))] #[inline(always)] -pub unsafe fn semihost_command(operation: u32, parameter: u32) -> u32 { +unsafe fn semihost_command(operation: u32, parameter: u32) -> u32 { use core::arch::asm; let result; - // # Safety - // - // - INPUTS: r0/r1 are set to `operation`/`parameter`, per the ABI ARM - // semihosting defines (ARM's "Semihosting for AArch32 and AArch64" - // specification). + // SAFETY: r0/r1 are set to `operation`/`parameter`, per the ABI ARM + // semihosting defines (ARM's "Semihosting for AArch32 and AArch64" + // specification); the caller is responsible for those being valid for + // the chosen `operation`, per this function's own `# Safety` doc above. // - OUTPUTS: r0 is overwritten with the semihosting call's result. // - Options set: // - nostack: This does not use the stack. @@ -226,8 +236,35 @@ pub unsafe fn semihost_command(operation: u32, parameter: u32) -> u32 { result } +/// Ask a semihosting host to terminate execution, reporting an abnormal +/// exit. +/// +/// Issues ARM semihosting's `SYS_EXIT` (`0x18`) with reason +/// `ADP_Stopped_ApplicationExit` (`0x20026`). Intended for use from an +/// already-unrecoverable state, such as a panic handler. +/// +/// # Safety +/// +/// Only meaningful when running under a semihosting host (e.g. QEMU started +/// with `-semihosting`, or an attached debug probe); otherwise this falls +/// through with no effect. This does not itself diverge -- the caller must +/// not rely on it halting execution, and must not resume normal operation +/// afterwards regardless of whether a host was present to service the call. +#[cfg(any(doc, all(target_arch = "arm", target_os = "none")))] +#[inline(always)] +pub unsafe fn semihost_terminate() { + const SYS_EXIT: u32 = 0x18; + const ADP_STOPPED_APPLICATION_EXIT: u32 = 0x20026; + // SAFETY: fixed, well-formed arguments per `semihost_command`'s safety + // doc above -- `ADP_STOPPED_APPLICATION_EXIT` is a plain reason code, + // not a pointer, so nothing here is dereferenced. + unsafe { + semihost_command(SYS_EXIT, ADP_STOPPED_APPLICATION_EXIT); + } +} + /// Mock implementation for tests on Travis-CI. #[cfg(not(any(doc, all(target_arch = "arm", target_os = "none"))))] -pub unsafe fn semihost_command(_operation: u32, _parameter: u32) -> u32 { +pub unsafe fn semihost_terminate() { unimplemented!() } diff --git a/boards/qemu_arm_mps2_an385/README.md b/boards/qemu_arm_mps2_an385/README.md index bba9d93b17f..2e92c04e204 100644 --- a/boards/qemu_arm_mps2_an385/README.md +++ b/boards/qemu_arm_mps2_an385/README.md @@ -71,9 +71,8 @@ device on this machine to show regardless). tock$ ``` -With the default linker script, this board loads processes from -flash=0x00040000-0x0007FFFF into ram=0x21004000-0x2101FFFF (RAM above the -kernel's own static allocations). +See `qemu_arm_mps2_lib`'s crate docs for the memory layout (shared by both +this board and `qemu_arm_mps2_an386`). Running an application ----------------------- diff --git a/boards/qemu_arm_mps2_an385/src/io.rs b/boards/qemu_arm_mps2_an385/src/io.rs index 3c8f05e8f3f..7ed99309d2a 100644 --- a/boards/qemu_arm_mps2_an385/src/io.rs +++ b/boards/qemu_arm_mps2_an385/src/io.rs @@ -39,12 +39,14 @@ pub unsafe fn panic_fmt(info: &PanicInfo) -> ! { PANIC_RESOURCES.get(), ); - // The system is no longer in a well-defined state. Ask QEMU (started - // with `-semihosting`) to exit; SYS_EXIT (0x18) with - // ADP_Stopped_ApplicationExit reports the target exited abnormally. - // Only takes effect under a semihosting host -- on real hardware, or - // QEMU without `-semihosting`, this falls through to the loop below. - cortexm3::support::semihost_command(0x18, 0x20026); + // SAFETY: the system is no longer in a well-defined state (we're in the + // panic handler), so falling through if there's no semihosting host to + // service this (e.g. real hardware, or QEMU without `-semihosting`) is + // fine -- we don't resume normal execution either way, per the loop + // below. + unsafe { + cortexm3::support::semihost_terminate(); + } loop {} } diff --git a/boards/qemu_arm_mps2_an385/src/main.rs b/boards/qemu_arm_mps2_an385/src/main.rs index 9b70290acd6..e4141235dfe 100644 --- a/boards/qemu_arm_mps2_an385/src/main.rs +++ b/boards/qemu_arm_mps2_an385/src/main.rs @@ -28,7 +28,12 @@ kernel::stack_size! {0x2000} /// Main function called after RAM initialized. #[no_mangle] pub unsafe fn main() { - let early = qemu_arm_mps2_lib::early_init::(&io::PANIC_RESOURCES); + // SAFETY: `main` is only ever invoked once, by the reset handler, before + // anything else touches the chip's peripherals or kernel state -- see + // `qemu_arm_mps2_lib::early_init()`'s safety doc. `CortexM3` is this + // board's actual CPU core. + let early = + unsafe { qemu_arm_mps2_lib::early_init::(&io::PANIC_RESOURCES) }; // Must be allocated here, not inside `qemu_arm_mps2_lib`: a `static` // can't reference a generic function's own type parameter, so this one @@ -39,7 +44,10 @@ pub unsafe fn main() { qemu_arm_mps2_lib::ChipHw::::new(early.peripherals) ); - let (board_kernel, platform, chip) = qemu_arm_mps2_lib::finish_start(early, chip); + // SAFETY: called immediately after the `early_init()`/`static_init!()` + // pair above, from the same boot, same `C` -- see + // `qemu_arm_mps2_lib::finish_start()`'s safety doc. + let (board_kernel, platform, chip) = unsafe { qemu_arm_mps2_lib::finish_start(early, chip) }; kernel::debug!("QEMU MPS2 AN385 (Cortex-M3) initialization complete."); kernel::debug!("Entering main loop."); diff --git a/boards/qemu_arm_mps2_an386/README.md b/boards/qemu_arm_mps2_an386/README.md index 4e7d652e653..19f7b38afb1 100644 --- a/boards/qemu_arm_mps2_an386/README.md +++ b/boards/qemu_arm_mps2_an386/README.md @@ -36,5 +36,5 @@ Running QEMU - **`run-app`**: same as `qemu_arm_mps2_an385`'s (`make run-app APP=$PATH_TO_APP.tbf`). -See `qemu_arm_mps2_an385/README.md` for the memory layout (identical -addresses on both machines). +See `qemu_arm_mps2_lib`'s crate docs for the memory layout (shared by both +this board and `qemu_arm_mps2_an385`). diff --git a/boards/qemu_arm_mps2_an386/src/io.rs b/boards/qemu_arm_mps2_an386/src/io.rs index 4bf61e5fed2..8b74e83f085 100644 --- a/boards/qemu_arm_mps2_an386/src/io.rs +++ b/boards/qemu_arm_mps2_an386/src/io.rs @@ -39,12 +39,14 @@ pub unsafe fn panic_fmt(info: &PanicInfo) -> ! { PANIC_RESOURCES.get(), ); - // The system is no longer in a well-defined state. Ask QEMU (started - // with `-semihosting`) to exit; SYS_EXIT (0x18) with - // ADP_Stopped_ApplicationExit reports the target exited abnormally. - // Only takes effect under a semihosting host -- on real hardware, or - // QEMU without `-semihosting`, this falls through to the loop below. - cortexm4::support::semihost_command(0x18, 0x20026); + // SAFETY: the system is no longer in a well-defined state (we're in the + // panic handler), so falling through if there's no semihosting host to + // service this (e.g. real hardware, or QEMU without `-semihosting`) is + // fine -- we don't resume normal execution either way, per the loop + // below. + unsafe { + cortexm4::support::semihost_terminate(); + } loop {} } diff --git a/boards/qemu_arm_mps2_an386/src/main.rs b/boards/qemu_arm_mps2_an386/src/main.rs index f1b226722bc..43537f8f361 100644 --- a/boards/qemu_arm_mps2_an386/src/main.rs +++ b/boards/qemu_arm_mps2_an386/src/main.rs @@ -28,7 +28,12 @@ kernel::stack_size! {0x2000} /// Main function called after RAM initialized. #[no_mangle] pub unsafe fn main() { - let early = qemu_arm_mps2_lib::early_init::(&io::PANIC_RESOURCES); + // SAFETY: `main` is only ever invoked once, by the reset handler, before + // anything else touches the chip's peripherals or kernel state -- see + // `qemu_arm_mps2_lib::early_init()`'s safety doc. `CortexM4` is this + // board's actual CPU core. + let early = + unsafe { qemu_arm_mps2_lib::early_init::(&io::PANIC_RESOURCES) }; // Must be allocated here, not inside `qemu_arm_mps2_lib`: a `static` // can't reference a generic function's own type parameter, so this one @@ -39,7 +44,10 @@ pub unsafe fn main() { qemu_arm_mps2_lib::ChipHw::::new(early.peripherals) ); - let (board_kernel, platform, chip) = qemu_arm_mps2_lib::finish_start(early, chip); + // SAFETY: called immediately after the `early_init()`/`static_init!()` + // pair above, from the same boot, same `C` -- see + // `qemu_arm_mps2_lib::finish_start()`'s safety doc. + let (board_kernel, platform, chip) = unsafe { qemu_arm_mps2_lib::finish_start(early, chip) }; kernel::debug!("QEMU MPS2 AN386 (Cortex-M4) initialization complete."); kernel::debug!("Entering main loop."); diff --git a/boards/qemu_arm_mps2_lib/src/lib.rs b/boards/qemu_arm_mps2_lib/src/lib.rs index a8b8d28ef53..e733d31dcd0 100644 --- a/boards/qemu_arm_mps2_lib/src/lib.rs +++ b/boards/qemu_arm_mps2_lib/src/lib.rs @@ -8,14 +8,24 @@ //! The two boards are identical other than their CPU core, so this crate is //! generic over the [`CortexMVariant`] and provides everything that //! genericity allows to be shared: the `Platform` syscall driver lookup, the -//! `ChipHw` type, and the process/capsule setup in [`start()`]. What can't -//! be made generic stays in each board's own `main.rs`/`io.rs`: -//! - The `#[panic_handler]` function and its `PANIC_RESOURCES` static, since -//! a `static` can't be generic and `#[panic_handler]` requires a concrete, -//! non-generic function. +//! `ChipHw` type, and the process/capsule setup in [`early_init()`] / +//! [`finish_start()`]. What can't be made generic stays in each board's own +//! `main.rs`/`io.rs`: +//! - The `static_init!(ChipHw, ...)` allocation in between those two +//! calls, since a `static` can't reference a generic function's own type +//! parameter. +//! - The `#[panic_handler]` function and its `PANIC_RESOURCES` static, for +//! the same reason (plus `#[panic_handler]` itself requires a concrete, +//! non-generic function). //! - `kernel::stack_size!`, an item-position macro that can't be invoked //! from inside a generic function body. //! - The board name used in the boot banner. +//! +//! Both boards use the same memory layout (identical addresses; only the +//! two linker scripts' `MEMORY` blocks are duplicated, since each board +//! crate needs its own): with the default linker script, a board loads +//! processes from flash=0x00040000-0x0007FFFF into ram=0x21004000- +//! 0x2101FFFF (RAM above the kernel's own static allocations). #![no_std] @@ -111,7 +121,8 @@ impl KernelResources> for Platform { } } -/// Peripherals and kernel state ready for [`chip()`] and [`finish_start()`]. +/// Peripherals and kernel state ready for allocating the chip and calling +/// [`finish_start()`]. pub struct EarlyInit { pub peripherals: &'static qemu_arm_mps2_chip::Mps2DefaultPeripherals<'static>, pub processes: &'static kernel::process::ProcessArray, @@ -129,6 +140,15 @@ pub struct EarlyInit { /// /// `panic_resources` is board-owned for the same reason but filled in here /// so both boards only have to declare it, not populate it. +/// +/// # Safety +/// +/// Must be called exactly once, before any other access to the chip's +/// peripherals or kernel state, from the board's `main()` entry point -- +/// this performs one-time hardware init and allocates `'static` state via +/// `static_init!()`, which does not itself guard against being called more +/// than once. `C` must be the actual `CortexMVariant` of the CPU this is +/// running on. #[inline(never)] pub unsafe fn early_init( panic_resources: &'static SingleThreadValue, ProcessPrinterInUse>>, @@ -162,6 +182,14 @@ pub unsafe fn early_init( /// `chip` must have been allocated by the caller via /// `static_init!(ChipHw, ChipHw::::new(early_init.peripherals))` /// after [`early_init()`] — see its docs for why. +/// +/// # Safety +/// +/// Must be called exactly once, immediately after the [`early_init()`] call +/// that produced `early` and the `static_init!()` that produced `chip` (both +/// from the same boot, same `C`) -- this allocates more `'static` state and +/// starts loading processes from the linker-defined app regions, neither of +/// which is safe to repeat. #[inline(never)] pub unsafe fn finish_start( early: EarlyInit,