2026-04-29 05:24:39 +07:00
|
|
|
/**
|
|
|
|
|
* Per-simulator bridge state for custom chips.
|
|
|
|
|
*
|
|
|
|
|
* Each simulator family exposes its peripherals differently:
|
|
|
|
|
* - AVR (avr8js) — `simulator.usart` / `simulator.spi` / `simulator.i2cBus`
|
|
|
|
|
* - RP2040 (rp2040js) — `simulator.serialWriteByte` / `simulator.setSPIHandler` /
|
|
|
|
|
* `simulator.addI2CDevice` (per-bus indexing)
|
|
|
|
|
* - ESP32 (QEMU shim) — `simulator.sendPinEvent` (no I2C/SPI/UART today)
|
|
|
|
|
*
|
|
|
|
|
* The bridges in this module install a single dispatcher per simulator that
|
|
|
|
|
* fans out to every chip subscribed, regardless of family.
|
|
|
|
|
*/
|
|
|
|
|
import { SPIBus } from './SPIBus';
|
|
|
|
|
|
|
|
|
|
export type SimulatorKind = 'avr' | 'rp2040' | 'esp32' | 'unknown';
|
|
|
|
|
|
|
|
|
|
export function detectSimulatorKind(simulator: any): SimulatorKind {
|
|
|
|
|
if (!simulator) return 'unknown';
|
|
|
|
|
if (simulator.usart && simulator.spi && simulator.i2cBus) return 'avr';
|
|
|
|
|
if (typeof simulator.addI2CDevice === 'function' && typeof simulator.setSPIHandler === 'function') {
|
|
|
|
|
return 'rp2040';
|
|
|
|
|
}
|
|
|
|
|
if (typeof simulator.sendPinEvent === 'function') return 'esp32';
|
|
|
|
|
return 'unknown';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SimulatorBridges {
|
|
|
|
|
/** Set of UART RX listeners (one per UART chip). */
|
|
|
|
|
uartListeners: Set<(byte: number) => void>;
|
|
|
|
|
/** Whether the UART dispatcher has already been wired to the simulator. */
|
|
|
|
|
uartInstalled: boolean;
|
|
|
|
|
/** Original onByteTransmit so non-chip listeners still receive bytes. */
|
|
|
|
|
uartPreviousOnByteTransmit: ((byte: number) => void) | null;
|
feat(attiny85+customchip): full ATtiny85 ADC/Timer0 + custom-chip pipeline fixes
ATtiny85 (AVRSimulator + collectPinStates + connectAnalogInputsToMcu + SimulatorCanvas + Attiny85Element + examples):
- Add attiny85AdcConfig with correct register addresses (ADMUX=0x27,
ADCSRA=0x26, ADCSRB=0x23, ADCL=0x24, ADCH=0x25, DIDR0=0x34, adcInterrupt=0x08).
Without this, analogRead() polled the wrong address forever and the
firmware hung on first ADC read.
- Add attiny85Timer0Config + instantiate AVRTimer so OVF fires at the
ATTinyCore-expected ~1.024 ms cadence. delay() advance is still blocked
on avr8js TIFR auto-clear semantics (separate upstream issue, see
ATTINY85_TIMER0_UPSTREAM_ISSUE.md in velxio-prod test plan).
- Map ATtiny85 ADC channels to PB-style pin names (PB5/PB2/PB4/PB3 -> 0..3)
in connectAnalogInputsToMcu so SPICE node voltages reach the right ADC
channel.
- Recognise /^PB\d+$/ in collectPinStates.pinNameToArduinoPin so wires
named "PB1" emit v_attiny85_pb1 V-source and the LED responds to MCU
writes. Previously every PB-wire returned -1 and SPICE saw no source.
- SimulatorCanvas: subscribe pin 1 (PB1) for the built-in LED on the
attiny85 board kind (Digispark convention), instead of falling through
to the pin-13 default.
- Attiny85Element: remove the hand-drawn "yellow LED" circle that was
floating above the chip. The bare DIP-8 has no on-board LED; examples
wire a real wokwi-led + resistor instead.
- examples.ts: add a real wokwi-led + 220 Ohm wokwi-resistor + wires to
attiny85-blink, and add missing series resistors to attiny85-button-led
and attiny85-ntc-sensor. attiny85-pwm-fade was already correct.
Custom-chip pipeline (CustomChipPart + simulatorBridges):
- Add a requestAnimationFrame loop that calls instance.tickTimers() every
frame in CustomChipPart. Chips that register vx_timer_create (e.g. an
i8080 stepping its core, or a sensor publishing samples) had timers
added to the queue but nothing fired them; tickTimers was dead code.
- Gate the ESP32 backend path with detectSimulatorKind(sim)==='esp32'.
The previous `typeof sim.registerSensor === 'function'` check matched
AVR and RP2040 simulators too (they expose registerSensor for I2C
sensor proxies), routing client-side chips to a non-existent ESP32
worker on those boards.
- Replace direct simulator.usart.writeByte calls in avrUartTx with a
JS-level FIFO + setTimeout(1ms) drainer. avr8js writeByte drops bytes
under burst load (a chip emitting print_string lost ~99% of bytes via
non-immediate, or kept only the last byte via immediate). The drainer
attempts one non-immediate write per tick and retries on RXC busy /
RXEN off. Added a guard for ATtiny85 (no USART -> would queue forever).
End-to-end verified: i8080-banner-streamer now prints the boot banner
followed by "uptime ticks: 0xNN" lines stepping every ~50 ms, executing
real Intel 8080 instructions inside the WASM chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:10:08 +07:00
|
|
|
/** Pending bytes to inject into the AVR/RP2040 RX register, drained at
|
|
|
|
|
* ~baud rate by `uartDrainHandle`. Without this queue, chips that emit
|
|
|
|
|
* bursts (e.g. an i8080 printing a banner) overflow the 2-byte USART
|
|
|
|
|
* RX register and most bytes get silently dropped. */
|
|
|
|
|
uartRxQueue: number[];
|
|
|
|
|
/** setTimeout handle for the queue drainer (0 if not active). */
|
|
|
|
|
uartDrainHandle: number;
|
2026-04-29 05:24:39 +07:00
|
|
|
|
|
|
|
|
/** Shared SPI bus across all custom chips on this simulator. */
|
|
|
|
|
spiBus: SPIBus;
|
|
|
|
|
/** Whether the SPI dispatcher has already been wired. */
|
|
|
|
|
spiInstalled: boolean;
|
|
|
|
|
/** Original SPI byte handler (preserved for restore). */
|
|
|
|
|
spiPreviousOnByte: ((byte: number) => void) | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SIM_BRIDGES = new WeakMap<object, SimulatorBridges>();
|
|
|
|
|
|
|
|
|
|
export function getSimulatorBridges(simulator: any): SimulatorBridges {
|
|
|
|
|
let b = SIM_BRIDGES.get(simulator);
|
|
|
|
|
if (!b) {
|
|
|
|
|
b = {
|
|
|
|
|
uartListeners: new Set(),
|
|
|
|
|
uartInstalled: false,
|
|
|
|
|
uartPreviousOnByteTransmit: null,
|
feat(attiny85+customchip): full ATtiny85 ADC/Timer0 + custom-chip pipeline fixes
ATtiny85 (AVRSimulator + collectPinStates + connectAnalogInputsToMcu + SimulatorCanvas + Attiny85Element + examples):
- Add attiny85AdcConfig with correct register addresses (ADMUX=0x27,
ADCSRA=0x26, ADCSRB=0x23, ADCL=0x24, ADCH=0x25, DIDR0=0x34, adcInterrupt=0x08).
Without this, analogRead() polled the wrong address forever and the
firmware hung on first ADC read.
- Add attiny85Timer0Config + instantiate AVRTimer so OVF fires at the
ATTinyCore-expected ~1.024 ms cadence. delay() advance is still blocked
on avr8js TIFR auto-clear semantics (separate upstream issue, see
ATTINY85_TIMER0_UPSTREAM_ISSUE.md in velxio-prod test plan).
- Map ATtiny85 ADC channels to PB-style pin names (PB5/PB2/PB4/PB3 -> 0..3)
in connectAnalogInputsToMcu so SPICE node voltages reach the right ADC
channel.
- Recognise /^PB\d+$/ in collectPinStates.pinNameToArduinoPin so wires
named "PB1" emit v_attiny85_pb1 V-source and the LED responds to MCU
writes. Previously every PB-wire returned -1 and SPICE saw no source.
- SimulatorCanvas: subscribe pin 1 (PB1) for the built-in LED on the
attiny85 board kind (Digispark convention), instead of falling through
to the pin-13 default.
- Attiny85Element: remove the hand-drawn "yellow LED" circle that was
floating above the chip. The bare DIP-8 has no on-board LED; examples
wire a real wokwi-led + resistor instead.
- examples.ts: add a real wokwi-led + 220 Ohm wokwi-resistor + wires to
attiny85-blink, and add missing series resistors to attiny85-button-led
and attiny85-ntc-sensor. attiny85-pwm-fade was already correct.
Custom-chip pipeline (CustomChipPart + simulatorBridges):
- Add a requestAnimationFrame loop that calls instance.tickTimers() every
frame in CustomChipPart. Chips that register vx_timer_create (e.g. an
i8080 stepping its core, or a sensor publishing samples) had timers
added to the queue but nothing fired them; tickTimers was dead code.
- Gate the ESP32 backend path with detectSimulatorKind(sim)==='esp32'.
The previous `typeof sim.registerSensor === 'function'` check matched
AVR and RP2040 simulators too (they expose registerSensor for I2C
sensor proxies), routing client-side chips to a non-existent ESP32
worker on those boards.
- Replace direct simulator.usart.writeByte calls in avrUartTx with a
JS-level FIFO + setTimeout(1ms) drainer. avr8js writeByte drops bytes
under burst load (a chip emitting print_string lost ~99% of bytes via
non-immediate, or kept only the last byte via immediate). The drainer
attempts one non-immediate write per tick and retries on RXC busy /
RXEN off. Added a guard for ATtiny85 (no USART -> would queue forever).
End-to-end verified: i8080-banner-streamer now prints the boot banner
followed by "uptime ticks: 0xNN" lines stepping every ~50 ms, executing
real Intel 8080 instructions inside the WASM chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:10:08 +07:00
|
|
|
uartRxQueue: [],
|
|
|
|
|
uartDrainHandle: 0,
|
2026-04-29 05:24:39 +07:00
|
|
|
spiBus: new SPIBus(),
|
|
|
|
|
spiInstalled: false,
|
|
|
|
|
spiPreviousOnByte: null,
|
|
|
|
|
};
|
|
|
|
|
SIM_BRIDGES.set(simulator, b);
|
|
|
|
|
}
|
|
|
|
|
return b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── UART ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Install the UART TX-out dispatcher idempotently. Whatever family the
|
|
|
|
|
* simulator belongs to, the dispatcher fans bytes out to every listener in
|
|
|
|
|
* `uartListeners` (one per UART chip).
|
|
|
|
|
*/
|
|
|
|
|
export function ensureUartBridge(simulator: any): void {
|
|
|
|
|
const b = getSimulatorBridges(simulator);
|
|
|
|
|
if (b.uartInstalled) return;
|
|
|
|
|
const kind = detectSimulatorKind(simulator);
|
|
|
|
|
|
|
|
|
|
if (kind === 'avr' && simulator.usart) {
|
|
|
|
|
b.uartPreviousOnByteTransmit = simulator.usart.onByteTransmit ?? null;
|
|
|
|
|
const previous = b.uartPreviousOnByteTransmit;
|
|
|
|
|
simulator.usart.onByteTransmit = (byte: number) => {
|
|
|
|
|
if (previous) { try { previous(byte); } catch { /* swallow */ } }
|
|
|
|
|
for (const listener of b.uartListeners) {
|
|
|
|
|
try { listener(byte); } catch { /* swallow */ }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
b.uartInstalled = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (kind === 'rp2040') {
|
|
|
|
|
// RP2040 emits each TX byte through `onSerialData(char)` (a string).
|
|
|
|
|
const previous = simulator.onSerialData;
|
|
|
|
|
simulator.onSerialData = (charStr: string) => {
|
|
|
|
|
if (previous) { try { previous(charStr); } catch { /* swallow */ } }
|
|
|
|
|
const code = typeof charStr === 'string' ? charStr.charCodeAt(0) : Number(charStr);
|
|
|
|
|
if (!Number.isFinite(code)) return;
|
|
|
|
|
for (const listener of b.uartListeners) {
|
|
|
|
|
try { listener(code & 0xff); } catch { /* swallow */ }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
b.uartInstalled = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// esp32/unknown: no client-side UART bridge today (QEMU has its own path).
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Inject a byte into the simulator's RX path so the sketch's `Serial.read()`
|
feat(attiny85+customchip): full ATtiny85 ADC/Timer0 + custom-chip pipeline fixes
ATtiny85 (AVRSimulator + collectPinStates + connectAnalogInputsToMcu + SimulatorCanvas + Attiny85Element + examples):
- Add attiny85AdcConfig with correct register addresses (ADMUX=0x27,
ADCSRA=0x26, ADCSRB=0x23, ADCL=0x24, ADCH=0x25, DIDR0=0x34, adcInterrupt=0x08).
Without this, analogRead() polled the wrong address forever and the
firmware hung on first ADC read.
- Add attiny85Timer0Config + instantiate AVRTimer so OVF fires at the
ATTinyCore-expected ~1.024 ms cadence. delay() advance is still blocked
on avr8js TIFR auto-clear semantics (separate upstream issue, see
ATTINY85_TIMER0_UPSTREAM_ISSUE.md in velxio-prod test plan).
- Map ATtiny85 ADC channels to PB-style pin names (PB5/PB2/PB4/PB3 -> 0..3)
in connectAnalogInputsToMcu so SPICE node voltages reach the right ADC
channel.
- Recognise /^PB\d+$/ in collectPinStates.pinNameToArduinoPin so wires
named "PB1" emit v_attiny85_pb1 V-source and the LED responds to MCU
writes. Previously every PB-wire returned -1 and SPICE saw no source.
- SimulatorCanvas: subscribe pin 1 (PB1) for the built-in LED on the
attiny85 board kind (Digispark convention), instead of falling through
to the pin-13 default.
- Attiny85Element: remove the hand-drawn "yellow LED" circle that was
floating above the chip. The bare DIP-8 has no on-board LED; examples
wire a real wokwi-led + resistor instead.
- examples.ts: add a real wokwi-led + 220 Ohm wokwi-resistor + wires to
attiny85-blink, and add missing series resistors to attiny85-button-led
and attiny85-ntc-sensor. attiny85-pwm-fade was already correct.
Custom-chip pipeline (CustomChipPart + simulatorBridges):
- Add a requestAnimationFrame loop that calls instance.tickTimers() every
frame in CustomChipPart. Chips that register vx_timer_create (e.g. an
i8080 stepping its core, or a sensor publishing samples) had timers
added to the queue but nothing fired them; tickTimers was dead code.
- Gate the ESP32 backend path with detectSimulatorKind(sim)==='esp32'.
The previous `typeof sim.registerSensor === 'function'` check matched
AVR and RP2040 simulators too (they expose registerSensor for I2C
sensor proxies), routing client-side chips to a non-existent ESP32
worker on those boards.
- Replace direct simulator.usart.writeByte calls in avrUartTx with a
JS-level FIFO + setTimeout(1ms) drainer. avr8js writeByte drops bytes
under burst load (a chip emitting print_string lost ~99% of bytes via
non-immediate, or kept only the last byte via immediate). The drainer
attempts one non-immediate write per tick and retries on RXC busy /
RXEN off. Added a guard for ATtiny85 (no USART -> would queue forever).
End-to-end verified: i8080-banner-streamer now prints the boot banner
followed by "uptime ticks: 0xNN" lines stepping every ~50 ms, executing
real Intel 8080 instructions inside the WASM chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:10:08 +07:00
|
|
|
* returns it.
|
|
|
|
|
*
|
|
|
|
|
* For AVR we go through a JS-level FIFO + setTimeout drainer instead of
|
|
|
|
|
* calling `simulator.usart.writeByte` directly. Two reasons:
|
|
|
|
|
*
|
|
|
|
|
* 1. The non-immediate form silently returns `false` for bytes that arrive
|
|
|
|
|
* while `rxBusyValue` is still set from the previous one — so chips
|
|
|
|
|
* that emit bursts (e.g. an i8080 print_string sequence) lose ~99% of
|
|
|
|
|
* their bytes.
|
|
|
|
|
* 2. The immediate form overwrites `rxByte` directly without waiting for
|
|
|
|
|
* the AVR sketch to drain it — same outcome, only the last byte of
|
|
|
|
|
* each burst survives.
|
|
|
|
|
*
|
|
|
|
|
* The drainer attempts one non-immediate write per tick (1 ms apart). On
|
|
|
|
|
* RXC busy / RXEN off it leaves the byte at the head of the queue and
|
|
|
|
|
* retries on the next tick. RP2040 has its own internal buffering, so we
|
|
|
|
|
* just forward to `serialWriteByte`.
|
2026-04-29 05:24:39 +07:00
|
|
|
*/
|
|
|
|
|
export function avrUartTx(simulator: any, byte: number): void {
|
|
|
|
|
const kind = detectSimulatorKind(simulator);
|
feat(attiny85+customchip): full ATtiny85 ADC/Timer0 + custom-chip pipeline fixes
ATtiny85 (AVRSimulator + collectPinStates + connectAnalogInputsToMcu + SimulatorCanvas + Attiny85Element + examples):
- Add attiny85AdcConfig with correct register addresses (ADMUX=0x27,
ADCSRA=0x26, ADCSRB=0x23, ADCL=0x24, ADCH=0x25, DIDR0=0x34, adcInterrupt=0x08).
Without this, analogRead() polled the wrong address forever and the
firmware hung on first ADC read.
- Add attiny85Timer0Config + instantiate AVRTimer so OVF fires at the
ATTinyCore-expected ~1.024 ms cadence. delay() advance is still blocked
on avr8js TIFR auto-clear semantics (separate upstream issue, see
ATTINY85_TIMER0_UPSTREAM_ISSUE.md in velxio-prod test plan).
- Map ATtiny85 ADC channels to PB-style pin names (PB5/PB2/PB4/PB3 -> 0..3)
in connectAnalogInputsToMcu so SPICE node voltages reach the right ADC
channel.
- Recognise /^PB\d+$/ in collectPinStates.pinNameToArduinoPin so wires
named "PB1" emit v_attiny85_pb1 V-source and the LED responds to MCU
writes. Previously every PB-wire returned -1 and SPICE saw no source.
- SimulatorCanvas: subscribe pin 1 (PB1) for the built-in LED on the
attiny85 board kind (Digispark convention), instead of falling through
to the pin-13 default.
- Attiny85Element: remove the hand-drawn "yellow LED" circle that was
floating above the chip. The bare DIP-8 has no on-board LED; examples
wire a real wokwi-led + resistor instead.
- examples.ts: add a real wokwi-led + 220 Ohm wokwi-resistor + wires to
attiny85-blink, and add missing series resistors to attiny85-button-led
and attiny85-ntc-sensor. attiny85-pwm-fade was already correct.
Custom-chip pipeline (CustomChipPart + simulatorBridges):
- Add a requestAnimationFrame loop that calls instance.tickTimers() every
frame in CustomChipPart. Chips that register vx_timer_create (e.g. an
i8080 stepping its core, or a sensor publishing samples) had timers
added to the queue but nothing fired them; tickTimers was dead code.
- Gate the ESP32 backend path with detectSimulatorKind(sim)==='esp32'.
The previous `typeof sim.registerSensor === 'function'` check matched
AVR and RP2040 simulators too (they expose registerSensor for I2C
sensor proxies), routing client-side chips to a non-existent ESP32
worker on those boards.
- Replace direct simulator.usart.writeByte calls in avrUartTx with a
JS-level FIFO + setTimeout(1ms) drainer. avr8js writeByte drops bytes
under burst load (a chip emitting print_string lost ~99% of bytes via
non-immediate, or kept only the last byte via immediate). The drainer
attempts one non-immediate write per tick and retries on RXC busy /
RXEN off. Added a guard for ATtiny85 (no USART -> would queue forever).
End-to-end verified: i8080-banner-streamer now prints the boot banner
followed by "uptime ticks: 0xNN" lines stepping every ~50 ms, executing
real Intel 8080 instructions inside the WASM chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:10:08 +07:00
|
|
|
if (kind === 'avr') {
|
|
|
|
|
// ATtiny85 has no hardware USART — silently drop instead of queueing
|
|
|
|
|
// forever. Users wiring a UART chip to a tiny85 need SoftwareSerial,
|
|
|
|
|
// which is a different bridge entirely (TODO).
|
|
|
|
|
if (!simulator.usart || typeof simulator.usart.writeByte !== 'function') return;
|
|
|
|
|
const b = getSimulatorBridges(simulator);
|
|
|
|
|
b.uartRxQueue.push(byte & 0xff);
|
|
|
|
|
if (!b.uartDrainHandle) {
|
|
|
|
|
const drain = () => {
|
|
|
|
|
const b2 = getSimulatorBridges(simulator);
|
|
|
|
|
if (b2.uartRxQueue.length === 0) {
|
|
|
|
|
b2.uartDrainHandle = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const next = b2.uartRxQueue[0];
|
|
|
|
|
let accepted = false;
|
|
|
|
|
try {
|
|
|
|
|
accepted = simulator.usart?.writeByte?.(next) ?? false;
|
|
|
|
|
} catch {
|
|
|
|
|
accepted = false;
|
|
|
|
|
}
|
|
|
|
|
if (accepted) b2.uartRxQueue.shift();
|
|
|
|
|
b2.uartDrainHandle = (setTimeout(drain, 1) as unknown) as number;
|
|
|
|
|
};
|
|
|
|
|
b.uartDrainHandle = (setTimeout(drain, 0) as unknown) as number;
|
2026-04-29 05:24:39 +07:00
|
|
|
}
|
feat(attiny85+customchip): full ATtiny85 ADC/Timer0 + custom-chip pipeline fixes
ATtiny85 (AVRSimulator + collectPinStates + connectAnalogInputsToMcu + SimulatorCanvas + Attiny85Element + examples):
- Add attiny85AdcConfig with correct register addresses (ADMUX=0x27,
ADCSRA=0x26, ADCSRB=0x23, ADCL=0x24, ADCH=0x25, DIDR0=0x34, adcInterrupt=0x08).
Without this, analogRead() polled the wrong address forever and the
firmware hung on first ADC read.
- Add attiny85Timer0Config + instantiate AVRTimer so OVF fires at the
ATTinyCore-expected ~1.024 ms cadence. delay() advance is still blocked
on avr8js TIFR auto-clear semantics (separate upstream issue, see
ATTINY85_TIMER0_UPSTREAM_ISSUE.md in velxio-prod test plan).
- Map ATtiny85 ADC channels to PB-style pin names (PB5/PB2/PB4/PB3 -> 0..3)
in connectAnalogInputsToMcu so SPICE node voltages reach the right ADC
channel.
- Recognise /^PB\d+$/ in collectPinStates.pinNameToArduinoPin so wires
named "PB1" emit v_attiny85_pb1 V-source and the LED responds to MCU
writes. Previously every PB-wire returned -1 and SPICE saw no source.
- SimulatorCanvas: subscribe pin 1 (PB1) for the built-in LED on the
attiny85 board kind (Digispark convention), instead of falling through
to the pin-13 default.
- Attiny85Element: remove the hand-drawn "yellow LED" circle that was
floating above the chip. The bare DIP-8 has no on-board LED; examples
wire a real wokwi-led + resistor instead.
- examples.ts: add a real wokwi-led + 220 Ohm wokwi-resistor + wires to
attiny85-blink, and add missing series resistors to attiny85-button-led
and attiny85-ntc-sensor. attiny85-pwm-fade was already correct.
Custom-chip pipeline (CustomChipPart + simulatorBridges):
- Add a requestAnimationFrame loop that calls instance.tickTimers() every
frame in CustomChipPart. Chips that register vx_timer_create (e.g. an
i8080 stepping its core, or a sensor publishing samples) had timers
added to the queue but nothing fired them; tickTimers was dead code.
- Gate the ESP32 backend path with detectSimulatorKind(sim)==='esp32'.
The previous `typeof sim.registerSensor === 'function'` check matched
AVR and RP2040 simulators too (they expose registerSensor for I2C
sensor proxies), routing client-side chips to a non-existent ESP32
worker on those boards.
- Replace direct simulator.usart.writeByte calls in avrUartTx with a
JS-level FIFO + setTimeout(1ms) drainer. avr8js writeByte drops bytes
under burst load (a chip emitting print_string lost ~99% of bytes via
non-immediate, or kept only the last byte via immediate). The drainer
attempts one non-immediate write per tick and retries on RXC busy /
RXEN off. Added a guard for ATtiny85 (no USART -> would queue forever).
End-to-end verified: i8080-banner-streamer now prints the boot banner
followed by "uptime ticks: 0xNN" lines stepping every ~50 ms, executing
real Intel 8080 instructions inside the WASM chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:10:08 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (kind === 'rp2040' && typeof simulator.serialWriteByte === 'function') {
|
|
|
|
|
try { simulator.serialWriteByte(byte); } catch { /* swallow */ }
|
|
|
|
|
}
|
2026-04-29 05:24:39 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── SPI ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Install the SPI master TX → SPIBus dispatcher idempotently. The chip's
|
|
|
|
|
* SPIDevice (created in `vx_spi_attach`) ends up on `b.spiBus` and is picked
|
|
|
|
|
* up automatically — no per-chip wiring needed beyond `bridges.spiBus`.
|
|
|
|
|
*/
|
|
|
|
|
export function ensureSpiBridge(simulator: any): void {
|
|
|
|
|
const b = getSimulatorBridges(simulator);
|
|
|
|
|
if (b.spiInstalled) return;
|
|
|
|
|
const kind = detectSimulatorKind(simulator);
|
|
|
|
|
|
|
|
|
|
if (kind === 'avr' && simulator.spi) {
|
|
|
|
|
b.spiPreviousOnByte = simulator.spi.onByte ?? null;
|
|
|
|
|
simulator.spi.onByte = (mosi: number) => {
|
|
|
|
|
const miso = b.spiBus.transferByte(mosi);
|
|
|
|
|
simulator.spi.completeTransfer(miso);
|
|
|
|
|
};
|
|
|
|
|
b.spiInstalled = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (kind === 'rp2040' && typeof simulator.setSPIHandler === 'function') {
|
|
|
|
|
// RP2040 has SPI0 and SPI1; we route both through the same bus so
|
|
|
|
|
// CS-gated chips can live on either.
|
|
|
|
|
for (const bus of [0, 1] as const) {
|
|
|
|
|
try {
|
|
|
|
|
simulator.setSPIHandler(bus, (mosi: number) => b.spiBus.transferByte(mosi));
|
|
|
|
|
} catch { /* this bus may not be in use; ignore */ }
|
|
|
|
|
}
|
|
|
|
|
b.spiInstalled = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── I2C adapter ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Pick the right I2C bus object for the chip runtime to call `addDevice`/
|
|
|
|
|
* `removeDevice` on. AVR exposes `simulator.i2cBus` directly; RP2040 needs
|
|
|
|
|
* a tiny adapter to forward to its `addI2CDevice` (per-bus) API.
|
|
|
|
|
*
|
|
|
|
|
* Returns `null` if the simulator doesn't expose any I2C bus (ESP32 today).
|
|
|
|
|
*/
|
|
|
|
|
export function getI2CBus(simulator: any, bus: 0 | 1 = 0): {
|
|
|
|
|
addDevice: (device: any) => void;
|
|
|
|
|
removeDevice: (address: number) => void;
|
|
|
|
|
} | null {
|
|
|
|
|
const kind = detectSimulatorKind(simulator);
|
|
|
|
|
if (kind === 'avr' && simulator.i2cBus) {
|
|
|
|
|
return simulator.i2cBus;
|
|
|
|
|
}
|
|
|
|
|
if (kind === 'rp2040' && typeof simulator.addI2CDevice === 'function') {
|
|
|
|
|
return {
|
|
|
|
|
addDevice: (device) => simulator.addI2CDevice(device, bus),
|
|
|
|
|
removeDevice: (address) => simulator.removeI2CDevice?.(address, bus),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|