Skip to content

Add QEMU ARM MPS2 AN385/AN386 boards (Cortex-M3/M4) - #1

Closed
ppannuto-claude wants to merge 6 commits into
ppannuto:qemu-arm-mps2from
ppannuto-claude:qemu-arm-mps2
Closed

Add QEMU ARM MPS2 AN385/AN386 boards (Cortex-M3/M4)#1
ppannuto-claude wants to merge 6 commits into
ppannuto:qemu-arm-mps2from
ppannuto-claude:qemu-arm-mps2

Conversation

@ppannuto-claude

Copy link
Copy Markdown

Pull Request Overview

Adds two new QEMU-backed ARM boards, qemu_arm_mps2_an385 (Cortex-M3) and
qemu_arm_mps2_an386 (Cortex-M4), targeting QEMU's mps2-an385/mps2-an386
machines (ARM's CMSDK reference platform). Tock has QEMU-backed RISC-V
(qemu_rv32_virt/qemu_rv64_virt) and x86 (qemu_i486_q35) boards, but no
ARM one — this fills that gap for Cortex-M CI/development without real
hardware.

Peripherals implemented in a shared qemu_arm_mps2_chip crate:

  • CMSDK APB UART (console/debug)
  • CMSDK APB Timer (Alarm/Time HIL)
  • FPGAIO LEDs
  • PL022 SPI (SpiMaster), run in hardware loopback since QEMU attaches no
    SSI 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)
  • CMSDK APB Watchdog (real hardware, backs WatchDog)
  • Process/MPU support (both boards run real userspace apps)

The two boards' own setup (syscall driver lookup, KernelResources,
capsule/process init) is shared in a new boards/qemu_arm_mps2_lib crate,
generic over cortexm::CortexMVariant: each board's main.rs is ~40 lines
(the concrete CortexM3/CortexM4 type argument, plus the handful of
things 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 (new
cortexm::support::semihost_command() primitive, since none existed for
ARM in-tree) so qemu-system-arm exits on its own under -semihosting
instead of hanging until killed — matching how qemu_rv32_virt already
behaves 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_q35 also don't do
GPIO). See boards/qemu_arm_mps2_an385/README.md for details and sourcing.

CI wiring (Makefile arm-softmmu target list, tools/ci/qemu-runner
entries, boards/README.md rows) is included but has only been verified
against the system-installed QEMU 10.2.1, not the exact CI-pinned QEMU
commit.

Testing Strategy

  • Both boards build clean and boot under QEMU 10.2.1, verified interactively
    (not just a boot-banner check): console/debug UART, list via the process
    console (exercises the timer/alarm — kernel doesn't hang).
  • Loaded libtock-c's c_hello and blink simultaneously via a new
    run-app Makefile target and confirmed both scheduled and ran correctly:
    c_hello printed via a real console syscall, blink ran continuously.
    This is what actually exercises SyscallDriverLookup,
    UserspaceKernelBoundary, and per-process MPU setup.
  • LEDs confirmed by reading the FPGAIO LED0 register live through the
    QEMU monitor while blink ran, observing it toggle.
  • SPI verified with a new loopback test app
    (tock/libtock-c#585: write a
    pattern, read it back, memcmp, print SPI PASS/FAIL) loaded alongside
    c_hello+blink on both boards — three concurrently-scheduled processes
    all correctly serviced through UART, Timer, and SPI at once.
  • Watchdog verified two ways: negative (apps ran ~9 reload periods with a
    single boot banner, no false-positive reset) and positive (temporarily
    disabled tickle() — and had to also disable suspend()/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).
  • After the qemu_arm_mps2_lib refactor: both boards rebuild to the
    identical binary hash as before it, and the SPI loopback test was re-run
    on both, still SPI PASS. The semihosting exit was verified by
    temporarily forcing a panic and confirming qemu-system-arm exits on its
    own (exit code 0) instead of hanging.
  • make prepush passes (format, clippy, syntax, license check across the
    whole workspace).

TODO or Help Wanted

  • CI wiring hasn't been run against the actual CI-pinned QEMU commit, only
    the system-installed 10.2.1 — worth a check on first CI run.
  • No PR yet to libtock-c for a proper SPI loopback example app — done:
    tock/libtock-c#585.

Checklist

  • Ran make prepush.

PR Contents

Documentation

  • Updated the relevant files in /docs and 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 this
follows the existing QEMU-board documentation pattern.

AI Use

  • AI was used in this PR. I have read Tock's AI policy and have properly disclosed my AI use below.

This PR was written by Claude (Claude Code) across an extended interactive
session, not from a single prompt. Representative directives from that
session:

❯ Investigate an appropriate ARM QEMU board [for Tock] and propose a plan
❯ Make a full implementation plan for mps2-an385/an386 including GPIO and LED support
❯ [after GPIO was shown non-functional under QEMU here] Remove gpio from the implementation plan
❯ Start implementing
❯ Yes, you should verify that both c_hello and blink can be loaded at the same time and execute correctly
❯ Make a plan for implementing SPI and Watchdog. The plan should include explicit testing by user space apps
❯ For SPI, use a dummy implementation for CS. For watchdog, do the positive
  test by temporarily commenting tickle. Implement and verify SPI first,
  committing when done. Then implement and test watchdog in a new commit
❯ Address review comments on 5115

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 than
generated 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.

ppannuto-claude and others added 6 commits August 25, 2026 23:07
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>
@ppannuto-claude
ppannuto-claude changed the base branch from master to qemu-arm-mps2 August 27, 2026 17:06
@ppannuto-claude

Copy link
Copy Markdown
Author

Closing: the changes on this branch have been incorporated into ppannuto/tock:qemu-arm-mps2 directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant