Add QEMU ARM MPS2 AN385/AN386 boards (Cortex-M3/M4) - #1
Closed
ppannuto-claude wants to merge 6 commits into
Closed
Conversation
Tock has QEMU-backed RISC-V and x86 boards but no ARM one, making it hard to test the Cortex-M port without real hardware. This adds two new boards targeting QEMU's `mps2-an385` (Cortex-M3) and `mps2-an386` (Cortex-M4) machines: emulations of ARM's own CMSDK reference platform, in the same spirit as the existing `qemu_rv32_virt`/`qemu_rv64_virt` boards (a stable virtual target, not a real vendor chip). New `chips/qemu_arm_mps2_chip` crate, shared by both boards (an385/an386 differ only in CPU core): - CMSDK APB UART driver (console/debug UART) - CMSDK APB Timer driven as the kernel's Alarm/Time HIL, by keeping RELOAD fixed at u32::MAX so the hardware free-runs and re-arming an alarm just shortens the current countdown by writing VALUE directly - LEDs via the FPGAIO block's LED0 register, not GPIO: QEMU emulates all four CMSDK AHB GPIO banks on this whole machine family (an385/386/500/511 and even the Cortex-M33 TrustZone an505/an521) as inert stubs that discard writes and always read 0, so pin state is never observable under this QEMU model. FPGAIO is genuinely emulated, so that's the only place LED state is real. - Vector table setup lives here (as concrete, non-generic per-core modules gated by cortex-m3/cortex-m4 Cargo features), not in the board crates: putting it in a board crate collides at LTO time with components::process_console's own `_estack` extern declaration (fn-typed vs data-typed references to the same linker symbol landing in one compilation unit), because the generic QemuArmMps2Chip::print_state gets monomorphized into whichever crate first instantiates a concrete CortexMVariant. Also required adding `thumbv7m-none-eabi` to rust-toolchain.toml (an385 is the first Cortex-M3 target in the workspace), widening the CI QEMU build's --target-list to include arm-softmmu, and wiring both boards into tools/ci/qemu-runner. Both boards verified booting under QEMU 10.2.1 (console, debug writer, process console, and the timer-backed alarm all confirmed working via an interactive `list` command over the emulated UART). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a run-app Makefile target (mirroring qemu_rv32_virt's) to both boards, loading app data via -device loader at the prog flash base. Uses the .bin kernel image rather than .elf: QEMU refuses to load two overlapping ROM blobs, and the kernel ELF's own 4-byte .apps placeholder otherwise collides with the injected app data at the same address. Verified on both an385 and an386 by building libtock-c's c_hello and blink examples and loading them at the same time: both processes load, schedule, and run correctly (c_hello prints via a real console syscall, blink runs continuously), which is what actually exercises the syscall dispatch, UserspaceKernelBoundary/switch_to_user, and per-process MPU setup that console/process-console alone never reach (those are kernel-internal and don't cross the syscall boundary). Also confirmed blink's LED toggling for real by reading the FPGAIO LED0 register live through the QEMU monitor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drives one of the machine's five PL022 controllers (the "Shield0" instance at 0x40026000, IRQ 24) as an hil::spi::SpiMaster, modeled on chips/rp2040/src/spi.rs, which already drives the same PrimeCell IP block. This is the first SPI support on any Tock QEMU board. Two real constraints from this QEMU model, not implementation shortcuts: - None of the five PL022 instances have an SSI slave device attached in QEMU (hw/arm/mps2.c creates bare controllers, no ssi_create_peripheral), so a non-loopback transfer just reads back whatever the empty bus's default is. The driver always enables CR1.LBM (loopback) in init() -- the only way to get a genuine, deterministic transfer under this model. - No functional GPIO to toggle for chip select (see led.rs's existing GPIO-is-a-stub docs), so ChipSelect is a zero-sized placeholder per the user's direction, rather than the GPIO-pin-based chip select every other Tock SpiMaster wiring uses. Verified with a new loopback test app (write a known pattern, read it back, memcmp, print SPI PASS/FAIL) loaded alongside c_hello and blink on both boards -- confirming three concurrently-scheduled processes are all correctly serviced through this chip crate's UART, Timer, and SPI drivers at once. libtock-c's existing wip/spi/* examples predate the current libtock_spi_controller_* API and don't build against it, so this needed a new app rather than reviving one of those. Along the way, discovered that Tock's own capsules_core::spi_controller capsule has never implemented the "set chip select" command on any board (a hard-coded NOSUPPORT, "TODO: do nothing, for now") -- not a bug in this board, documented in the README so it isn't mistaken for one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drives the CMSDK APB Watchdog (SP805-style, base 0x40008000) as the kernel's WatchDog resource. Unlike GPIO, this is a real, non-stub QEMU peripheral: it genuinely counts down and genuinely resets the machine. Its interrupt is wired to NMI, not a normal NVIC line, so unlike the other peripherals in this chip crate it's never dispatched through InterruptService -- this board's vector table already maps NMI to unhandled_interrupt, so a missed kick surfaces as a panic first. Verified in two parts, since a well-behaved userspace app can't legitimately hang the kernel to trigger the real failure mode: - Negative test: ran c_hello, blink, and spi_loopback_test together for ~18s (~9x the ~2s reload period) and confirmed the boot banner printed exactly once -- no spurious reset under real interrupt/ scheduling load. - Positive test (one-off, not shipped): temporarily commented out the tickle() call in kernel/src/kernel.rs. Discovered along the way that idle sleep/wake cycles alone also incidentally reload the watchdog (setting INTEN high after being disabled reloads from WDOGLOAD -- a real SP805 hardware property, not a QEMU quirk), so suspend()/ resume() had to be neutralized too to actually provoke an expiry. Result: NMI fired on schedule (panicked with "Unhandled Interrupt. ISR 2 is active."), and since the panic loop doesn't kick the dog either, QEMU genuinely reset the machine shortly after, repeating in a clean cycle. Both changes reverted immediately after, confirmed via an identical post-revert binary hash. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Extract the two boards' shared setup (syscall driver lookup, KernelResources, capsule/process init) into a new qemu_arm_mps2_lib crate, generic over cortexm::CortexMVariant. Each board's main.rs shrinks to just the concrete CortexM3/CortexM4 type argument, plus a static_init!() and a #[panic_handler] that structurally can't be generic (a static can't reference an enclosing generic function's own type parameter). - Add cortexm::support::semihost_command(), since no ARM semihosting primitive existed in-tree; call it from each board's panic handler and pass -semihosting to QEMU, so a panic now makes qemu-system-arm exit on its own instead of hanging until killed. - Trim both READMEs down to reference documentation: move LED observability into the peripheral list itself, drop verification transcripts and the watchdog test narrative (that belongs in commit history, not a permanent README), and fix an385/an386 chip_layout.ld comments (an386's had copy-pasted "AN385"). - Minor doc-comment cleanups in spi.rs/watchdog.rs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Make cortexm::support::semihost_command() module-private and add a narrow pub semihost_terminate() instead -- the general, unrestricted semihosting interface shouldn't be exposed when only one specific operation is actually needed. - Add /// # Safety docs and // SAFETY: comments to semihost_command, semihost_terminate, and qemu_arm_mps2_lib's early_init()/finish_start(), plus their call sites, per Tock's SAFETY comment convention. - Move the shared memory-layout documentation from an385's README (with an386's pointing at it) into qemu_arm_mps2_lib's crate-level doc comment, since that's the crate actually shared between both boards. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Author
|
Closing: the changes on this branch have been incorporated into |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request Overview
Adds two new QEMU-backed ARM boards,
qemu_arm_mps2_an385(Cortex-M3) andqemu_arm_mps2_an386(Cortex-M4), targeting QEMU'smps2-an385/mps2-an386machines (ARM's CMSDK reference platform). Tock has QEMU-backed RISC-V
(
qemu_rv32_virt/qemu_rv64_virt) and x86 (qemu_i486_q35) boards, but noARM one — this fills that gap for Cortex-M CI/development without real
hardware.
Peripherals implemented in a shared
qemu_arm_mps2_chipcrate:Alarm/TimeHIL)FPGAIOLEDsSpiMaster), run in hardware loopback since QEMU attaches noSSI slave to any PL022 instance on this machine; chip select is a
zero-sized placeholder for the same reason (no functional GPIO to toggle
one — see below)
WatchDog)The two boards' own setup (syscall driver lookup,
KernelResources,capsule/process init) is shared in a new
boards/qemu_arm_mps2_libcrate,generic over
cortexm::CortexMVariant: each board'smain.rsis ~40 lines(the concrete
CortexM3/CortexM4type argument, plus the handful ofthings Rust's generics can't cover — a
static_init!()and a#[panic_handler], both of which need a concrete, non-generic call site).On panic, both boards now also issue an ARM semihosting
SYS_EXIT(newcortexm::support::semihost_command()primitive, since none existed forARM in-tree) so
qemu-system-armexits on its own under-semihostinginstead of hanging until killed — matching how
qemu_rv32_virtalreadybehaves via RISC-V's own semihosting call.
GPIO is intentionally not implemented: QEMU models all CMSDK GPIO banks
on this machine as inert stubs (writes discarded, reads always 0), so there's
nothing observable to drive a capsule against. This matches every other Tock
QEMU board (
qemu_rv32_virt/qemu_rv64_virt/qemu_i486_q35also don't doGPIO). See
boards/qemu_arm_mps2_an385/README.mdfor details and sourcing.CI wiring (Makefile
arm-softmmutarget list,tools/ci/qemu-runnerentries,
boards/README.mdrows) is included but has only been verifiedagainst the system-installed QEMU 10.2.1, not the exact CI-pinned QEMU
commit.
Testing Strategy
(not just a boot-banner check): console/debug UART,
listvia the processconsole (exercises the timer/alarm — kernel doesn't hang).
libtock-c'sc_helloandblinksimultaneously via a newrun-appMakefile target and confirmed both scheduled and ran correctly:c_helloprinted via a real console syscall,blinkran continuously.This is what actually exercises
SyscallDriverLookup,UserspaceKernelBoundary, and per-process MPU setup.FPGAIOLED0register live through theQEMU monitor while
blinkran, observing it toggle.(tock/libtock-c#585: write a
pattern, read it back,
memcmp, printSPI PASS/FAIL) loaded alongsidec_hello+blinkon both boards — three concurrently-scheduled processesall correctly serviced through UART, Timer, and SPI at once.
single boot banner, no false-positive reset) and positive (temporarily
disabled
tickle()— and had to also disablesuspend()/resume(),since idle sleep/wake cycles incidentally reload the watchdog too, a real
SP805 hardware property — confirmed the NMI fires and QEMU genuinely
resets the machine on schedule; reverted immediately, verified via
identical post-revert binary hash before committing anything).
qemu_arm_mps2_librefactor: both boards rebuild to theidentical binary hash as before it, and the SPI loopback test was re-run
on both, still
SPI PASS. The semihosting exit was verified bytemporarily forcing a panic and confirming
qemu-system-armexits on itsown (exit code 0) instead of hanging.
make prepushpasses (format, clippy, syntax, license check across thewhole workspace).
TODO or Help Wanted
the system-installed 10.2.1 — worth a check on first CI run.
No PR yet to— done:libtock-cfor a proper SPI loopback example apptock/libtock-c#585.
Checklist
make prepush.PR Contents
Documentation
/docsand the Book.Per-board READMEs added/updated (
boards/qemu_arm_mps2_an385/README.md,boards/qemu_arm_mps2_an386/README.md); no Book changes needed since thisfollows the existing QEMU-board documentation pattern.
AI Use
This PR was written by Claude (Claude Code) across an extended interactive
session, not from a single prompt. Representative directives from that
session:
All register maps and IRQ numbers were ground-truthed by reading actual QEMU
10.2 source (
hw/arm/mps2.c,hw/ssi/pl022.c,hw/watchdog/cmsdk-apb-watchdog.c,hw/misc/mps2-fpgaio.c,hw/char/cmsdk-apb-uart.c,hw/timer/cmsdk-apb-timer.c) rather thangenerated from training data.
Review status: reviewed by Pat Pannuto (changes requested) — the
board-duplication question, semihosting exit, README trims, and doc-comment
fixes above address that round. Not yet re-reviewed/approved.