Skip to content

Commit 125e741

Browse files
committed
Keep SPI parts working after a simulator restart
AVRSimulator.reset() rebuilds the CPU and every peripheral, which meant a brand new AVRSPI with its own MISO loopback handler. Parts hook simulator.spi.onByte once from attachEvents, and DynamicComponent only re-runs attachEvents when the simulator instance, the hex, or the wiring changes — a restart changes none of them. So the part kept talking to a dead peripheral while the MCU talked to a loopback. Stop calls reset(), so does the Reset button. Every SPI part was one-run-only on AVR boards: RC522, ILI9341, SD card, e-paper, MAX7219. With an RC522 that looks like the reader working on the first Run and then ignoring the card forever, no matter how many times you tap it. .spi is now a persistent facade and the real peripheral lives in spiPeripheral. Parts hook the facade and keep it; loadHex/reset swap the peripheral underneath and re-route its per-byte callback back through the facade. Same approach as i2cBus, which survives a reset via attachMaster() — that is why I2C displays never had this problem. Also fixes the RP2040 MicroPython reset path, which re-wired SPI0 to plain loopback instead of going through _spiAdapter. Identical bug on the Reset button there; the Arduino path was already adapter-aware. Checked with real MFRC522 firmware driving the real rc522-rfid emulation across Run, Stop, Run, Reset, Run: the UID now reads on every run. Before the change the sketch booted fine each time but only ever saw the card on the first run.
1 parent 25927e7 commit 125e741

2 files changed

Lines changed: 59 additions & 10 deletions

File tree

src/lib/velxio/simulation/AVRSimulator.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,39 @@ export class AVRSimulator {
301301
private megaPorts: Map<string, AVRIOPort> = new Map();
302302
private megaPortValues: Map<string, number> = new Map();
303303
private adc: AVRADC | null = null;
304-
public spi: AVRSPI | null = null;
304+
/**
305+
* The real avr8js SPI peripheral. Rebuilt from scratch on every loadHex /
306+
* reset() alongside the CPU, so nothing outside this class may hold on to
307+
* it — hook `this.spi` instead.
308+
*/
309+
public spiPeripheral: AVRSPI | null = null;
310+
/**
311+
* Persistent SPI bus facade (SpiBusLike). Parts hook `simulator.spi.onByte`
312+
* once from attachEvents and keep that hook for the lifetime of the part;
313+
* loadHex/reset swap the peripheral underneath without disturbing it. Same
314+
* trick as `i2cBus`, which survives a reset via attachMaster(), and as
315+
* RP2040Simulator's `_spiAdapter`.
316+
*
317+
* Before this facade existed, `this.spi` WAS the peripheral: Stop (which
318+
* calls reset()) built a fresh AVRSPI with its own loopback handler and
319+
* dropped the part's handler on the floor. Every SPI part — RC522, ILI9341,
320+
* SD card, e-paper, MAX7219 — then went silent for the rest of the session,
321+
* because DynamicComponent only re-runs attachEvents when the simulator
322+
* instance, the hex, or the wiring changes, and a restart changes none of
323+
* them. Symptom: works on the first Run, dead on every Run after.
324+
*
325+
* Defaults to the same MISO loopback the peripheral used to install, so
326+
* parts that chain a previous handler (and EPaperPart's
327+
* `typeof spi.onByte === "function"` probe) see what they did before.
328+
*/
329+
public readonly spi: {
330+
onByte: ((mosi: number) => void) | null;
331+
completeTransfer: (miso: number) => void;
332+
} = {
333+
onByte: (value: number) => this.spiPeripheral?.completeTransfer(value),
334+
completeTransfer: (miso: number) =>
335+
this.spiPeripheral?.completeTransfer(miso),
336+
};
305337
public usart: AVRUSART | null = null;
306338
public twi: AVRTWI | null = null;
307339
public i2cBus!: I2CBusManager;
@@ -481,10 +513,15 @@ export class AVRSimulator {
481513
? { ...twiConfig, twiInterrupt: 0x4e }
482514
: twiConfig;
483515

484-
this.spi = new AVRSPI(this.cpu, activeSpiConfig, 16000000);
485-
this.spi.onByte = (value) => {
486-
this.spi!.completeTransfer(value);
516+
const spiPeripheral = new AVRSPI(this.cpu, activeSpiConfig, 16000000);
517+
// Route every transmitted byte through the persistent facade, so a part
518+
// that hooked `.spi.onByte` before this (re)build keeps receiving bytes.
519+
spiPeripheral.onByte = (value) => {
520+
const handler = this.spi.onByte;
521+
if (handler) handler(value);
522+
else spiPeripheral.completeTransfer(value);
487523
};
524+
this.spiPeripheral = spiPeripheral;
488525

489526
this.usart = new AVRUSART(this.cpu, activeUsart0Config, 16000000);
490527
this.usart.onByteTransmit = (value: number) => {
@@ -512,7 +549,7 @@ export class AVRSimulator {
512549
new AVRTimer(this.cpu, activeTimer1Config),
513550
new AVRTimer(this.cpu, activeTimer2Config),
514551
this.usart,
515-
this.spi,
552+
this.spiPeripheral,
516553
this.twi,
517554
];
518555

@@ -1017,10 +1054,15 @@ export class AVRSimulator {
10171054
];
10181055
this.usart = null;
10191056
} else {
1020-
this.spi = new AVRSPI(this.cpu, spiConfig, 16000000);
1021-
this.spi.onByte = (value) => {
1022-
this.spi!.completeTransfer(value);
1057+
// Same facade routing as loadHex — the parts' `.spi.onByte` hooks
1058+
// must survive a reset (Stop/Run and the Reset button both land here).
1059+
const spiPeripheral = new AVRSPI(this.cpu, spiConfig, 16000000);
1060+
spiPeripheral.onByte = (value) => {
1061+
const handler = this.spi.onByte;
1062+
if (handler) handler(value);
1063+
else spiPeripheral.completeTransfer(value);
10231064
};
1065+
this.spiPeripheral = spiPeripheral;
10241066

10251067
this.usart = new AVRUSART(this.cpu, usart0Config, 16000000);
10261068
this.usart.onByteTransmit = (value: number) => {
@@ -1042,7 +1084,7 @@ export class AVRSimulator {
10421084
new AVRTimer(this.cpu, timer1Config),
10431085
new AVRTimer(this.cpu, timer2Config),
10441086
this.usart,
1045-
this.spi,
1087+
this.spiPeripheral,
10461088
this.twi,
10471089
];
10481090
this.adc = new AVRADC(this.cpu, adcConfig);

src/lib/velxio/simulation/RP2040Simulator.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,8 +784,15 @@ export class RP2040Simulator {
784784
};
785785
this.wireI2C(0);
786786
this.wireI2C(1);
787+
// Adapter-aware, like initMCU / initMicroPython: a part that hooked
788+
// `.spi.onByte` before this reset must keep receiving bytes, otherwise
789+
// SPI displays go black after the first Reset press.
787790
this.rp2040.spi[0].onTransmit = (v: number) => {
788-
this.rp2040!.spi[0].completeTransmit(v);
791+
if (this._spiAdapter?.onByte) {
792+
this._spiAdapter.onByte(v);
793+
} else {
794+
this.rp2040!.spi[0].completeTransmit(v);
795+
}
789796
};
790797
this.rp2040.spi[1].onTransmit = (v: number) => {
791798
this.rp2040!.spi[1].completeTransmit(v);

0 commit comments

Comments
 (0)