diff --git a/Cargo.lock b/Cargo.lock index 8a6bd64536b..c5e0c301ebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,51 @@ dependencies = [ "kernel", ] +[[package]] +name = "qemu_arm_mps2_an385" +version = "0.2.3-dev" +dependencies = [ + "cortexm3", + "kernel", + "qemu_arm_mps2_chip", + "qemu_arm_mps2_lib", + "tock_build_scripts", +] + +[[package]] +name = "qemu_arm_mps2_an386" +version = "0.2.3-dev" +dependencies = [ + "cortexm4", + "kernel", + "qemu_arm_mps2_chip", + "qemu_arm_mps2_lib", + "tock_build_scripts", +] + +[[package]] +name = "qemu_arm_mps2_chip" +version = "0.2.3-dev" +dependencies = [ + "cortexm", + "cortexm3", + "cortexm4", + "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 5780cb36381..5a7811e5f53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,9 @@ members = [ "boards/teensy40", "boards/nano33ble", "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", @@ -104,6 +107,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/arch/cortex-m/src/support.rs b/arch/cortex-m/src/support.rs index 407d9bef9dd..05d9dddcfe6 100644 --- a/arch/cortex-m/src/support.rs +++ b/arch/cortex-m/src/support.rs @@ -184,3 +184,87 @@ 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. +/// +/// 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)] +unsafe fn semihost_command(operation: u32, parameter: u32) -> u32 { + use core::arch::asm; + let result; + + // 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. + // - 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 +} + +/// 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_terminate() { + unimplemented!() +} 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..cc98cad5f94 --- /dev/null +++ b/boards/qemu_arm_mps2_an385/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_an385" +version.workspace = true +authors.workspace = true +build = "../build.rs" +edition.workspace = true + +[dependencies] +cortexm3 = { path = "../../arch/cortex-m3" } +kernel = { path = "../../kernel" } +qemu_arm_mps2_chip = { path = "../../chips/qemu_arm_mps2_chip", features = ["cortex-m3"] } +qemu_arm_mps2_lib = { path = "../qemu_arm_mps2_lib" } + +[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..f257144bee0 --- /dev/null +++ b/boards/qemu_arm_mps2_an385/Makefile @@ -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. + +# 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 + +# 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 := \ + $(QEMU_CMD) \ + -machine mps2-an385 \ + -nographic \ + -semihosting + +# 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 + +# 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 new file mode 100644 index 00000000000..2e92c04e204 --- /dev/null +++ b/boards/qemu_arm_mps2_an385/README.md @@ -0,0 +1,90 @@ +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. 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. + +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`). +- 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. + +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$ + ``` + +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 +----------------------- + +- **`run-app`**: Start Tock with one or more apps loaded at + `APP_ADDRESS` (0x00040000): + + ``` + $ make run-app APP=$PATH_TO_APP.tbf + ``` + + 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 new file mode 100644 index 00000000000..1d86b019e9f --- /dev/null +++ b/boards/qemu_arm_mps2_an385/chip_layout.ld @@ -0,0 +1,29 @@ +/* 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 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) + * 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..7ed99309d2a --- /dev/null +++ b/boards/qemu_arm_mps2_an385/src/io.rs @@ -0,0 +1,52 @@ +// 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::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] +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, + PANIC_RESOURCES.get(), + ); + + // 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 new file mode 100644 index 00000000000..e4141235dfe --- /dev/null +++ b/boards/qemu_arm_mps2_an385/src/main.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. + +//! 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). +//! +//! 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::create_capability; +use kernel::static_init; + +pub mod io; + +kernel::stack_size! {0x2000} + +/// Main function called after RAM initialized. +#[no_mangle] +pub unsafe fn main() { + // 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 + // `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) + ); + + // 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."); + + let main_loop_capability = create_capability!(capabilities::MainLoopCapability); + 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/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..bf8e17d4889 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/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_an386" +version.workspace = true +authors.workspace = true +build = "../build.rs" +edition.workspace = true + +[dependencies] +cortexm4 = { path = "../../arch/cortex-m4" } +kernel = { path = "../../kernel" } +qemu_arm_mps2_chip = { path = "../../chips/qemu_arm_mps2_chip", features = ["cortex-m4"] } +qemu_arm_mps2_lib = { path = "../qemu_arm_mps2_lib" } + +[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..2fc37749e34 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/Makefile @@ -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. + +# 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 + +# 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 := \ + $(QEMU_CMD) \ + -machine mps2-an386 \ + -nographic \ + -semihosting + +# 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 + +# 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 new file mode 100644 index 00000000000..19f7b38afb1 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/README.md @@ -0,0 +1,40 @@ +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$ + ``` + +- **`run-app`**: same as `qemu_arm_mps2_an385`'s (`make run-app + APP=$PATH_TO_APP.tbf`). + +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/chip_layout.ld b/boards/qemu_arm_mps2_an386/chip_layout.ld new file mode 100644 index 00000000000..1078fb45e7d --- /dev/null +++ b/boards/qemu_arm_mps2_an386/chip_layout.ld @@ -0,0 +1,29 @@ +/* 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 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 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) + * 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..8b74e83f085 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/src/io.rs @@ -0,0 +1,52 @@ +// 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::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] +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, + PANIC_RESOURCES.get(), + ); + + // 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 new file mode 100644 index 00000000000..43537f8f361 --- /dev/null +++ b/boards/qemu_arm_mps2_an386/src/main.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. + +//! 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). +//! +//! 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::create_capability; +use kernel::static_init; + +pub mod io; + +kernel::stack_size! {0x2000} + +/// Main function called after RAM initialized. +#[no_mangle] +pub unsafe fn main() { + // 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 + // `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) + ); + + // 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."); + + let main_loop_capability = create_capability!(capabilities::MainLoopCapability); + 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..e733d31dcd0 --- /dev/null +++ b/boards/qemu_arm_mps2_lib/src/lib.rs @@ -0,0 +1,328 @@ +// 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 [`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] + +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 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, + 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. +/// +/// # 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>>, +) -> 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. +/// +/// # 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, + 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/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..fcbbfc0bc1e --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/interrupts.rs @@ -0,0 +1,28 @@ +// 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 (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; +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; +/// 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; +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..88f568a5a38 --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/lib.rs @@ -0,0 +1,71 @@ +// 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 spi; +pub mod timer; +pub mod uart; +pub mod watchdog; + +#[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, 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>, + pub watchdog: watchdog::Watchdog, +} + +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), + spi_shield0: spi::Spi::new(spi::SPI_SHIELD0_BASE), + watchdog: watchdog::Watchdog::new(watchdog::WATCHDOG_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(), + 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..bcff2482d68 --- /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. +//! +//! 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) {} +} 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/chips/qemu_arm_mps2_chip/src/watchdog.rs b/chips/qemu_arm_mps2_chip/src/watchdog.rs new file mode 100644 index 00000000000..3baa4bcf2ef --- /dev/null +++ b/chips/qemu_arm_mps2_chip/src/watchdog.rs @@ -0,0 +1,120 @@ +// 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. +//! +//! 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 +//! (`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. 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; +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); + } +} 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.");