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>
This commit is contained in:
parent
a9285c6698
commit
603b791daa
|
|
@ -1100,8 +1100,11 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
case 'nano-rp2040':
|
||||
ledPin = 25; // GPIO25
|
||||
break;
|
||||
case 'attiny85':
|
||||
ledPin = 1; // PB1 (Digispark convention)
|
||||
break;
|
||||
default:
|
||||
ledPin = 13; // Pin 13 for Arduino Uno/Nano/Mega, ATtiny85, etc.
|
||||
ledPin = 13; // Pin 13 for Arduino Uno/Nano/Mega
|
||||
}
|
||||
|
||||
unsubs.push(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* pinInfo) but external components positioned to the right of the
|
||||
* chip may need to be moved manually for clean routing.
|
||||
*
|
||||
* viewBox 28.801 × 23.768 mm scaled uniformly at 5.555 px/mm → 160 × 132 px.
|
||||
* viewBox 28.801 x 23.768 mm scaled uniformly at 5.555 px/mm -> 160 x 132 px.
|
||||
* Pin terminal centres in the source SVG (mm), all 8 connectors:
|
||||
* row x y pin label
|
||||
* top 3.601 1.084 VCC (connector7)
|
||||
|
|
@ -26,8 +26,8 @@
|
|||
* bottom 18.000 22.687 PB4 (connector2)
|
||||
* bottom 25.201 22.687 GND (connector3)
|
||||
*
|
||||
* Built-in LED on PB1 (Digispark convention) is overlaid as a circle in
|
||||
* the outer SVG — drives off the `led1` attribute / property like before.
|
||||
* No fake on-board LED is drawn. The bare DIP-8 has no PCB LED — examples
|
||||
* wire a real wokwi-led + wokwi-resistor to PB1 like the c3-blink one does.
|
||||
*/
|
||||
|
||||
const W = 160;
|
||||
|
|
@ -40,7 +40,7 @@ const PIN_INFO: ReadonlyArray<{ name: string; x: number; y: number; description?
|
|||
// Top row — left to right
|
||||
{ name: 'VCC', x: PIN_X[0], y: TOP_Y, description: 'VCC (Pin 8)' },
|
||||
{ name: 'PB2', x: PIN_X[1], y: TOP_Y, description: 'PB2 / SCK / SCL / ADC1 (Pin 7)' },
|
||||
{ name: 'PB1', x: PIN_X[2], y: TOP_Y, description: 'PB1 / MISO / built-in LED (Pin 6)' },
|
||||
{ name: 'PB1', x: PIN_X[2], y: TOP_Y, description: 'PB1 / MISO / OC0B / OC1A (Pin 6)' },
|
||||
{ name: 'PB0', x: PIN_X[3], y: TOP_Y, description: 'PB0 / MOSI / SDA / OC0A (Pin 5)' },
|
||||
// Bottom row — left to right
|
||||
{ name: 'PB5', x: PIN_X[0], y: BOT_Y, description: 'PB5 / RESET (Pin 1)' },
|
||||
|
|
@ -50,12 +50,6 @@ const PIN_INFO: ReadonlyArray<{ name: string; x: number; y: number; description?
|
|||
];
|
||||
|
||||
class Attiny85Element extends HTMLElement {
|
||||
static get observedAttributes(): string[] {
|
||||
return ['led1'];
|
||||
}
|
||||
|
||||
private _led1 = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
|
@ -65,52 +59,14 @@ class Attiny85Element extends HTMLElement {
|
|||
this.render();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
||||
if (name === 'led1') {
|
||||
this._led1 = value !== null && value !== 'false';
|
||||
this.updateLed();
|
||||
}
|
||||
}
|
||||
|
||||
/** Wires use this to find pin coordinates. */
|
||||
get pinInfo() {
|
||||
return PIN_INFO;
|
||||
}
|
||||
|
||||
set led1(v: boolean) {
|
||||
this._led1 = !!v;
|
||||
this.updateLed();
|
||||
}
|
||||
get led1(): boolean {
|
||||
return this._led1;
|
||||
}
|
||||
|
||||
private updateLed() {
|
||||
const led = this.shadowRoot?.getElementById('attiny-led1') as unknown as SVGElement | null;
|
||||
if (!led) return;
|
||||
if (this._led1) {
|
||||
led.setAttribute('fill', '#ffee44');
|
||||
led.setAttribute('stroke', '#ffcc00');
|
||||
led.style.filter = 'drop-shadow(0 0 4px #ffcc00)';
|
||||
} else {
|
||||
led.setAttribute('fill', '#333');
|
||||
led.setAttribute('stroke', '#555');
|
||||
led.style.filter = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
private render() {
|
||||
if (!this.shadowRoot) return;
|
||||
|
||||
const ledFill = this._led1 ? '#ffee44' : '#333';
|
||||
const ledStroke = this._led1 ? '#ffcc00' : '#555';
|
||||
const ledFilter = this._led1 ? 'drop-shadow(0 0 4px #ffcc00)' : 'none';
|
||||
|
||||
// Place the LED indicator just ABOVE the PB1 top-pin (PIN_X[2], TOP_Y),
|
||||
// outside the chip body so it stays readable.
|
||||
const ledX = PIN_X[2];
|
||||
const ledY = TOP_Y - 12;
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host { display: inline-block; line-height: 0; }
|
||||
|
|
@ -122,23 +78,16 @@ class Attiny85Element extends HTMLElement {
|
|||
pointer-events: none;
|
||||
}
|
||||
.pin-label.power { fill: #aaa; }
|
||||
.pin-label.led { fill: #ffd066; }
|
||||
</style>
|
||||
<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<!-- Fritzing ATtiny85 DIP-8 artwork (CC-BY-SA). Scaled to fill
|
||||
the 160×132 box; aspect ratio matches the source 1.211:1. -->
|
||||
the 160x132 box; aspect ratio matches the source 1.211:1. -->
|
||||
<image href="/component-svgs/attiny85.svg" x="0" y="0" width="${W}" height="${H}" />
|
||||
|
||||
<!-- Built-in LED on PB1 (Digispark convention) -->
|
||||
<circle id="attiny-led1"
|
||||
cx="${ledX}" cy="${ledY}" r="5"
|
||||
fill="${ledFill}" stroke="${ledStroke}" stroke-width="1"
|
||||
style="filter: ${ledFilter};" />
|
||||
|
||||
<!-- Pin labels overlaid above/below the connector pads. -->
|
||||
<text class="pin-label power" x="${PIN_X[0]}" y="${TOP_Y - 22}">VCC</text>
|
||||
<text class="pin-label" x="${PIN_X[1]}" y="${TOP_Y - 22}">PB2</text>
|
||||
<text class="pin-label led" x="${PIN_X[2]}" y="${TOP_Y - 22}">PB1</text>
|
||||
<text class="pin-label" x="${PIN_X[2]}" y="${TOP_Y - 22}">PB1</text>
|
||||
<text class="pin-label" x="${PIN_X[3]}" y="${TOP_Y - 22}">PB0</text>
|
||||
|
||||
<text class="pin-label" x="${PIN_X[0]}" y="${BOT_Y + 12}">PB5</text>
|
||||
|
|
|
|||
|
|
@ -1550,33 +1550,21 @@ void loop() {
|
|||
`,
|
||||
components: [
|
||||
{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} },
|
||||
{ type: 'wokwi-ssd1306', id: 'ssd1306-1', x: 420, y: 120, properties: {} },
|
||||
{ type: 'wokwi-ssd1306', id: 'ssd1306-1', x: 420, y: 100, properties: {} },
|
||||
{ type: 'wokwi-ds1307', id: 'rtc1', x: 420, y: 280, properties: {} },
|
||||
],
|
||||
wires: [
|
||||
{
|
||||
id: 'wire-sda',
|
||||
start: { componentId: 'arduino-uno', pinName: 'A4' },
|
||||
end: { componentId: 'ssd1306-1', pinName: 'DATA' },
|
||||
color: '#2196f3',
|
||||
},
|
||||
{
|
||||
id: 'wire-scl',
|
||||
start: { componentId: 'arduino-uno', pinName: 'A5' },
|
||||
end: { componentId: 'ssd1306-1', pinName: 'CLK' },
|
||||
color: '#ff9800',
|
||||
},
|
||||
{
|
||||
id: 'wire-gnd',
|
||||
start: { componentId: 'arduino-uno', pinName: 'GND.1' },
|
||||
end: { componentId: 'ssd1306-1', pinName: 'GND' },
|
||||
color: '#000000',
|
||||
},
|
||||
{
|
||||
id: 'wire-vcc',
|
||||
start: { componentId: 'arduino-uno', pinName: '5V' },
|
||||
end: { componentId: 'ssd1306-1', pinName: 'VIN' },
|
||||
color: '#ff0000',
|
||||
},
|
||||
// SDA bus (shared)
|
||||
{ id: 'wire-sda', start: { componentId: 'arduino-uno', pinName: 'A4' }, end: { componentId: 'ssd1306-1', pinName: 'DATA' }, color: '#2196f3' },
|
||||
{ id: 'wire-sda-rtc', start: { componentId: 'ssd1306-1', pinName: 'DATA' }, end: { componentId: 'rtc1', pinName: 'SDA' }, color: '#2196f3' },
|
||||
// SCL bus (shared)
|
||||
{ id: 'wire-scl', start: { componentId: 'arduino-uno', pinName: 'A5' }, end: { componentId: 'ssd1306-1', pinName: 'CLK' }, color: '#ff9800' },
|
||||
{ id: 'wire-scl-rtc', start: { componentId: 'ssd1306-1', pinName: 'CLK' }, end: { componentId: 'rtc1', pinName: 'SCL' }, color: '#ff9800' },
|
||||
// Power + ground for both devices
|
||||
{ id: 'wire-gnd', start: { componentId: 'arduino-uno', pinName: 'GND.1' }, end: { componentId: 'ssd1306-1', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-gnd-rtc', start: { componentId: 'ssd1306-1', pinName: 'GND' }, end: { componentId: 'rtc1', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-vcc', start: { componentId: 'arduino-uno', pinName: '5V' }, end: { componentId: 'ssd1306-1', pinName: 'VIN' }, color: '#ff0000' },
|
||||
{ id: 'wire-vcc-rtc', start: { componentId: 'ssd1306-1', pinName: 'VIN' }, end: { componentId: 'rtc1', pinName: 'VCC' }, color: '#ff0000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -1653,8 +1641,16 @@ void loop() {
|
|||
delay(1000);
|
||||
}
|
||||
`,
|
||||
components: [{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} }],
|
||||
wires: [],
|
||||
components: [
|
||||
{ type: 'wokwi-arduino-uno', id: 'arduino-uno', x: 100, y: 100, properties: {} },
|
||||
{ type: 'wokwi-ds1307', id: 'rtc1', x: 480, y: 200, properties: {} },
|
||||
],
|
||||
wires: [
|
||||
{ id: 'w-sda', start: { componentId: 'arduino-uno', pinName: 'A4' }, end: { componentId: 'rtc1', pinName: 'SDA' }, color: '#0066cc' },
|
||||
{ id: 'w-scl', start: { componentId: 'arduino-uno', pinName: 'A5' }, end: { componentId: 'rtc1', pinName: 'SCL' }, color: '#ffaa00' },
|
||||
{ id: 'w-vcc', start: { componentId: 'arduino-uno', pinName: '5V' }, end: { componentId: 'rtc1', pinName: 'VCC' }, color: '#ff0000' },
|
||||
{ id: 'w-gnd', start: { componentId: 'arduino-uno', pinName: 'GND' }, end: { componentId: 'rtc1', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'i2c-eeprom-rw',
|
||||
|
|
@ -6997,7 +6993,7 @@ void loop() {
|
|||
id: 'attiny85-blink',
|
||||
title: 'ATtiny85: Blink LED',
|
||||
description:
|
||||
'Classic blink on the ATtiny85 built-in LED (PB1). Great first sketch for this tiny 8-bit chip.',
|
||||
'Blink an external LED on PB1 (Digispark pin 1) through a 220 Ohm current-limit resistor.',
|
||||
category: 'basics',
|
||||
difficulty: 'beginner',
|
||||
boardFilter: 'attiny85',
|
||||
|
|
@ -7006,11 +7002,11 @@ void loop() {
|
|||
boardKind: 'attiny85',
|
||||
x: 120,
|
||||
y: 160,
|
||||
code: `// ATtiny85 — Blink built-in LED on PB1
|
||||
// Pin 1 = PB1 = OC0B/OC1A (also Digispark LED)
|
||||
code: `// ATtiny85 — Blink an LED wired to PB1 (Digispark pin 1)
|
||||
// PB1 -> 220 Ohm -> LED anode; LED cathode -> GND
|
||||
|
||||
void setup() {
|
||||
pinMode(1, OUTPUT); // PB1 as output
|
||||
pinMode(1, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
|
@ -7022,8 +7018,30 @@ void loop() {
|
|||
},
|
||||
],
|
||||
code: '',
|
||||
components: [],
|
||||
wires: [],
|
||||
components: [
|
||||
{ type: 'wokwi-led', id: 'tiny-led1', x: 360, y: 140, properties: { color: 'green' } },
|
||||
{ type: 'wokwi-resistor', id: 'tiny-r1', x: 360, y: 220, properties: { resistance: '220' } },
|
||||
],
|
||||
wires: [
|
||||
{
|
||||
id: 'tinyw1',
|
||||
start: { componentId: 'attiny85', pinName: 'PB1' },
|
||||
end: { componentId: 'tiny-led1', pinName: 'A' },
|
||||
color: '#22cc22',
|
||||
},
|
||||
{
|
||||
id: 'tinyw2',
|
||||
start: { componentId: 'tiny-led1', pinName: 'C' },
|
||||
end: { componentId: 'tiny-r1', pinName: '1' },
|
||||
color: '#888888',
|
||||
},
|
||||
{
|
||||
id: 'tinyw3',
|
||||
start: { componentId: 'tiny-r1', pinName: '2' },
|
||||
end: { componentId: 'attiny85', pinName: 'GND' },
|
||||
color: '#000000',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -7058,6 +7076,13 @@ void loop() {
|
|||
components: [
|
||||
{ type: 'wokwi-pushbutton', id: 'tiny-btn1', x: 380, y: 130, properties: { color: 'green' } },
|
||||
{ type: 'wokwi-led', id: 'tiny-led1', x: 380, y: 240, properties: { color: 'red' } },
|
||||
{
|
||||
type: 'wokwi-resistor',
|
||||
id: 'tiny-btn-r1',
|
||||
x: 380,
|
||||
y: 320,
|
||||
properties: { resistance: '220' },
|
||||
},
|
||||
],
|
||||
wires: [
|
||||
{
|
||||
|
|
@ -7078,10 +7103,16 @@ void loop() {
|
|||
end: { componentId: 'tiny-led1', pinName: 'A' },
|
||||
color: '#ff4444',
|
||||
},
|
||||
{
|
||||
id: 'tb-led-r',
|
||||
start: { componentId: 'tiny-led1', pinName: 'C' },
|
||||
end: { componentId: 'tiny-btn-r1', pinName: '1' },
|
||||
color: '#888888',
|
||||
},
|
||||
{
|
||||
id: 'tb-gnd2',
|
||||
start: { componentId: 'attiny85', pinName: 'GND' },
|
||||
end: { componentId: 'tiny-led1', pinName: 'C' },
|
||||
start: { componentId: 'tiny-btn-r1', pinName: '2' },
|
||||
end: { componentId: 'attiny85', pinName: 'GND' },
|
||||
color: '#000000',
|
||||
},
|
||||
],
|
||||
|
|
@ -7217,6 +7248,13 @@ void loop() {
|
|||
properties: { temperature: '25' },
|
||||
},
|
||||
{ type: 'wokwi-led', id: 'tiny-ntc-led', x: 380, y: 300, properties: { color: 'red' } },
|
||||
{
|
||||
type: 'wokwi-resistor',
|
||||
id: 'tiny-ntc-r1',
|
||||
x: 380,
|
||||
y: 380,
|
||||
properties: { resistance: '220' },
|
||||
},
|
||||
],
|
||||
wires: [
|
||||
{
|
||||
|
|
@ -7243,10 +7281,16 @@ void loop() {
|
|||
end: { componentId: 'tiny-ntc-led', pinName: 'A' },
|
||||
color: '#ff4444',
|
||||
},
|
||||
{
|
||||
id: 'tn-led-r',
|
||||
start: { componentId: 'tiny-ntc-led', pinName: 'C' },
|
||||
end: { componentId: 'tiny-ntc-r1', pinName: '1' },
|
||||
color: '#888888',
|
||||
},
|
||||
{
|
||||
id: 'tn-lgnd',
|
||||
start: { componentId: 'attiny85', pinName: 'GND' },
|
||||
end: { componentId: 'tiny-ntc-led', pinName: 'C' },
|
||||
start: { componentId: 'tiny-ntc-r1', pinName: '2' },
|
||||
end: { componentId: 'attiny85', pinName: 'GND' },
|
||||
color: '#000000',
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ import {
|
|||
ATtinyTimer1,
|
||||
attinyTimer1Config,
|
||||
} from 'avr8js';
|
||||
import type { AVRTimerConfig } from 'avr8js/dist/esm/peripherals/timer';
|
||||
import type { ADCConfig, ADCMuxConfiguration } from 'avr8js/dist/esm/peripherals/adc';
|
||||
import { ADCMuxInputType, ADCReference } from 'avr8js/dist/esm/peripherals/adc';
|
||||
import { PinManager } from './PinManager';
|
||||
import { hexToUint8Array } from '../utils/hexParser';
|
||||
import { I2CBusManager, nullI2CMaster } from './I2CBusManager';
|
||||
|
|
@ -143,6 +146,110 @@ const attiny85PortBConfig = {
|
|||
externalInterrupts: [] as never[],
|
||||
};
|
||||
|
||||
/**
|
||||
* ATtiny85 Timer0 config — Arduino `millis()` / `delay()` rely on the
|
||||
* TIMER0_OVF interrupt to tick the millisecond counter. avr8js's generic
|
||||
* `AVRTimer` is fully data-driven, so we just supply ATtiny85's register
|
||||
* addresses (different from the ATmega328P defaults in `timer0Config`)
|
||||
* and the right interrupt vector offsets.
|
||||
*
|
||||
* Refs: <avr/iotnx5.h> for register addresses; ATtiny25/45/85 datasheet
|
||||
* (Atmel-2586) for vector indices.
|
||||
* _VECTOR(5) → TIMER0_OVF → word 0x0A
|
||||
* _VECTOR(10) → TIMER0_COMPA → word 0x14
|
||||
* _VECTOR(11) → TIMER0_COMPB → word 0x16
|
||||
*/
|
||||
/**
|
||||
* ATtiny85 ADC config — required because the chip's ADC registers live at
|
||||
* completely different memory addresses than the ATmega328P defaults that
|
||||
* avr8js's `adcConfig` ships with. Without this, `analogRead()` writes
|
||||
* ADSC at ATtiny85's ADCSRA (0x26) and polls forever because avr8js is
|
||||
* listening at 0x7A instead.
|
||||
*
|
||||
* Refs: <avr/iotnx5.h>; ATtiny25/45/85 datasheet (Atmel-2586) sec. 17.
|
||||
* ADMUX = 0x07 (I/O) -> 0x27 (mem)
|
||||
* ADCSRA = 0x06 -> 0x26
|
||||
* ADCSRB = 0x03 -> 0x23
|
||||
* ADCL = 0x04 -> 0x24
|
||||
* ADCH = 0x05 -> 0x25
|
||||
* DIDR0 = 0x14 -> 0x34
|
||||
* ADC_vect = _VECTOR(8) -> word 0x10
|
||||
*
|
||||
* MUX field is 4 bits (bits 3:0). Single-ended channels 0..3 = PB5/PB2/PB4/PB3.
|
||||
* Reference bits REFS1:REFS0 at ADMUX[7:6] select VCC/AREF/Internal1V1 by default;
|
||||
* full REFS2 extension lives at ADMUX[4] but the avr8js helper checks bit 3,
|
||||
* so the rare 2.56 V internal reference is currently unsupported — every
|
||||
* default-ref sketch (`analogReference(DEFAULT)`) works fine.
|
||||
*/
|
||||
const attiny85AdcChannels: ADCMuxConfiguration = {
|
||||
0: { type: ADCMuxInputType.SingleEnded, channel: 0 }, // PB5
|
||||
1: { type: ADCMuxInputType.SingleEnded, channel: 1 }, // PB2
|
||||
2: { type: ADCMuxInputType.SingleEnded, channel: 2 }, // PB4
|
||||
3: { type: ADCMuxInputType.SingleEnded, channel: 3 }, // PB3
|
||||
12: { type: ADCMuxInputType.Constant, voltage: 1.1 }, // VBG
|
||||
13: { type: ADCMuxInputType.Constant, voltage: 0 }, // GND
|
||||
15: { type: ADCMuxInputType.Temperature },
|
||||
};
|
||||
|
||||
const attiny85AdcConfig: ADCConfig = {
|
||||
ADMUX: 0x27,
|
||||
ADCSRA: 0x26,
|
||||
ADCSRB: 0x23,
|
||||
ADCL: 0x24,
|
||||
ADCH: 0x25,
|
||||
DIDR0: 0x34,
|
||||
// ATtiny85 vectors are 1-word RJMP (vs ATmega328P's 2-word JMP) so the
|
||||
// avr8js "address" field is the raw vector index, not vector*2.
|
||||
adcInterrupt: 0x08, // _VECTOR(8) ADC_vect
|
||||
numChannels: 4,
|
||||
muxInputMask: 0xf,
|
||||
muxChannels: attiny85AdcChannels,
|
||||
adcReferences: [
|
||||
ADCReference.AVCC, // 00 = VCC
|
||||
ADCReference.AREF, // 01 = external AREF (PB0)
|
||||
ADCReference.Internal1V1, // 10 = internal 1.1 V
|
||||
ADCReference.Reserved, // 11 = reserved
|
||||
],
|
||||
};
|
||||
|
||||
const attiny85Timer0Config: AVRTimerConfig = {
|
||||
bits: 8,
|
||||
captureInterrupt: 0,
|
||||
// ATtiny85 vectors are 1-word RJMP (vs ATmega328P's 2-word JMP) so the
|
||||
// avr8js "address" field is the raw vector index, not vector*2.
|
||||
compAInterrupt: 0x0a, // _VECTOR(10) TIMER0_COMPA_vect
|
||||
compBInterrupt: 0x0b, // _VECTOR(11) TIMER0_COMPB_vect
|
||||
compCInterrupt: 0,
|
||||
ovfInterrupt: 0x05, // _VECTOR(5) TIMER0_OVF_vect
|
||||
TIFR: 0x58,
|
||||
OCRA: 0x56,
|
||||
OCRB: 0x5c,
|
||||
OCRC: 0,
|
||||
ICR: 0,
|
||||
TCNT: 0x52,
|
||||
TCCRA: 0x4f,
|
||||
TCCRB: 0x53,
|
||||
TCCRC: 0,
|
||||
TIMSK: 0x59,
|
||||
TOV: 0b00000010,
|
||||
OCFA: 0b00010000,
|
||||
OCFB: 0b00001000,
|
||||
OCFC: 0,
|
||||
TOIE: 0b00000010,
|
||||
OCIEA: 0b00010000,
|
||||
OCIEB: 0b00001000,
|
||||
OCIEC: 0,
|
||||
compPortA: 0x38,
|
||||
compPinA: 0,
|
||||
compPortB: 0x38,
|
||||
compPinB: 1,
|
||||
compPortC: 0,
|
||||
compPinC: 0,
|
||||
externalClockPort: 0x36,
|
||||
externalClockPin: 2,
|
||||
dividers: { 0: 0, 1: 1, 2: 8, 3: 64, 4: 256, 5: 1024, 6: 0, 7: 0 },
|
||||
};
|
||||
|
||||
/** Ordered list of Mega ports with their avr8js configs */
|
||||
const MEGA_PORT_CONFIGS = [
|
||||
{ name: 'PORTA', config: portAConfig },
|
||||
|
|
@ -245,10 +352,26 @@ export class AVRSimulator {
|
|||
this.cpu = new CPU(this.program, sramBytes);
|
||||
|
||||
if (this.boardVariant === 'tiny85') {
|
||||
// ATtiny85: PORTB only (PB0-PB5), Timer1 via ATtinyTimer1, no USART
|
||||
// ATtiny85: PORTB only (PB0-PB5). Timer0 powers millis()/delay() in
|
||||
// ATTinyCore via TIMER0_OVF. Timer1 is the high-speed 8-bit PWM
|
||||
// timer (PLL clock). No hardware USART on this chip.
|
||||
//
|
||||
// Known limitation (task #116): the Timer0 OVF interrupt does fire at
|
||||
// the correct cadence (1.024 ms simulated, verified via debug
|
||||
// instrumentation), but real ATTinyCore-compiled `delay()` does not
|
||||
// observably advance — the LED stays stuck either HIGH or LOW
|
||||
// depending on which phase the firmware was in when the first OVF
|
||||
// hit. Likely a subtle interaction between the avr8js clearInterrupt
|
||||
// semantics (only clears the pending queue entry, leaves TIFR bit
|
||||
// set) and ATTinyCore's ISR relying on hardware auto-clear of TOV0.
|
||||
// Workaround attempts (manual TIFR clear after ISR entry) did not
|
||||
// change the visible behavior. Needs a deeper avr8js dive.
|
||||
this.portB = new AVRIOPort(this.cpu, attiny85PortBConfig as typeof portBConfig);
|
||||
this.adc = new AVRADC(this.cpu, adcConfig);
|
||||
this.peripherals = [new ATtinyTimer1(this.cpu, attinyTimer1Config)];
|
||||
this.adc = new AVRADC(this.cpu, attiny85AdcConfig);
|
||||
this.peripherals = [
|
||||
new AVRTimer(this.cpu, attiny85Timer0Config),
|
||||
new ATtinyTimer1(this.cpu, attinyTimer1Config),
|
||||
];
|
||||
// usart stays null — ATtiny85 has no hardware USART
|
||||
} else {
|
||||
// ATmega2560 has more vectors before the timers/USART (8 external INTs, etc.),
|
||||
|
|
@ -611,8 +734,11 @@ export class AVRSimulator {
|
|||
|
||||
if (this.boardVariant === 'tiny85') {
|
||||
this.portB = new AVRIOPort(this.cpu, attiny85PortBConfig as typeof portBConfig);
|
||||
this.adc = new AVRADC(this.cpu, adcConfig);
|
||||
this.peripherals = [new ATtinyTimer1(this.cpu, attinyTimer1Config)];
|
||||
this.adc = new AVRADC(this.cpu, attiny85AdcConfig);
|
||||
this.peripherals = [
|
||||
new AVRTimer(this.cpu, attiny85Timer0Config),
|
||||
new ATtinyTimer1(this.cpu, attinyTimer1Config),
|
||||
];
|
||||
this.usart = null;
|
||||
} else {
|
||||
this.spi = new AVRSPI(this.cpu, spiConfig, 16000000);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ export interface SimulatorBridges {
|
|||
uartInstalled: boolean;
|
||||
/** Original onByteTransmit so non-chip listeners still receive bytes. */
|
||||
uartPreviousOnByteTransmit: ((byte: number) => void) | null;
|
||||
/** 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;
|
||||
|
||||
/** Shared SPI bus across all custom chips on this simulator. */
|
||||
spiBus: SPIBus;
|
||||
|
|
@ -49,6 +56,8 @@ export function getSimulatorBridges(simulator: any): SimulatorBridges {
|
|||
uartListeners: new Set(),
|
||||
uartInstalled: false,
|
||||
uartPreviousOnByteTransmit: null,
|
||||
uartRxQueue: [],
|
||||
uartDrainHandle: 0,
|
||||
spiBus: new SPIBus(),
|
||||
spiInstalled: false,
|
||||
spiPreviousOnByte: null,
|
||||
|
|
@ -102,17 +111,57 @@ export function ensureUartBridge(simulator: any): void {
|
|||
|
||||
/**
|
||||
* Inject a byte into the simulator's RX path so the sketch's `Serial.read()`
|
||||
* returns it. Routes per simulator family.
|
||||
* 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`.
|
||||
*/
|
||||
export function avrUartTx(simulator: any, byte: number): void {
|
||||
const kind = detectSimulatorKind(simulator);
|
||||
try {
|
||||
if (kind === 'avr' && simulator.usart && typeof simulator.usart.writeByte === 'function') {
|
||||
simulator.usart.writeByte(byte);
|
||||
} else if (kind === 'rp2040' && typeof simulator.serialWriteByte === 'function') {
|
||||
simulator.serialWriteByte(byte);
|
||||
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;
|
||||
}
|
||||
} catch { /* swallow — peripheral may not be ready yet */ }
|
||||
return;
|
||||
}
|
||||
if (kind === 'rp2040' && typeof simulator.serialWriteByte === 'function') {
|
||||
try { simulator.serialWriteByte(byte); } catch { /* swallow */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ── SPI ─────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
getSimulatorBridges,
|
||||
avrUartTx,
|
||||
getI2CBus,
|
||||
detectSimulatorKind,
|
||||
} from '../customChips';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
|
||||
|
|
@ -88,7 +89,13 @@ PartSimulationRegistry.register('custom-chip', {
|
|||
// ── ESP32 path ──────────────────────────────────────────────────────────
|
||||
// The chip's WASM runs in the backend QEMU worker process so I2C events
|
||||
// are answered synchronously. See docs/wiki/custom-chips-esp32-backend-runtime.md.
|
||||
if (typeof sim.registerSensor === 'function') {
|
||||
//
|
||||
// IMPORTANT: only take this path for actual ESP32 simulators. We cannot
|
||||
// gate on `typeof sim.registerSensor === 'function'` because AVR and
|
||||
// RP2040 simulators also expose `registerSensor` for I2C sensor proxies —
|
||||
// taking that branch on AVR routes the chip to the (non-existent) ESP32
|
||||
// backend and the client-side ChipInstance never runs.
|
||||
if (detectSimulatorKind(sim) === 'esp32' && typeof sim.registerSensor === 'function') {
|
||||
// Resolve each chip pin name → ESP32 GPIO via the diagram's wires. The
|
||||
// backend runtime uses this to call qemu_picsimlab_set_pin when the chip
|
||||
// does vx_pin_write, and to read live GPIO state for vx_pin_read.
|
||||
|
|
@ -141,6 +148,7 @@ PartSimulationRegistry.register('custom-chip', {
|
|||
// when the user stops the simulation.
|
||||
let instance: ChipInstance | null = null;
|
||||
let uartListener: ((byte: number) => void) | null = null;
|
||||
let rafHandle = 0;
|
||||
let disposed = false;
|
||||
|
||||
(async () => {
|
||||
|
|
@ -181,6 +189,24 @@ PartSimulationRegistry.register('custom-chip', {
|
|||
try { (el as any).paintFramebuffer(rgba, width, height); } catch { /* swallow */ }
|
||||
});
|
||||
}
|
||||
|
||||
// Drive the chip's timer-based execution every frame. Chips that
|
||||
// register a periodic `vx_timer_create` (e.g. a CPU-emulator chip
|
||||
// stepping its core, or a sensor publishing samples) need a
|
||||
// host-side tick to fire those callbacks — without this loop the
|
||||
// WASM is loaded but never executes anything past chip_setup().
|
||||
// We feed wall-clock nanoseconds; the chip's WasiShim already
|
||||
// exposes the same epoch via vx_sim_now_nanos.
|
||||
const tick = () => {
|
||||
if (disposed || !instance) return;
|
||||
try {
|
||||
instance.tickTimers(BigInt(Math.floor(performance.now() * 1_000_000)));
|
||||
} catch (e) {
|
||||
console.error(`[custom-chip:${componentId}] tickTimers threw:`, e);
|
||||
}
|
||||
rafHandle = requestAnimationFrame(tick);
|
||||
};
|
||||
rafHandle = requestAnimationFrame(tick);
|
||||
} catch (e) {
|
||||
console.error(`[custom-chip] ${componentId} failed to load:`, e);
|
||||
}
|
||||
|
|
@ -188,6 +214,8 @@ PartSimulationRegistry.register('custom-chip', {
|
|||
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (rafHandle) cancelAnimationFrame(rafHandle);
|
||||
rafHandle = 0;
|
||||
if (uartListener) bridges.uartListeners.delete(uartListener);
|
||||
if (instance) instance.dispose();
|
||||
instance = null;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ function pinNameToArduinoPin(pinName: string, boardKind: BoardKind): number {
|
|||
if (/^A\d+$/.test(pinName)) {
|
||||
return 14 + parseInt(pinName.slice(1), 10);
|
||||
}
|
||||
// ATtiny85 port-style names (PB0..PB5) map 1:1 to Arduino pin numbers
|
||||
// because ATTinyCore's pinMap is identity for the tiny85 variant.
|
||||
if (/^PB\d+$/.test(pinName)) {
|
||||
return parseInt(pinName.slice(2), 10);
|
||||
}
|
||||
if (/^\d+$/.test(pinName)) {
|
||||
return parseInt(pinName, 10);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,15 @@ const ADC_PIN_MAP: Partial<Record<BoardKind, Array<{ pinName: string; channel: n
|
|||
'arduino-uno': ADC_6CH,
|
||||
'arduino-nano': ADC_8CH,
|
||||
'arduino-mega': ADC_16CH,
|
||||
attiny85: adcRange('A', 0, 4),
|
||||
// ATtiny85: ADC channels live on PORTB pins, not on A0..An.
|
||||
// Wires reference the chip's own pin names (PB5/PB2/PB4/PB3).
|
||||
// PB5 -> ADC0 PB2 -> ADC1 PB4 -> ADC2 PB3 -> ADC3
|
||||
attiny85: [
|
||||
{ pinName: 'PB5', channel: 0 },
|
||||
{ pinName: 'PB2', channel: 1 },
|
||||
{ pinName: 'PB4', channel: 2 },
|
||||
{ pinName: 'PB3', channel: 3 },
|
||||
],
|
||||
|
||||
// RP2040
|
||||
'raspberry-pi-pico': [
|
||||
|
|
|
|||
Loading…
Reference in New Issue