Skip to content

walk-free B1: SysTick + SCB scheduler migration + batched-IRQ dispatch fix#518

Merged
w1ne merged 4 commits into
mainfrom
perf/walk-free-b1-systick
Jul 11, 2026
Merged

walk-free B1: SysTick + SCB scheduler migration + batched-IRQ dispatch fix#518
w1ne merged 4 commits into
mainfrom
perf/walk-free-b1-systick

Conversation

@w1ne

@w1ne w1ne commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Batch B1 of the walk-free campaign — SysTick and SCB leave the per-cycle walk, and the armed-firmware differential flushed out a real pre-existing engine bug.

The bug (fixed first, own commit)

CortexM::step_batch broke the batch when a takeable exception was pending — including at executed == 0, before step_internal could dispatch it. Machine::run then saw zero progress and returned; every batched caller spun forever at the same PC the moment a pended IRQ became takeable between batches. Unnoticed because every in-tree IRQ harness drives machine.step() — B1's differential is the first gate running IRQ firmware through the batched path (which is the whole point of walk freedom). Fix: break only after progress; at batch top the exception is dispatched exactly like the single-step path (per-instruction byte-identity pinned).

SysTick

Lazy CVR via the bus CycleClock: exact closed-form replay of the edge-triggered countdown (property-tested against a literal tick-by-tick walk, incl. RVR-shrunk-mid-count), COUNTFLAG latch/read-clear semantics preserved, CVR-write does not cancel a latched pend (silicon behavior, unit-pinned). Expiry as scheduled events routing system_exception: Some(15) through the existing apply_event_result arm; accumulated wraps merge into one pend like silicon's single PENDST bit; arm_seq kills stale chains. Also normalizes the tick-cost artifact: legacy SysTick charged +1 cycle per enabled tick (inflating total_cycles ~2× while armed — real SysTick consumes no core cycles); both modes now charge 0.

SCB

Its tick was an ICSR write-latch drain (one per cycle, NMI>ST>PendSV) — now a delay-0 event chain replicating that pacing exactly. drain_scb_reset_request untouched.

Evidence

tests/systick_walk_differential.rs: delay-loop + SysTick-ISR firmware — per-instruction probes (total_cycles, PC, all 16 registers, raw CVR polls, RAM counters) byte-identical walk vs scheduler at interval 1; ISR count exact at interval 64 with the documented grid-quantization bound; PendSV same-cycle; cpsid i; wfi wake + idle fast-forward clamping to the scheduled expiry. firmware_survival (real HAL/FreeRTOS/Zephyr) green both lanes.

Surface: invaders walk-forcing set 22 → 20 (systick, scb out). kw41z is now 4 walkers from full walk-deletion derivation. All lanes green; known machine-local failures verified identical on origin/main.

w1ne added 4 commits July 11, 2026 21:08
…ead of wedging the run loop

step_batch's early-out broke the batch whenever a takeable exception was
pending — including at executed == 0, BEFORE the first step_internal could
dispatch it. Machine::run then saw zero progress and returned, and every
batched caller (Machine::run(Some(n)) loops, the wasm step_batch wrapper)
spun forever at the same PC the moment a walk- or scheduler-pended IRQ
(e.g. an armed SysTick) became takeable between batches.

Nothing hit this before because every IRQ-driven Cortex-M harness in-tree
(firmware_survival, integration tests, the CLI single-step path) drives
machine.step(), which dispatches inside step_internal. The walk-free B1
differential is the first gate to run IRQ firmware through the batched
path — which is the whole point of walk deletion, so batched dispatch has
to work.

Fix: break only after the batch has made progress (executed > 0); at the
batch top the pending exception is dispatched by step_internal exactly
like the single-step path (byte-identical timing at interval 1). Both the
SystemBus fast arm and the boxed dyn-Bus arm get the same rule.
…, scheduled exception-15 expiries, write-armed ICSR pend drain

The highest-value walker migration (plan Part 2, batch B1): every HAL
delay loop arms SysTick, so this is the batch that makes arbitrary user
firmware batchable.

SysTick (peripherals/systick.rs):
- Scheduler mode (event-scheduler + bus CycleClock attached): SYST_CVR is
  derived lazily from the published clock with exact modular arithmetic
  (advance_counter — closed-form replay of the walk's edge-triggered
  countdown, property-tested against a literal tick-by-tick walk), so
  &self reads stay fresh with zero walk. COUNTFLAG folds in on the same
  lazy sync; word-read-clears, CVR-write-clears, TICKINT/ENABLE gating,
  RVR=0 park and the Zephyr tickless-idle reload semantics preserved
  exactly.
- The periodic exception (system exception 15, NOT an NVIC line) rides
  scheduled events: arming writes emit (ticks_until_fire - 1, arm_seq)
  through take_scheduled_events (the bus + 1 convention pins the expiry
  to the exact legacy fire cycle); on_event claims accumulated wraps as
  ONE exception-15 pend (pends merge exactly as silicon's single PENDST
  bit) and reschedules from the just-synced state, so deadlines stay
  absolute — no cumulative drift at any tick interval. Stale chains die
  on an arm_seq token mismatch. Event results already route to
  Cpu::set_exception_pending via EventResult::system_exception — the
  system-exception arm of apply_event_result — so no new routing was
  needed (a future C3-interrupt-matrix arm slots in beside it).
- Legacy mode (feature off / no clock) keeps the eager per-cycle walk
  countdown byte-for-byte; the two modes are mutually exclusive by
  construction on ONE predicate (the rtc_timer pattern).

SCB (peripherals/scb.rs):
- Its tick() was a write-latch drain: ICSR SET bits (NMI/PENDST/PENDSV)
  drained one per cycle, NMI > SysTick > PendSV. That is not periodic
  work, so it becomes a write-armed delay-0 event chain: one chain at a
  time (drain_chain_armed), on_event drains ONE latch per event and
  reschedules at +1 cycle while more remain — replicating the legacy
  one-exception-per-cycle pacing exactly. AIRCR SYSRESETREQ stays on the
  machine-level drain_scb_reset_request (untouched).

Tick-cost normalization: SysTick charged cycles:1 into the tick-cost
channel on every enabled tick (SCB per drained pend), inflating
total_cycles by ~1/cycle while armed — a sim artifact (real SysTick
consumes zero core cycles) structurally incompatible with walk deletion.
Both models now charge zero in both modes; the logic-capture cost-path
fixture and the metrics accounting test keep their coverage via a
dedicated CostTicker test peripheral and now also pin systick's cost at
zero.

Attach chokes: push_peripheral (the from_config twin of add_peripheral)
now hands every descriptor-built model the bus CycleClock;
configure_cortex_m attaches it to the SCB it installs by replacement.
Hand-built buses that bypass the chokes keep exact legacy semantics.

Gates:
- tests/systick_walk_differential.rs (replaces the Part-1 spike, as the
  plan prescribes): hand-assembled Thumb firmware, walk-on reference vs
  scheduler from the same bus assembly.
  * delay-loop + SysTick-ISR-counter firmware (arms RVR/CVR/CSR, polls
    CVR): per-instruction probes of total_cycles/PC/all registers/RAM
    counters byte-identical at interval 1 (pins IRQ delivery cycles and
    ISR count exactly).
  * scheduler @ 64 vs walk-on @ 1: ISR count over a fixed window exact;
    delivery quantisation bound (< one interval, delay-only) verified
    against the reference's own delivery trace.
  * ICSR.PENDSVSET firmware: PendSV delivery byte-identical at interval 1.
  * cpsid-i/wfi + periodic wfi/ISR loop with idle fast-forward: FF clamps
    to the SCHEDULED SysTick expiry, wakes with identical event
    observables and one thread pass per wake, retiring <1/4 of the
    instructions.
- B0 surface test updated: systick + scb leave the walk-forcing set in
  event-scheduler builds (20 remain: 11 timers, dma1/2, i2c1-3, adc1,
  exti, can1, dwt); the featureless lane honestly keeps 22. No bus flips
  derive_walk_deletable yet (kw41z is closest: mcg, rsim, i2c1, dwt).
…ouched by walk-free B1; sim-validated tier, no silicon capture, so no drift_ack owed)
@w1ne w1ne merged commit ee4746c into main Jul 11, 2026
2 checks passed
@w1ne w1ne deleted the perf/walk-free-b1-systick branch July 11, 2026 19:17
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