Merge pull request #221 from davidmonterocrespo24/fix/rp2040-realtime-idle-elision
fix(rp2040): keep delay()-based sketches real-time on slower hosts
This commit is contained in:
commit
31dafdac4a
|
|
@ -0,0 +1,164 @@
|
|||
/**
|
||||
* RP2040 real-time scheduler
|
||||
*
|
||||
* The RP2040 core (125 MHz Cortex-M0) is ~8x heavier to emulate than the AVR.
|
||||
* A `delay()` on the arduino-pico core BUSY-WAITS (polls the timer in a tight
|
||||
* loop) instead of sleeping, so the WFI fast-path never triggers and a host
|
||||
* that cannot sustain 125 M instr/s would render a 1 s blink every 4-5 s.
|
||||
*
|
||||
* Two mechanisms keep simulated time locked to wall-clock:
|
||||
* 1. the frame budget is derived from the measured wall-clock delta, and
|
||||
* 2. IdleSpinDetector recognises a side-effect-free busy-wait spin so the
|
||||
* scheduler advances the clock over it instead of grinding every cycle.
|
||||
*
|
||||
* These tests cover the detector in isolation (the risky heuristic) and the
|
||||
* end-to-end scheduler against a real rp2040js core running a hand-assembled
|
||||
* busy-wait loop — no compiled firmware fixture required.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { RP2040 } from 'rp2040js';
|
||||
import { RP2040Simulator, IdleSpinDetector } from '../simulation/RP2040Simulator';
|
||||
import { PinManager } from '../simulation/PinManager';
|
||||
|
||||
// ── IdleSpinDetector — the heuristic that decides what is safe to skip ───────
|
||||
describe('IdleSpinDetector', () => {
|
||||
const constGpio = () => 0;
|
||||
|
||||
it('detects a stable, side-effect-free spin after the threshold', () => {
|
||||
const d = new IdleSpinDetector(32);
|
||||
// Drive the real PC sequence: L, L+2, L, L+2, ... (backward branch each loop)
|
||||
const L = 0x100;
|
||||
let detected = false;
|
||||
d.observe(L, constGpio);
|
||||
for (let i = 0; i < 40; i++) {
|
||||
d.observe(L + 2, constGpio);
|
||||
detected = d.observe(L, constGpio) || detected; // L < L+2 → backward branch
|
||||
}
|
||||
expect(detected).toBe(true);
|
||||
});
|
||||
|
||||
it('never elides a bit-bang loop (GPIO changes every iteration)', () => {
|
||||
const d = new IdleSpinDetector(32);
|
||||
const L = 0x100;
|
||||
let toggling = 0;
|
||||
const changingGpio = () => (toggling ^= 1); // different value each read
|
||||
let detected = false;
|
||||
d.observe(L, changingGpio);
|
||||
for (let i = 0; i < 200; i++) {
|
||||
d.observe(L + 2, changingGpio);
|
||||
detected = d.observe(L, changingGpio) || detected;
|
||||
}
|
||||
expect(detected).toBe(false);
|
||||
});
|
||||
|
||||
it('never elides straight-line code (no backward branch)', () => {
|
||||
const d = new IdleSpinDetector(8);
|
||||
let detected = false;
|
||||
for (let pc = 0x100; pc < 0x100 + 8 * 100; pc += 2) {
|
||||
detected = d.observe(pc, constGpio) || detected;
|
||||
}
|
||||
expect(detected).toBe(false);
|
||||
});
|
||||
|
||||
it('resets on a long forward jump (loop that calls out)', () => {
|
||||
const d = new IdleSpinDetector(4);
|
||||
const L = 0x100;
|
||||
let detected = false;
|
||||
// Each "iteration" jumps far away (a bl to a subroutine) then comes back.
|
||||
for (let i = 0; i < 50; i++) {
|
||||
d.observe(L, constGpio);
|
||||
d.observe(L + 2, constGpio);
|
||||
d.observe(L + 0x4000, constGpio); // long forward jump → reset
|
||||
detected = d.observe(L, constGpio) || detected; // backward, but count was reset
|
||||
}
|
||||
expect(detected).toBe(false);
|
||||
});
|
||||
|
||||
it('noteElided() makes it re-accumulate before signalling again', () => {
|
||||
const d = new IdleSpinDetector(4);
|
||||
const L = 0x100;
|
||||
const tick = () => {
|
||||
d.observe(L + 2, constGpio);
|
||||
return d.observe(L, constGpio);
|
||||
};
|
||||
d.observe(L, constGpio);
|
||||
let detected = false;
|
||||
for (let i = 0; i < 4; i++) detected = tick() || detected;
|
||||
expect(detected).toBe(true);
|
||||
d.noteElided();
|
||||
// Immediately after eliding, it must NOT re-signal until the loop runs again.
|
||||
expect(tick()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── End-to-end scheduler against a real rp2040js core ────────────────────────
|
||||
describe('RP2040Simulator — real-time scheduler', () => {
|
||||
const RAM = 0x20000000;
|
||||
const NOP = 0xbf00;
|
||||
const B_BACK_1 = 0xe7fd; // b .-2 (branch to the previous 16-bit instruction)
|
||||
|
||||
let sim: RP2040Simulator;
|
||||
let rp: RP2040;
|
||||
|
||||
/** Build a simulator wrapping a fresh core preloaded with `opcodes` at RAM. */
|
||||
function withProgram(opcodes: number[]): void {
|
||||
rp = new RP2040();
|
||||
opcodes.forEach((op, i) => rp.writeUint16(RAM + i * 2, op));
|
||||
rp.core.PC = RAM;
|
||||
sim = new RP2040Simulator(new PinManager());
|
||||
// Inject the bare core directly — we are unit-testing the scheduler, not
|
||||
// the bootrom/flash loader.
|
||||
(sim as unknown as { rp2040: RP2040 }).rp2040 = rp;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
sim?.stop();
|
||||
});
|
||||
|
||||
it('elides a busy-wait spin: sim-time tracks wall-time with few instructions', () => {
|
||||
withProgram([NOP, B_BACK_1]); // nop; loop forever — a side-effect-free spin
|
||||
const before = sim.getCurrentCycles();
|
||||
const { cyclesAdvanced, instructionsExecuted } = sim.runFrameForTime(16); // 16 ms
|
||||
|
||||
// 16 ms of wall-clock at 125 MHz == 2 000 000 simulated cycles.
|
||||
expect(cyclesAdvanced).toBeGreaterThan(1_900_000);
|
||||
expect(sim.getCurrentCycles() - before).toBeGreaterThan(1_900_000);
|
||||
// ...yet almost none of those cycles were actually executed (the win).
|
||||
expect(instructionsExecuted).toBeLessThan(5_000);
|
||||
});
|
||||
|
||||
it('does NOT elide when GPIO keeps changing (bit-bang safety)', () => {
|
||||
withProgram([NOP, B_BACK_1]);
|
||||
// Same loop, but make every GPIO snapshot differ — mimics a pin toggling
|
||||
// each iteration. The detector must refuse to skip and grind every cycle.
|
||||
let n = 0;
|
||||
Object.defineProperty(rp, 'gpioValues', { configurable: true, get: () => n++ });
|
||||
|
||||
const { cyclesAdvanced, instructionsExecuted } = sim.runFrameForTime(1); // 1 ms
|
||||
// No skipping: instructions executed are on the order of cycles advanced.
|
||||
expect(instructionsExecuted).toBeGreaterThan(cyclesAdvanced / 4);
|
||||
});
|
||||
|
||||
it('locks the cycle budget to the measured wall-clock delta', () => {
|
||||
withProgram([NOP, B_BACK_1]);
|
||||
const small = sim.runFrameForTime(4).cyclesAdvanced;
|
||||
const big = sim.runFrameForTime(16).cyclesAdvanced;
|
||||
// 4x the wall-time => ~4x the simulated cycles (within scheduling slack).
|
||||
expect(big).toBeGreaterThan(small * 3);
|
||||
// And the long-delta clamp keeps a backgrounded tab from over-running.
|
||||
const clamped = sim.runFrameForTime(100_000).cyclesAdvanced; // 100 s wall-clock
|
||||
expect(clamped).toBeLessThan(50 /*MAX_DELTA_MS*/ * 125_000 + 200_000);
|
||||
});
|
||||
|
||||
it('still fires scheduled pin changes during an elided frame (not skipped past)', () => {
|
||||
withProgram([NOP, B_BACK_1]);
|
||||
const setPin = vi.spyOn(sim, 'setPinState');
|
||||
const at = sim.getCurrentCycles() + 1000;
|
||||
sim.schedulePinChange(2, true, at); // external edge 1000 cycles into the future
|
||||
sim.runFrameForTime(16);
|
||||
// advanceClock caps each jump at the next scheduled change, so it is
|
||||
// applied on time rather than swallowed by the idle skip.
|
||||
expect(setPin).toHaveBeenCalledWith(2, true);
|
||||
});
|
||||
});
|
||||
|
|
@ -37,7 +37,110 @@ import {
|
|||
const F_CPU = 125_000_000; // 125 MHz
|
||||
const CYCLE_NANOS = 1e9 / F_CPU; // nanoseconds per cycle (~8 ns)
|
||||
const FPS = 60;
|
||||
const CYCLES_PER_FRAME = Math.floor(F_CPU / FPS); // ~2 083 333
|
||||
const CYCLES_PER_MS = F_CPU / 1000; // 125 000 cycles per simulated millisecond
|
||||
|
||||
/** Minimal structural view of the rp2040js clock we drive. */
|
||||
interface SimClock {
|
||||
readonly nanosToNextAlarm: number;
|
||||
tick(nanos: number): void;
|
||||
}
|
||||
|
||||
// Real-time scheduler. The RP2040 core is ~8x heavier to emulate than the
|
||||
// AVR (125 MHz vs 16 MHz), so a host that cannot execute 125 M instructions
|
||||
// per second of wall-clock would otherwise run the simulation in slow motion:
|
||||
// a `delay(1000)` blink renders every 4-5 s. Two mechanisms keep sim-time
|
||||
// locked to wall-time:
|
||||
// 1. The frame budget is derived from the MEASURED wall-clock delta (like
|
||||
// AVRSimulator), not a fixed 1/60 s, so the sim never silently falls
|
||||
// behind the assumed 60 fps.
|
||||
// 2. A `delay()` busy-wait spins reading the timer without putting the core
|
||||
// to sleep (no WFI), so the WFI fast-path never triggers and the emulator
|
||||
// grinds every idle cycle. IdleSpinDetector recognises such a
|
||||
// side-effect-free spin and we advance the clock over it instead of
|
||||
// executing it — exactly what the WFI path already does for sleep().
|
||||
const MAX_DELTA_MS = 50; // clamp the wall-clock delta (paused/backgrounded tab)
|
||||
// When an idle spin is elided with no timer alarm to anchor the jump, advance
|
||||
// at most this many cycles before letting the firmware re-check its deadline.
|
||||
// Bounds the delay overshoot to ~1 ms; with an alarm pending we stop exactly
|
||||
// at the alarm (no overshoot).
|
||||
const IDLE_SLICE_CYCLES = CYCLES_PER_MS; // 1 ms
|
||||
|
||||
/**
|
||||
* Detects a side-effect-free busy-wait spin (e.g. arduino-pico `delay()`,
|
||||
* which polls the timer in a tight loop instead of sleeping). Fed the PC
|
||||
* about to execute on every instruction; reads the GPIO snapshot lazily, only
|
||||
* when a backward branch closes a loop iteration, so the hot path stays cheap.
|
||||
*
|
||||
* Reports a spin only once the SAME loop has iterated `threshold` times with
|
||||
* NO GPIO change (input or output) — so a bit-bang loop (toggles a pin every
|
||||
* iteration) and an input-poll that just saw its pin move are never elided,
|
||||
* and neither is a loop that calls out (long forward jump resets the count).
|
||||
* A false positive is bounded-harmless: we only ever advance time up to the
|
||||
* wall-clock budget, never past the next timer alarm or scheduled pin change.
|
||||
*/
|
||||
export class IdleSpinDetector {
|
||||
private prevPc = -1;
|
||||
private loopTarget = -1;
|
||||
private iters = 0;
|
||||
private gpioAtLastIter = -1;
|
||||
|
||||
constructor(
|
||||
private readonly threshold = 32,
|
||||
private readonly maxStride = 256,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param pc program counter about to execute
|
||||
* @param gpio thunk returning the current GPIO snapshot (called only on a
|
||||
* backward branch, so the 30-pin scan stays off the hot path)
|
||||
* @returns true when a stable, side-effect-free spin is detected
|
||||
*/
|
||||
observe(pc: number, gpio: () => number): boolean {
|
||||
const prev = this.prevPc;
|
||||
this.prevPc = pc;
|
||||
if (prev === -1) return false;
|
||||
|
||||
if (pc < prev) {
|
||||
// Backward branch — one loop iteration just closed.
|
||||
const g = gpio();
|
||||
if (this.loopTarget !== pc) {
|
||||
// First time we land on this loop top (or the loop moved): start over.
|
||||
this.loopTarget = pc;
|
||||
this.gpioAtLastIter = g;
|
||||
this.iters = 1;
|
||||
return false;
|
||||
}
|
||||
if (g !== this.gpioAtLastIter) {
|
||||
// A pin changed during the iteration — real work (bit-bang) or an
|
||||
// input arrived. Not idle; restart the count from this iteration.
|
||||
this.gpioAtLastIter = g;
|
||||
this.iters = 1;
|
||||
return false;
|
||||
}
|
||||
this.iters++;
|
||||
return this.iters >= this.threshold;
|
||||
}
|
||||
|
||||
if (pc > prev + this.maxStride) {
|
||||
// Long forward jump (call / loop exit) — left the tight spin.
|
||||
this.reset();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Call right after eliding a slice so the firmware re-checks its deadline
|
||||
* (executes the loop body again) before the next jump. */
|
||||
noteElided(): void {
|
||||
this.iters = 0;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.prevPc = -1;
|
||||
this.loopTarget = -1;
|
||||
this.iters = 0;
|
||||
this.gpioAtLastIter = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible alias for the unified `I2CDevice` shape used by
|
||||
|
|
@ -60,6 +163,9 @@ export class RP2040Simulator {
|
|||
private pioStepAccum = 0;
|
||||
private usbCDC: USBCDC | null = null;
|
||||
private micropythonMode = false;
|
||||
// Real-time scheduler state (see IdleSpinDetector + runFrameForTime).
|
||||
private lastTimestamp = 0;
|
||||
private readonly idleDetector = new IdleSpinDetector();
|
||||
|
||||
// ── Pico W WiFi (CYW43439) — only attached when boardKind === 'pi-pico-w'.
|
||||
private cyw43: Cyw43Emulator | null = null;
|
||||
|
|
@ -626,67 +732,22 @@ export class RP2040Simulator {
|
|||
}
|
||||
|
||||
this.running = true;
|
||||
this.lastTimestamp = 0;
|
||||
this.idleDetector.reset();
|
||||
console.log('[RP2040] Starting simulation at 125 MHz...');
|
||||
|
||||
const execute = () => {
|
||||
const execute = (timestamp: number) => {
|
||||
if (!this.running || !this.rp2040) return;
|
||||
|
||||
const cyclesTarget = Math.floor(CYCLES_PER_FRAME * this.speed);
|
||||
const { core } = this.rp2040;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const clock = (this.rp2040 as any).clock;
|
||||
// Derive this frame's cycle budget from the MEASURED wall-clock delta
|
||||
// (mirrors AVRSimulator) so the sim cannot silently run in slow motion
|
||||
// by assuming a perfect 60 fps. First frame falls back to one frame; the
|
||||
// upper clamp (paused/backgrounded tab) is applied in runFrameForTime.
|
||||
const deltaMs = this.lastTimestamp === 0 ? 1000 / FPS : timestamp - this.lastTimestamp;
|
||||
this.lastTimestamp = timestamp;
|
||||
|
||||
try {
|
||||
let cyclesDone = 0;
|
||||
const pioDiv = this.getPIOClockDiv();
|
||||
while (cyclesDone < cyclesTarget) {
|
||||
if (core.waiting) {
|
||||
if (clock) {
|
||||
const jump: number = clock.nanosToNextAlarm;
|
||||
if (jump <= 0) {
|
||||
// No clock alarms — step PIO so it can unblock the CPU
|
||||
// (e.g. PIO consuming FIFO data may generate an interrupt)
|
||||
this.stepPIO();
|
||||
break;
|
||||
}
|
||||
const jumped = Math.ceil(jump / CYCLE_NANOS);
|
||||
const pioSteps = Math.floor(jumped / pioDiv);
|
||||
// Advance clock incrementally per PIO step so GPIO transitions
|
||||
// get accurate timestamps (not all lumped at the end of the jump).
|
||||
const nanoPerPioStep = pioDiv * CYCLE_NANOS;
|
||||
const maxSteps = Math.min(pioSteps, 50000);
|
||||
let nanosStepped = 0;
|
||||
for (let i = 0; i < maxSteps; i++) {
|
||||
clock.tick(nanoPerPioStep);
|
||||
nanosStepped += nanoPerPioStep;
|
||||
this.totalCycles += pioDiv;
|
||||
this.stepPIO();
|
||||
}
|
||||
// Tick any remaining nanoseconds not covered by PIO steps
|
||||
const remaining = jump - nanosStepped;
|
||||
if (remaining > 0) {
|
||||
clock.tick(remaining);
|
||||
this.totalCycles += Math.ceil(remaining / CYCLE_NANOS);
|
||||
}
|
||||
cyclesDone += jumped;
|
||||
this.flushScheduledPinChanges();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const cycles: number = core.executeInstruction();
|
||||
if (clock) clock.tick(cycles * CYCLE_NANOS);
|
||||
cyclesDone += cycles;
|
||||
this.totalCycles += cycles;
|
||||
// Step PIO synchronously at the PIO clock rate
|
||||
this.pioStepAccum += cycles;
|
||||
while (this.pioStepAccum >= pioDiv) {
|
||||
this.pioStepAccum -= pioDiv;
|
||||
this.stepPIO();
|
||||
}
|
||||
this.flushScheduledPinChanges();
|
||||
}
|
||||
}
|
||||
this.runFrameForTime(deltaMs);
|
||||
} catch (error) {
|
||||
console.error('[RP2040] Simulation error:', error);
|
||||
this.stop();
|
||||
|
|
@ -699,6 +760,117 @@ export class RP2040Simulator {
|
|||
this.animationFrame = requestAnimationFrame(execute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one frame's worth of simulation for `deltaMs` of wall-clock time.
|
||||
* Returns counters for tests. Keeps simulated time locked to wall-clock:
|
||||
* idle spins (busy-wait `delay()`) and WFI sleeps advance the clock instead
|
||||
* of executing every idle cycle, so timing stays correct even when the host
|
||||
* cannot emulate 125 MHz in real time. Exposed (not private) so the
|
||||
* real-time scheduler can be driven deterministically in tests without rAF.
|
||||
*/
|
||||
runFrameForTime(deltaMs: number): { cyclesAdvanced: number; instructionsExecuted: number } {
|
||||
if (!this.rp2040) return { cyclesAdvanced: 0, instructionsExecuted: 0 };
|
||||
// Guard against NaN/negative deltas and clamp the upper bound so a single
|
||||
// frame never simulates more than MAX_DELTA_MS of CPU time (a paused or
|
||||
// backgrounded tab must not trigger a multi-second catch-up burst).
|
||||
let dt = deltaMs > 0 ? deltaMs : 1000 / FPS;
|
||||
if (dt > MAX_DELTA_MS) dt = MAX_DELTA_MS;
|
||||
const cyclesTarget = Math.max(1, Math.floor(CYCLES_PER_MS * dt * this.speed));
|
||||
const { core } = this.rp2040;
|
||||
const clock = (this.rp2040 as unknown as { clock?: SimClock }).clock ?? null;
|
||||
const pioDiv = this.getPIOClockDiv();
|
||||
const gpioSnapshot = () => this.rp2040!.gpioValues;
|
||||
|
||||
let cyclesDone = 0;
|
||||
let instructionsExecuted = 0;
|
||||
while (cyclesDone < cyclesTarget) {
|
||||
if (core.waiting) {
|
||||
// CPU asleep (WFI/WFE): jump to the next timer alarm, but never past
|
||||
// this frame's wall-clock budget, so a long sleep advances at real
|
||||
// time across frames rather than leaping ahead.
|
||||
if (!clock || clock.nanosToNextAlarm <= 0) {
|
||||
this.stepPIO(); // nothing scheduled to wake it this frame
|
||||
break;
|
||||
}
|
||||
const jumped = this.advanceClock(cyclesTarget - cyclesDone, pioDiv, clock);
|
||||
if (jumped <= 0) break;
|
||||
cyclesDone += jumped;
|
||||
} else if (this.idleDetector.observe(core.PC, gpioSnapshot)) {
|
||||
// Detected a side-effect-free busy-wait spin (e.g. delay()): advance
|
||||
// the clock over it instead of grinding every cycle. Capped at the
|
||||
// next alarm/scheduled pin change inside advanceClock, and to a small
|
||||
// slice so the firmware re-checks its deadline (bounds overshoot).
|
||||
const budget = Math.min(cyclesTarget - cyclesDone, IDLE_SLICE_CYCLES);
|
||||
const jumped = this.advanceClock(budget, pioDiv, clock);
|
||||
if (jumped <= 0) {
|
||||
cyclesDone += this.execOne(core, clock, pioDiv);
|
||||
instructionsExecuted++;
|
||||
} else {
|
||||
cyclesDone += jumped;
|
||||
this.idleDetector.noteElided();
|
||||
}
|
||||
} else {
|
||||
cyclesDone += this.execOne(core, clock, pioDiv);
|
||||
instructionsExecuted++;
|
||||
}
|
||||
}
|
||||
return { cyclesAdvanced: cyclesDone, instructionsExecuted };
|
||||
}
|
||||
|
||||
/** Execute one ARM instruction in the production loop, advancing the clock
|
||||
* and stepping PIO. Returns the cycles it took. */
|
||||
private execOne(
|
||||
core: { executeInstruction(): number },
|
||||
clock: SimClock | null,
|
||||
pioDiv: number,
|
||||
): number {
|
||||
const cycles: number = core.executeInstruction();
|
||||
if (clock) clock.tick(cycles * CYCLE_NANOS);
|
||||
this.totalCycles += cycles;
|
||||
this.pioStepAccum += cycles;
|
||||
while (this.pioStepAccum >= pioDiv) {
|
||||
this.pioStepAccum -= pioDiv;
|
||||
this.stepPIO();
|
||||
}
|
||||
this.flushScheduledPinChanges();
|
||||
return cycles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the simulated clock by up to `budgetCycles` WITHOUT executing
|
||||
* instructions, stepping PIO at the PIO clock rate so GPIO timestamps stay
|
||||
* accurate. Never advances past the next timer alarm or the next scheduled
|
||||
* pin change (so those still fire at their exact simulated time). Returns
|
||||
* the number of cycles actually advanced.
|
||||
*/
|
||||
private advanceClock(budgetCycles: number, pioDiv: number, clock: SimClock | null): number {
|
||||
if (budgetCycles <= 0 || !clock) return 0;
|
||||
const alarmNanos: number = clock.nanosToNextAlarm ?? 0;
|
||||
const alarmCycles = alarmNanos > 0 ? Math.ceil(alarmNanos / CYCLE_NANOS) : Infinity;
|
||||
const nextPin =
|
||||
this.scheduledPinChanges.length > 0
|
||||
? this.scheduledPinChanges[0].cycle - this.totalCycles
|
||||
: Infinity;
|
||||
let jumped = Math.min(budgetCycles, alarmCycles, nextPin > 0 ? nextPin : Infinity);
|
||||
if (!Number.isFinite(jumped) || jumped <= 0) return 0;
|
||||
jumped = Math.ceil(jumped);
|
||||
|
||||
const totalNanos = jumped * CYCLE_NANOS;
|
||||
const nanoPerPioStep = pioDiv * CYCLE_NANOS;
|
||||
const pioSteps = Math.min(Math.floor(jumped / pioDiv), 50000);
|
||||
let nanosStepped = 0;
|
||||
for (let i = 0; i < pioSteps; i++) {
|
||||
clock.tick(nanoPerPioStep);
|
||||
nanosStepped += nanoPerPioStep;
|
||||
this.stepPIO();
|
||||
}
|
||||
const remaining = totalNanos - nanosStepped;
|
||||
if (remaining > 0) clock.tick(remaining);
|
||||
this.totalCycles += jumped;
|
||||
this.flushScheduledPinChanges();
|
||||
return jumped;
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (!this.running) return;
|
||||
this.running = false;
|
||||
|
|
@ -710,6 +882,8 @@ export class RP2040Simulator {
|
|||
// typically cleared on stop/start, so the previous run's "seeded"
|
||||
// flag would suppress the baseline sample for the next session.
|
||||
this.uartTxSeeded = [false, false];
|
||||
this.lastTimestamp = 0;
|
||||
this.idleDetector.reset();
|
||||
console.log('[RP2040] Simulation stopped');
|
||||
}
|
||||
|
||||
|
|
@ -717,6 +891,7 @@ export class RP2040Simulator {
|
|||
this.stop();
|
||||
this.totalCycles = 0;
|
||||
this.scheduledPinChanges = [];
|
||||
this.idleDetector.reset();
|
||||
if (this.rp2040 && this.flashCopy) {
|
||||
if (this.micropythonMode) {
|
||||
// In MicroPython mode, restore the full flash snapshot (UF2 + LittleFS)
|
||||
|
|
|
|||
Loading…
Reference in New Issue