Skip to content

test(qemu): run the no_std code on emulated Cortex-M0, not just cargo check - #21

Merged
ddsha441981 merged 1 commit into
mainfrom
test/qemu-cortex-m
Sep 5, 2026
Merged

ddsha441981 merged 1 commit into
mainfrom
test/qemu-cortex-m

Conversation

@ddsha441981

Copy link
Copy Markdown
Owner

What this adds

PR #20 added a CI matrix proving pulse_map compiles for 8 bare-metal targets, and found two that previously did not (thumbv6m-none-eabi, riscv32imc-unknown-none-elf). But cargo check does not link and never executes an instruction, so the critical-section path it introduced was still an unverified claim.

This boots a real binary on emulated hardware.

New qemu-test/ crate — cortex-m-rt + memory.x + a panic handler — run under qemu-system-arm on two machines:

Machine Target AtomicU64 comes from
-cpu cortex-m3 -machine lm3s6965evb thumbv7m-none-eabi portable-atomic's spinlock fallback
-machine microbit (nRF51822) thumbv6m-none-eabi critical-section — no CAS instruction exists

The second row is the point. MetaWord::on_access performs an AtomicU64 CAS on every get hit, and ARMv6-M has no LDREX/STREX, so on Cortex-M0 that CAS can only work through the critical-section feature. A passing get there is the runtime proof. microbit is the only emulated ARM machine that is actually thumbv6m — lm3s6965evb, the machine the Embedded Rust Book uses, is Cortex-M3 and already passed before PR #20, so it proves nothing new on its own and serves here as the control.

Local results

$ cargo run --release --target thumbv7m-none-eabi
qemu-test: 16 buckets, 64 nominal slots, 3072 heap bytes used
qemu-test: all checks passed

$ cargo run --release --target thumbv6m-none-eabi --features m0
qemu-test: 16 buckets, 64 nominal slots, 3072 heap bytes used
qemu-test: all checks passed

What it checks

12 assertions: capacity, empty state, get hit/miss, len after insert, insert 4× capacity to force eviction then peek every key, eviction_count() > 0, len within capacity, TTL live-then-expired by insertion count, remove reports a hit, removed key gone.

The eviction check is deliberately asymmetric: a missing key is legal — which of a bucket's four slots loses is not observable from outside — but a key reading back a value it was never stored with never is. That is the invariant worth asserting.

Failures are real failures. Checks report through semihosting and call debug::exit(EXIT_FAILURE), which reaches cargo run as a non-zero process exit and fails the job. Verified by breaking one assertion on purpose:

FAIL: capacity
qemu-test: 1 check(s) FAILED
=== exit code: 1 ===

Measured along the way: 128 bytes per bucket, not 64

The first run died with memory allocation of 256 bytes failed on an 8 KiB heap. PulseMapRaw::new allocates two Vecs, not one:

  • buckets — 64 B per bucket, the cache line
  • slots_ttl — 4 × sizeof(SlotTTL) = 4 × 16 B per bucket

So a map costs 128 B per bucket, taken upfront regardless of occupancy. A 64-bucket map is 8 KiB — the entire heap I had budgeted. Not a crate bug; my test was sized against a wrong model of the cost. It now runs 16 buckets (2 KiB, 64 nominal slots) and prints its own heap usage into the CI log.

This is consistent with the README's measured 40.0 B/entry (16,384 buckets × 128 B ÷ ~52,800 resident entries ≈ 39.7), but on a 16 KiB part bucket count, not entry count, is the number to budget with — so the README now says so explicitly.

Scope, stated plainly

  • riscv32imc-unknown-none-elf (ESP32-C3) stays compile-checked only. qemu-system-riscv32 -machine virt has the A extension, so emulating it would exercise a target that does not need the feature — a green job there would be misleading.
  • Nothing has run on physical silicon.
  • The bump allocator in the test is a run-once toy (dealloc is a no-op); real firmware wants embedded-alloc. It is marked as such.

Notes

  • qemu-test/ is its own workspace with its own .cargo/config.toml runner — the same isolation fuzz/ already uses — so it cannot affect a host build of pulse_map. No changes to src/.
  • CI installs QEMU with --no-install-recommends: the default pulls qemu-efi-aarch64, 322 MB of aarch64 UEFI firmware that nothing here boots.
  • README's "nothing here has been executed on real silicon or under QEMU" is now replaced with what was actually executed.
  • CHANGELOG appended under ## [Unreleased] — v0.6.5.

PR #20 added a CI matrix proving the crate compiles for 8 bare-metal
targets. `cargo check` does not link and executes nothing, so the
`critical-section` path on Cortex-M0 was still an unverified claim.

New `qemu-test/` crate links a real `cortex-m-rt` binary and boots it
under `qemu-system-arm`, on two machines:

  -cpu cortex-m3      thumbv7m — portable-atomic's spinlock fallback
  -machine microbit   thumbv6m — Cortex-M0, no CAS instruction at all

The second is the point. `MetaWord::on_access` runs an AtomicU64 CAS on
every `get` hit, and on ARMv6-M that can only work through
`critical-section`, so a passing `get` there is runtime proof. microbit
(nRF51822) is the only emulated ARM machine that is actually thumbv6m.

12 assertions: capacity, empty state, hit/miss, insert past 4x capacity
to force eviction then peek every key (absence is legal, a value the key
was never stored with is not), eviction_count, TTL live-then-expired by
insertion count, remove. Failures exit EXIT_FAILURE through semihosting,
which reaches `cargo run` as a non-zero exit; confirmed by breaking one
assertion on purpose.

Also measured, and now documented: a map costs 128 bytes per bucket up
front — 64 for the Bucket, 4x16 for its SlotTTL entries — not the 64 the
cache-line framing suggests. A 64-bucket map exhausted an 8 KiB heap.

riscv32imc stays compile-checked only: qemu-system-riscv32 -machine virt
has the A extension, so it would test a target that doesn't need the
feature.
@ddsha441981
ddsha441981 merged commit f144eac into main Sep 5, 2026
23 checks passed
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