diff --git a/client/public/ATtiny85-overlay.svg b/client/public/ATtiny85-overlay.svg
new file mode 100644
index 000000000..42a3704e3
--- /dev/null
+++ b/client/public/ATtiny85-overlay.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/client/public/ATtiny85.svg b/client/public/ATtiny85.svg
new file mode 100644
index 000000000..6ebc2098b
--- /dev/null
+++ b/client/public/ATtiny85.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/client/src/components/features/arduino-board.tsx b/client/src/components/features/arduino-board.tsx
index d73fab32d..2eb8cfc54 100644
--- a/client/src/components/features/arduino-board.tsx
+++ b/client/src/components/features/arduino-board.tsx
@@ -7,7 +7,7 @@ import { usePinPollingEngine } from "@/hooks/usePinPollingEngine";
import { onCustomEvent, offCustomEvent } from "@/utils/event-utils";
import { Logger } from "@shared/logger";
import { DEFAULT_BOARD_ID, getBoard, type BoardId } from "@shared/boards";
-import { activePinLabel, analogChannelPin, isAnalogPin } from "@/lib/active-board";
+import { activeAnalogPinTitle, activePinLabel, analogChannelPin, isAnalogPin } from "@/lib/active-board";
import { type BoardView, DEFAULT_LOGO_PLACEMENT, boardView } from "@/lib/board-view";
import type { RuntimeSimulationStatus } from "@shared/types/arduino.types";
@@ -731,7 +731,7 @@ function getAnalogDialogCoordinates(
left = Math.max(viewportMargin, Math.min(globalThis.innerWidth - dialogWidth - viewportMargin, left));
top = Math.max(viewportMargin, Math.min(globalThis.innerHeight - dialogHeight - viewportMargin, top));
- const pinLabel = activePinLabel(dialog.pin);
+ const pinLabel = activeAnalogPinTitle(dialog.pin);
return { left, top, dialogWidth, dialogHeight, pinLabel };
}
diff --git a/client/src/hooks/use-simulation-controller.ts b/client/src/hooks/use-simulation-controller.ts
index 6ca1c213f..ded294f7a 100644
--- a/client/src/hooks/use-simulation-controller.ts
+++ b/client/src/hooks/use-simulation-controller.ts
@@ -2,6 +2,7 @@ import { useCallback, useRef } from "react";
import { useMutation, type UseMutationResult } from "@tanstack/react-query";
import { Logger } from "@shared/logger";
import { normalizeSimulationTimeout } from "@shared/input-limits";
+import { sketchSerialBaud } from "@shared/parser-patterns";
import type { SimulationStatus } from "@shared/types/arduino.types";
import type { IncomingArduinoMessage } from "@/types/websocket";
import { useSimulationControllerState } from "./use-simulation-controller-state";
@@ -190,7 +191,11 @@ export function useSimulationController(
if (!artifact) throw new Error("The latest build has no image for the browser emulation. Compile again.");
const runtime = await hub.load("browser");
// Start first: replacing a run reports the old one stopped, which must not end the new one.
- runtime.start({ kind: "browser", artifact, ...(params.board ? { board: params.board } : {}), timeoutSec: timeout });
+ const serialBaud = sketchSerialBaud(compiledCodeRef.current ?? "");
+ runtime.start({
+ kind: "browser", artifact, ...(params.board ? { board: params.board } : {}), timeoutSec: timeout,
+ ...(serialBaud === undefined ? {} : { serialBaud }),
+ });
hub.setActive("browser");
return { success: true };
}
diff --git a/client/src/hooks/usePinPollingEngine.ts b/client/src/hooks/usePinPollingEngine.ts
index 980634781..bffcdc3a5 100644
--- a/client/src/hooks/usePinPollingEngine.ts
+++ b/client/src/hooks/usePinPollingEngine.ts
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import type { PinState } from "@/components/features/arduino-board";
import type { BoardPin } from "@shared/board-pins";
-import { activeAnalogPins, activePins, activePwmPins, isActivePwmPin } from "@/lib/active-board";
+import { activeAnalogPins, activeLedBuiltin, activePins, activePwmPins, isActivePwmPin } from "@/lib/active-board";
// Constants
/** Height of the Uno drawing; the fallback where the SVG names no view box. */
@@ -362,7 +362,7 @@ export function usePinPollingEngine({
}
};
- /** Update a single analog pin element: the SVG names it by its label (`A0`…`A15`). */
+ /** Update a single analog pin element: the SVG names it as the sketch does (`A0`…`A15`; on the ATtiny85 its number). */
const updateAnalogPin = (svgEl: SVGSVGElement, { pin: pinNumber, name }: BoardPin) => {
const frame = pinElement(svgEl, `pin-${name}-frame`);
const state = pinElement(svgEl, `pin-${name}-state`);
@@ -397,9 +397,10 @@ export function usePinPollingEngine({
const ledL = svgEl.querySelector("#led-l");
const ledTx = svgEl.querySelector("#led-tx");
const ledRx = svgEl.querySelector("#led-rx");
- const pin13On = (pinStates.find((p) => p.pin === 13)?.value ?? 0) > 0;
+ const ledBuiltin = activeLedBuiltin();
+ const ledBuiltinOn = (pinStates.find((p) => p.pin === ledBuiltin)?.value ?? 0) > 0;
applyLedVisual(ledOn, isSimulationRunning, "var(--color-led-green)", "#glow-green");
- applyLedVisual(ledL, pin13On, "var(--color-led-yellow)", "#glow-yellow");
+ applyLedVisual(ledL, ledBuiltinOn, "var(--color-led-yellow)", "#glow-yellow");
applyLedVisual(ledTx, txBlink, "var(--color-led-yellow)", "#glow-yellow");
applyLedVisual(ledRx, rxBlink, "var(--color-led-yellow)", "#glow-yellow");
};
@@ -422,7 +423,7 @@ export function usePinPollingEngine({
for (const analog of activeAnalogPins()) {
const valStr = getPinState(analog.pin)?.value?.toString() ?? "";
- updateLabelForPin(svgEl, `pin-${analog.label}-state`, `pin-${analog.label}-frame`, `pin-${analog.label}-val`, valStr, showPWMValues);
+ updateLabelForPin(svgEl, `pin-${analog.name}-state`, `pin-${analog.name}-frame`, `pin-${analog.name}-val`, valStr, showPWMValues);
}
};
diff --git a/client/src/index.css b/client/src/index.css
index 2fd2bb46b..46f49347c 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -489,6 +489,12 @@ button:not(.arduino-overlay):not(.parser-message-btn) > svg {
min-height: 0 !important;
}
+ /* The floating panel navigation (mobile-layout.tsx: up to five 40px buttons with gaps, padding
+ and margins, bottom right) must not cover the board's pins: the board is drawn above it. */
+ .workspace-layout[data-layout-mode="mobile"] .workspace-board-panel.workspace-mobile-full-panel {
+ padding-bottom: 20rem;
+ }
+
/* Recharts default tooltip — styled with Tailwind utilities via @apply */
.recharts-default-tooltip {
@apply text-ui-xs text-white rounded p-2;
diff --git a/client/src/lib/active-board.ts b/client/src/lib/active-board.ts
index cdbd69196..2a4db25cc 100644
--- a/client/src/lib/active-board.ts
+++ b/client/src/lib/active-board.ts
@@ -1,4 +1,4 @@
-import { boardAnalogPins, boardPins, boardPwmPins, boardPinLabel, findBoardPin, type BoardPin } from "@shared/board-pins";
+import { boardAnalogPins, boardPins, boardPwmPins, boardPinLabel, findBoardPin, resolveBoardPin, type BoardPin } from "@shared/board-pins";
import { type BoardId, DEFAULT_BOARD_ID } from "@shared/boards";
/**
@@ -25,9 +25,18 @@ export const isAnalogPin = (pin: number): boolean => findBoardPin(hardwareBoard(
/** `13`, `A0`: how a sketch writes the pin on the active board. */
export const activePinLabel = (pin: number): string => boardPinLabel(hardwareBoard(), pin);
+/** The name and, where it is a second name (ATtiny85), the analog name: `2 (A1)`; else as `activePinLabel`. */
+export const activeAnalogPinTitle = (pin: number): string => {
+ const found = findBoardPin(hardwareBoard(), pin);
+ return found?.analogName ? `${found.name} (${found.analogName})` : activePinLabel(pin);
+};
+
/** Pin number of analog input `A` on the active board. */
export const analogChannelPin = (channel: number): number | undefined => boardAnalogPins(hardwareBoard()).find((pin) => pin.adcChannel === channel)?.pin;
export const isActivePwmPin = (pin: number): boolean => boardPwmPins(hardwareBoard()).includes(pin);
export const activePwmPins = (): readonly number[] => boardPwmPins(hardwareBoard());
export const activeAnalogPins = (): readonly BoardPin[] => boardAnalogPins(hardwareBoard());
+
+/** `LED_BUILTIN` of the active board: 13 on the ATmega boards, 1 on the ATtiny85. */
+export const activeLedBuiltin = (): number => resolveBoardPin(hardwareBoard(), "LED_BUILTIN") ?? 13;
diff --git a/client/src/lib/board-view.ts b/client/src/lib/board-view.ts
index 90167d15a..1a63e2ddc 100644
--- a/client/src/lib/board-view.ts
+++ b/client/src/lib/board-view.ts
@@ -28,9 +28,16 @@ const MEGA_VIEW: BoardView = {
logo: { left: 74, top: 38, width: 20, height: 10 },
};
-const VIEWS: Partial> = { uno: UNO_VIEW, nano: NANO_VIEW, mega2560: MEGA_VIEW };
+// Generated by scripts/generate-attiny85-svg.mjs: a DIP-8 on a 32 x 24 mm carrier at 4 units per mm.
+// The mark sits on the free area at the top, between the ON and L LEDs.
+const ATTINY85_VIEW: BoardView = {
+ boardSvg: "/ATtiny85.svg", overlaySvg: "/ATtiny85-overlay.svg", width: 128, height: 96,
+ logo: { left: 42, top: 15, width: 24, height: 10 },
+};
+
+const VIEWS: Record = { uno: UNO_VIEW, nano: NANO_VIEW, mega2560: MEGA_VIEW, attiny85: ATTINY85_VIEW };
-/** The view of a board; a board without graphics yet (ATtiny85) is drawn as the Uno. */
+/** The view of a board. */
export function boardView(board: BoardId = DEFAULT_BOARD_ID): BoardView {
return VIEWS[board] ?? UNO_VIEW;
}
diff --git a/client/src/lib/runtime/browser-runtime.ts b/client/src/lib/runtime/browser-runtime.ts
index 454b2b4c6..2cfc7dceb 100644
--- a/client/src/lib/runtime/browser-runtime.ts
+++ b/client/src/lib/runtime/browser-runtime.ts
@@ -90,7 +90,7 @@ export class BrowserRuntime implements SimulationRuntime {
private readonly gate = new RunGate();
private worker: EmulatorWorker | undefined;
private phase: Phase = "idle";
- private pendingStart: { readonly timeoutSec: number } | undefined;
+ private pendingStart: { readonly timeoutSec: number; readonly serialBaud?: number } | undefined;
private timeoutSec = 0;
private deadline: unknown;
private heartbeatTimer: unknown;
@@ -120,7 +120,7 @@ export class BrowserRuntime implements SimulationRuntime {
this.registry.clear();
this.board = artifact.board;
this.heldCarriageReturn.clear();
- this.pendingStart = { timeoutSec: request.timeoutSec };
+ this.pendingStart = { timeoutSec: request.timeoutSec, ...(request.serialBaud === undefined ? {} : { serialBaud: request.serialBaud }) };
this.timeoutSec = request.timeoutSec;
this.phase = "loading";
this.lastHeard = this.timers.now();
@@ -264,7 +264,12 @@ export class BrowserRuntime implements SimulationRuntime {
if (this.phase !== "loading" || !start) return;
this.pendingStart = undefined;
this.phase = "starting";
- this.post({ type: "start", runId: this.gate.runIdFor("start"), timeoutMs: start.timeoutSec * 1000 });
+ this.post({
+ type: "start",
+ runId: this.gate.runIdFor("start"),
+ timeoutMs: start.timeoutSec * 1000,
+ ...(start.serialBaud === undefined ? {} : { serialBaud: start.serialBaud }),
+ });
this.expect(this.limits.confirmMs, "the emulator did not start");
}
diff --git a/client/src/lib/runtime/simulation-runtime.ts b/client/src/lib/runtime/simulation-runtime.ts
index 7df3caf34..49f120998 100644
--- a/client/src/lib/runtime/simulation-runtime.ts
+++ b/client/src/lib/runtime/simulation-runtime.ts
@@ -13,6 +13,8 @@ export type StartRequest =
/** The board selected in the UI; the worker refuses an artifact built for another board. Defaults to the artifact's board. */
readonly board?: BoardId;
readonly timeoutSec: number;
+ /** `Serial.begin()` value of the sketch, for a board whose `Serial` is a software UART (ATtiny85). */
+ readonly serialBaud?: number;
}
| {
readonly kind: "host-mock";
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index 76cfc5aaf..b88d1b02b 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -56,16 +56,18 @@ beschreibt das
## Board wählen
Unter **Tools → Board** wählst du das Board, für das UnoSim kompiliert und das
-es darstellt: **Arduino UNO**, **Arduino Nano** oder **Arduino Mega 2560**. Die
-Wahl bleibt im Browser gespeichert; ein Wechsel stoppt einen laufenden Sketch.
-Nano und Mega tragen den Hinweis „preview“: Sie laufen in der Emulation, aber
-UnoSim hat ihre Ergebnisse noch nicht mit einem echten Board verglichen.
+es darstellt: **Arduino UNO**, **Arduino Nano**, **Arduino Mega 2560** oder
+**ATtiny85**. Die Wahl bleibt im Browser gespeichert; ein Wechsel stoppt einen
+laufenden Sketch. Nano, Mega und ATtiny85 tragen den Hinweis „preview“: Sie
+laufen in der Emulation, aber UnoSim hat ihre Ergebnisse noch nicht mit einem
+echten Board verglichen.
| Board | Pins | PWM (`analogWrite`) | Analogeingänge | Serielle Schnittstellen |
|---|---|---|---|---|
| Uno | 0 bis 13, A0 bis A5 | 3, 5, 6, 9, 10, 11 | A0 bis A5 | `Serial` |
| Nano | wie der Uno, dazu A6 und A7 (nur analog) | wie der Uno | A0 bis A7 | `Serial` |
| Mega 2560 | 0 bis 53, A0 bis A15 | 2 bis 13 und 44 bis 46 | A0 bis A15 | `Serial`, `Serial1`, `Serial2`, `Serial3` |
+| ATtiny85 | 0 bis 4 (PB0 bis PB4; PB5 ist RESET) | 0, 1, 4 | A0 bis A3 auf den Pins 5, 2, 4, 3 | `Serial` (in Software, auf 0 und 1) |
Beim **Mega 2560** gilt:
@@ -89,6 +91,27 @@ Beim **Mega 2560** gilt:
keine Pinnummer über 13 voraussetzen (`A0` bis `A5` heißen dort anders
nummeriert, siehe oben).
+Beim **ATtiny85** gilt (Arduino-Core ATTinyCore 1.5.2, 8 MHz intern):
+
+- Die Pins 0 bis 5 sind PB0 bis PB5. `A1` ist ein zweiter Name für Pin 2, `A2`
+ für Pin 4, `A3` für Pin 3, `A0` für Pin 5; `analogRead(1)` liest wie
+ `analogRead(A1)`. Auf dem Board heißt der Pin deshalb „2 (A1)“. `LED_BUILTIN`
+ ist Pin 1.
+- PB5 ist der RESET-Pin und als Ein- oder Ausgang nicht nutzbar; UnoSim warnt,
+ wenn der Sketch ihn digital verwendet. `analogRead(A0)` funktioniert.
+- Es gibt keine serielle Schnittstelle in Hardware. `Serial` sendet in Software
+ auf Pin 0 (TX) und empfängt über den Analogkomparator auf Pin 1 (RX), mit
+ einem Format von 8N1. Daraus folgen Grenzen des Boards, die UnoSim nachbildet:
+ - **Halbduplex:** Während der Sketch sendet, empfängt er nichts. Lies
+ Eingaben zuerst (`Serial.parseInt()`) und antworte danach; ein Sketch, der
+ ständig ausgibt, verliert Eingaben.
+ - Wer Pin 0 oder Pin 1 anders benutzt (auch für `analogWrite`), stört
+ `Serial`; UnoSim warnt dann.
+ - Die Baudrate steht in keinem Register. UnoSim erkennt sie an der Ausgabe;
+ bis dahin gilt der Wert aus `Serial.begin()`. Übliche Rate ist 9600.
+- Speicher: 8 KB Flash, 512 Byte SRAM, 512 Byte EEPROM.
+- Die Legacy-Simulation gibt es nicht; `Serial1` bis `Serial3` auch nicht.
+
## Der Tutor
Der Tutor stellt Lernfragen zu deinem aktuellen Sketch und gibt Rückmeldung auf
@@ -149,11 +172,11 @@ Unter **File → Settings** findest du außerdem:
UnoSim prüft jeden Sketch zuerst mit dem echten Arduino-Compiler für das
gewählte Board (Standard: Uno). Was hier kompiliert, kompiliert also auch auf der
Hardware. Danach läuft das fertige Programm in der **Emulation**: ein Nachbau
-des Mikrocontrollers (ATmega328P, beim Mega 2560 der ATmega2560) im Browser,
+des Mikrocontrollers (ATmega328P, beim Mega 2560 der ATmega2560, beim ATtiny85 der ATtiny85) im Browser,
der den Maschinencode des Sketches ausführt, nicht ein Programm für den
UnoSim-Server. Datentypen, Überläufe, `millis()` und `delay()` verhalten sich
deshalb wie auf dem Uno. Wer die frühere Art der Simulation braucht, wählt
-unter **File → Settings → Simulation engine** „Legacy“ (siehe unten). Das gibt es nur für den Uno; bei Nano und Mega 2560 ist die Einstellung sichtbar, aber fest auf „Emulation“ gesetzt und nennt den Grund.
+unter **File → Settings → Simulation engine** „Legacy“ (siehe unten). Das gibt es nur für den Uno; bei Nano, Mega 2560 und ATtiny85 ist die Einstellung sichtbar, aber fest auf „Emulation“ gesetzt und nennt den Grund.
Die Emulation ist ein Modell. Sie weicht in diesen Punkten vom echten Board ab:
diff --git a/docs/proposals/browser-emulation-plan.md b/docs/proposals/browser-emulation-plan.md
index ee2f38414..bc0973393 100644
--- a/docs/proposals/browser-emulation-plan.md
+++ b/docs/proposals/browser-emulation-plan.md
@@ -1136,6 +1136,12 @@ PB2, PB4, PB3; Referenzen VCC, 1,1 V, 2,56 V), Watchdog, Ein-Wort-Vektoren,
8 MHz; kein USART. Die Referenzauswahl des ADC liest `REFS2` selbst an Bit 4,
weil avr8js 0.21.1 es an Bit 3 erwartet (P0.5: sonst 102 statt 200 bei
2,56 V); der Fehler wird upstream gemeldet.
+Umgesetzt (`mcu/attiny85.ts`, exportiert als `ATTINY85`; registriert erst mit
+der Board-Hardware in P6.4). Weitere Funde: INT0 und PCINT teilen GIMSK/GIFR,
+avr8js verdrahtet das nur für je einen (das Profil umhüllt die Hooks);
+`ATtinyTimer1` erzeugt in der PWM-Konfiguration von ATTinyCore kein Signal an
+OC1x (Register stimmen, das Tastverhältnis kommt aus ihnen; offen, siehe
+README des Pakets). Die Meldungen an avr8js macht ein Mensch.
**P6.3 Software-UART (M).**
TX-Pin des Cores dekodieren, Baudrate aus der Bitbreite (größte
@@ -1145,13 +1151,30 @@ Analogkomparators (ACSR, Vektor 7) mit einer Wellenform an PB1 in simulierter
Zeit; Baudrate aus der Ausgabe, sonst aus dem Serial Monitor. Das
Benutzerhandbuch nennt die Grenzen des Boards: Halbduplex, kein Empfang, wenn
PB1 anders genutzt wird (P0.5).
+Umgesetzt als `io/software-uart.ts` (`SoftwareUart` mit der Schnittstelle des
+USART für `SerialPorts`): Erkennung verlangt 90 % passende Abstände (die
+Pin-Initialisierung erzeugt Kurzimpulse, der Core dehnt einzelne Bits), die
+Bitbreite wird aus den Abständen gemittelt, Startbits werden in Bitmitte
+geprüft. Anbindung an die Serial-Schicht, die Baudrate des Serial Monitors und
+das Benutzerhandbuch folgen mit der Board-Hardware (P6.4/P6.5).
**P6.4 Board-Hardware und Regeln (M).**
Pins 0–5, PB5 als RESET sichtbar, aber nicht nutzbar; A0–A3; PWM 0, 1, 4;
Parserregeln (kein Hardware-Serial, Warnung bei Nutzung des TX-Pins).
+Umgesetzt: `ATTINY85_BOARD` und Registrierung von Profil und Hardware; Serial-
+Port der Art `software` (`SerialPorts` mit `SoftwareUart`); PWM-Beobachtung für
+Timer 1 des ATtiny85; Pinmodell mit Analog-Zweitnamen; die Parser rechnen
+`A` wie ATTinyCore als `0x80 | n` und `analogRead(n)` als Kanal; Regeln für
+RESET (PB5) und die Serial-Pins; Pinbelegung im Toolchain-Gate gegen den Core.
+Freigegebene Teständerungen: fünf Tests, die den ATtiny85 als Beispiel für ein
+Board ohne Hardware nutzten, prüfen den Rückfall jetzt mit einer unbekannten ID.
**P6.5 ATtiny85 freigeben (M).**
SVG (DIP-8), Proben, E2E; Status `preview`.
+Umgesetzt: Boardansicht (`npm run generate:attiny85-svg`), 5 Korpus-Proben,
+`e2e/attiny85.spec.ts`, Status `preview`, Benutzerhandbuch; Startbefehl des
+Workers mit `serialBaud` aus `Serial.begin()`; Abnahme
+[report-m3-acceptance.md](report-m3-acceptance.md).
### Phase 7 – Betrieb, Kapazität, Release (M4)
diff --git a/docs/proposals/browser-emulation-status.md b/docs/proposals/browser-emulation-status.md
index 22787e009..4cc2d32b9 100644
--- a/docs/proposals/browser-emulation-status.md
+++ b/docs/proposals/browser-emulation-status.md
@@ -7,7 +7,7 @@
|---|---|---|---|
| P0.1 | in Arbeit | #258 | Entwürfe ADR 0009/0010 (`Proposed`) mit den Ergebnissen von P0.2, P0.4–P0.6 (#262, #263); offen: E12 nach P0.3, Annahme durch die Maintainer |
| P0.2 | erledigt | #259 | Kriterium erfüllt: p95-Mehraufwand ≤ 195 ms mit kopiertem Core-Cache, keine fremde Datei lesbar; Nebenbefund `.include` auf `/proc/self/environ` (V1) |
-| P0.3 | erledigt (synthetisch) | (dieser PR) | Ersatzmessung in Containern mit 1 vCPU/2 GB und 2 vCPU/4 GB, Chromium 145 und Firefox 146: IT05.5 `peakSpeed` 2,11–4,42 (Kriterium ≥ 1,2), siehe [report-p0-3-vm-matrix.md](report-p0-3-vm-matrix.md); **keine** Messung auf realen Schulgeräten, eine Messung auf dem schwächsten echten Gerät bleibt empfohlen |
+| P0.3 | erledigt (synthetisch) | #291 | Ersatzmessung in Containern mit 1 vCPU/2 GB und 2 vCPU/4 GB, Chromium 145 und Firefox 146: IT05.5 `peakSpeed` 2,11–4,42 (Kriterium ≥ 1,2), siehe [report-p0-3-vm-matrix.md](report-p0-3-vm-matrix.md); **keine** Messung auf realen Schulgeräten, eine Messung auf dem schwächsten echten Gerät bleibt empfohlen |
| P0.4 | erledigt | #260 | Registerweg + Befehlsdekodierung: alle Paare pin-genau außer einem `digitalWrite` ohne Pegeländerung; Symbole nach LTO in 1/62 Builds |
| P0.5 | erledigt | #262 | ATTinyCore 1.5.2; Software-UART TX/RX inkl. Komparatormodell; avr8js-Fehler REFS2 |
| P0.6 | erledigt | #261 | Mega-Profil mit eigenen Vektoren; alle Proben stimmen |
@@ -20,54 +20,60 @@
| P1.6 | erledigt | #270 | Reglerwert → (Wert + 0,5)/1024 · VCC, `analogRead` = Reglerwert; INTERNAL nach Datenblatt, AREF = VCC (neue Modellannahme in 4.8). Emulator-Tests laufen seit #269 in eigenem Coverage-Durchlauf |
| P1.7 | erledigt | #271 | UTF-8-Stromdekoder je Port, Eingabe im Rahmentakt erst nach `Serial.begin`, 64-Byte-Puffer des Cores, Grenzen 4 KiB/1 MiB; SerialOutput-SSOT folgt mit dem Worker-Transport (P1.11/P1.12) |
| P1.8 | erledigt | #272 | `Emulator`: Scheiben ≤ 8 ms, Neusetzen bei > 100 ms Rückstand, Warten bei Vorauslauf, Echtzeitfaktor, Pause/Resume/Stop/Reset, Timeout ohne Pausen, Headless `advance`/`at`; Abnahme „30 s bei Faktor 0,5“ skaliert getestet (400 ms → 200 ms) |
-| P1.9 | erledigt | (dieser PR) | `IoUsageObserver` nach Spike P0.4, Stufe R2, je Maschine (auch nach Reset); IoRegistry-SSOT-Fälle 1–11 als Tests mit HEX-Fixtures (Fall 7: `digitalWrite` portgenau, mit ELF-Symbolen pin-genau); `analogWrite` 0/255 als `digitalWrite`; keine Wiederholung in `loop()` |
+| P1.9 | erledigt | #273 | `IoUsageObserver` nach Spike P0.4, Stufe R2, je Maschine (auch nach Reset); IoRegistry-SSOT-Fälle 1–11 als Tests mit HEX-Fixtures (Fall 7: `digitalWrite` portgenau, mit ELF-Symbolen pin-genau); `analogWrite` 0/255 als `digitalWrite`; keine Wiederholung in `loop()` |
| P1.10 | erledigt | #274 | `Diagnostics`: Stack in `.bss`/Heap, PC außerhalb des Programms, Watchdog-Reset (baut die Maschine neu auf), `sleep` bei gesperrten Interrupts; neue SSOT SimulationRuntime (Abschnitt Diagnosen); Prüfung je 4000-Takte-Abschnitt und bei SPL-Schreibzugriffen |
| P1.11 | erledigt | #275 | `protocol/`: Befehls- und Ereignistypen mit Version, `parseCommand`, `EventBatcher` (≈ 60 Gruppen/s, Telemetrie 1/s, begrenzter Speicher), `RunGate` (UI-Seite der `runId`); SSOT SimulationRuntime Abschnitt 2 |
| P1.12 | erledigt | #276 | `WorkerHost` + `installWorker`, Einstieg `@unosim/avr-emulator/worker`: Befehle prüfen und `runId`-Regeln, Bestätigungen, Heartbeat auch in der Pause, `fault` bei Board-Konflikt und ungültigem Abbild, Zustand nach Stop verworfen; `loadFlashImage`, `Emulator.configureTimeout` |
| P1.13 | erledigt | #277 | `runHeadless` (Reize, Zustandsprüfungen, Ergebnis), Werkzeug `npm run emulate` (ersetzt das Prototyp-Skript), Messbericht [report-p1-13-leistung.md](report-p1-13-leistung.md): 3,5–6,4 × Echtzeit auf M2 Pro; `Emulator` führt Aktionen zur Endzeit von `advance` aus |
| P1.14 | erledigt | #278, #279, #281 | Korpus mit 12 Proben und den 51 Beispielen von UnoSim-Examples v3.0.0 samt Reizskripten und Zustandsprüfungen (61 `derived`, zwei `regression`: `memory_values`, `it05-05`); Fund: avr8js-Fehler beim RXC-Flag (#280); Korpus in Shards im Unit-Gate, nicht im Coverage-Durchlauf, Neuerzeugung der HEX-Dateien im Toolchain-Gate |
-| P1.14 (Klasse `hardware`) | teilweise erledigt | (dieser PR) | Aufnahmen am echten Uno R3 (Original, USB 2341:0043): 44 von 64 Fällen jetzt Klasse `hardware` (alle Fälle ohne Verdrahtung außer `it05-05`, darunter das neue `sp_at_reset`); Funde: `it05-05` weicht reproduzierbar um 8 Schleifendurchläufe je Ausgabe ab, Bytes vor `Serial.begin()` gehen auf dem Board verloren (Emulator verwirft sie jetzt ebenso, Diagnose `serial-input-discarded`, Fall `serial_before_begin`); offen: 19 Fälle mit Pins, Spannungen oder Pinbeobachtung brauchen Messaufbau; siehe [report-p1-14-hardware.md](report-p1-14-hardware.md) |
-| (Nachtrag P1.12) | erledigt | (dieser PR) | `WorkerHost` behält den EEPROM-Inhalt je Board über Läufe desselben Workers (Modellannahme 4.8 „EEPROM bleibt im Tab je Board erhalten“, bis dahin nicht umgesetzt); Fixture `uno-eeprom_counter` |
+| P1.14 (Klasse `hardware`) | teilweise erledigt | #292 | Aufnahmen am echten Uno R3 (Original, USB 2341:0043): 44 von 64 Fällen jetzt Klasse `hardware` (alle Fälle ohne Verdrahtung außer `it05-05`, darunter das neue `sp_at_reset`); Funde: `it05-05` weicht reproduzierbar um 8 Schleifendurchläufe je Ausgabe ab, Bytes vor `Serial.begin()` gehen auf dem Board verloren (Emulator verwirft sie jetzt ebenso, Diagnose `serial-input-discarded`, Fall `serial_before_begin`); offen: 19 Fälle mit Pins, Spannungen oder Pinbeobachtung brauchen Messaufbau; siehe [report-p1-14-hardware.md](report-p1-14-hardware.md) |
+| (Nachtrag P1.12) | erledigt | #298 | `WorkerHost` behält den EEPROM-Inhalt je Board über Läufe desselben Workers (Modellannahme 4.8 „EEPROM bleibt im Tab je Board erhalten“, bis dahin nicht umgesetzt); Fixture `uno-eeprom_counter` |
| (Fund P1.14) | erledigt | #280 | Korpus fand einen avr8js-Fehler: Schreiben auf UCSR0B löscht ein anstehendes RXC, serielle Eingabe ging beim gleichzeitigen Drucken verloren; Korrektur `createUsart` im Profil, Probe `serial_read_while_printing`, README-Abschnitt „avr8js 0.21.1: corrections and known differences“ |
| P2.1 | erledigt | #282 | `shared/boards.ts`: Katalog der vier Boards (alle `planned`; der Uno seit M1 `available`), `boardIdSchema`, `enabledBoards`, `fqbnAllowlist`, `boardPinAliases`; Paket-Einstieg `@unosim/avr-emulator/boards` ohne avr8js; `GET /api/config` meldet Boards, Standard-Board, Backends (fest `host-mock`); SSOT Boards. Abweichung: Hardware im Paket gibt es erst für Uno und Nano (Mega P5.2, ATtiny P6.4) |
| P2.2 | erledigt | #283 | `board` im Compile-Request (`fqbn` veraltet, nur passend zum Board), Auflösung gegen die angebotenen Boards in der Route, FQBN-Prüfung im Prozess-Executor aus dem Katalog, Boardname aus dem Katalog in der Compile-Ausgabe, `board` in Compiler-Optionen und Worker-Auftrag, `CodeParser(…, board)`; Katalogname des Uno jetzt „Arduino UNO“ (wie die bestehende Ausgabe) |
-| P2.4 | erledigt (nach Merge der drei PRs) | (dieser PR) | Teil a: `Dockerfile.compile-sandbox` (`arduino-cli` 1.5.1, `arduino:avr@1.8.8`, vorkompilierte Cores für Uno/Nano/Mega, 737 MB), `npm run build:compile-sandbox`, `compileInSandbox` (Grenzen 4.10, Name `unosim-compile-`, Owner-Label, explizite Umgebung, Zeitlimit im Container und am Host, Aufräumen nach Abbruch), Ergebnis gerahmt über stdout statt beschreibbarem Mount; Vertragstest im Docker-Gate (10 Fälle). Gemessen: Uno-Compile 585 ms, Fehler-Compile 445 ms (M2 Pro, Docker Desktop). Teil b (#327 gestapelt, dieser PR): Auswahl je Profil (`compiler/toolchain.ts`), Startprüfung des Images im Docker-Profil, Artefakt im Docker-Profil, `browser` im Docker-Profil zulässig, Isolationsprüfung im `ProcessExecutor`, `check-leaks.sh` mit Compile-Containern, Deployment-Tests/Installer/Compose mit dem Image; SECURITY, ARCHITECTURE, INSTALL_SERVER, INSTALL_UBUNTU_VBOX, OPEN_POINTS (S1-ASM). Teil c (gestapelt auf Teil b, dieser PR): Backend-Image ohne `arduino-cli` und `g++` (1,62 GB → 726 MB), CI prüft die Lage der Toolchain |
+| P2.4 | erledigt | #327, #328, #329 | Teil a (#327): `Dockerfile.compile-sandbox` (`arduino-cli` 1.5.1, `arduino:avr@1.8.8`, vorkompilierte Cores für Uno/Nano/Mega, 737 MB), `npm run build:compile-sandbox`, `compileInSandbox` (Grenzen 4.10, Name `unosim-compile-`, Owner-Label, explizite Umgebung, Zeitlimit im Container und am Host, Aufräumen nach Abbruch), Ergebnis gerahmt über stdout statt beschreibbarem Mount; Vertragstest im Docker-Gate (10 Fälle). Gemessen: Uno-Compile 585 ms, Fehler-Compile 445 ms (M2 Pro, Docker Desktop). Teil b (#328): Auswahl je Profil (`compiler/toolchain.ts`), Startprüfung des Images im Docker-Profil, Artefakt im Docker-Profil, `browser` im Docker-Profil zulässig, Isolationsprüfung im `ProcessExecutor`, `check-leaks.sh` mit Compile-Containern, Deployment-Tests/Installer/Compose mit dem Image; SECURITY, ARCHITECTURE, INSTALL_SERVER, INSTALL_UBUNTU_VBOX, OPEN_POINTS (S1-ASM). Teil c (#329): Backend-Image ohne `arduino-cli` und `g++` (1,62 GB → 726 MB), CI prüft die Lage der Toolchain |
| P2.5 | erledigt | #284, #286, #288 | Teil a: Schema des Felds `artifact` (Abbild des Pakets + `buildId`), Build-Identität (E15), Toolchain-Information. Teil b: Artefakt-Bau im Compiler aus HEX und ELF, Sidecar `.artifact.json` in beiden Caches, Treffer nur bei gleicher Identität, Neubau sonst; `@unosim/avr-emulator/image` im Server- und Worker-Bundle. Fund: der Bibliotheks-Fingerabdruck des Workers las das alte Ausgabeformat. Teil c: `POST /api/compile` liefert `artifact` bei Erfolg im lokalen Profil, nie im Docker-Profil (bis P2.4), nie `binary`; SECURITY.md. REST-Version `1.1.0` (E14) freigegeben und angehoben, `1.0.0` bleibt akzeptiert (Zeile in `protocol-version.test.ts` bewusst angepasst) |
| P2.6 | erledigt | #287 | `server/runtime-settings.ts` (`UNOSIM_RUNTIME_BACKENDS`, `UNOSIM_DEFAULT_RUNTIME`, `UNOSIM_BOARDS`, `UNOSIM_DEFAULT_BOARD`, `ARDUINO_FQBN` als Alias, Startprüfungen inkl. „`browser` im Docker-Profil nur mit Compile-Sandbox“), `/api/config`, Compile-Route und `config.compilation.fqbn` aus den Einstellungen; Runner-Pool nur mit `host-mock`; `start_simulation` ohne Host-Mock → `operation_error` `RUNTIME_UNAVAILABLE`; `/api/readiness` bereit, wenn alle Backends bereit sind; `/api/status` nennt die Backends. Vorauswahl bleibt `host-mock` bis P3.10 |
-| P2.7 | erledigt | (dieser PR) | Tutor-Anfragen (`question`, `dialog`) tragen optional `board`; der deterministische Kontext nennt Board, MCU und Datentypgrößen (AVR-ABI), die Systemvorgabe verweist darauf; ohne Board bleibt der Kontext unverändert; `npm run test:tutor-quality` grün (502 Tests); LearningQuestions-SSOT 4.2 |
-| P3.1 | erledigt | (dieser PR) | `client/src/lib/runtime/`: Schnittstelle `SimulationRuntime` (`StartRequest`, `RuntimeFeatures`, `RuntimeEvent`), Auswahlregel `selectRuntime` (Nutzerwahl → Server-Default → erste nutzbare), `parseRuntimeConfig`/`loadRuntimeConfig` für `/api/config` mit Rückfall auf Uno + Host-Mock; Hook `useSimulationRuntimeSelection`; Einstellung „Simulation engine“ (Emulation/Legacy) in den Settings, nur sichtbar, wenn mehr als eine Laufzeit nutzbar ist; die Wahl gilt ab dem nächsten Start |
-| P3.2 | erledigt | (dieser PR) | `BrowserRuntime`: Worker-Steuerung mit `runId` (`RunGate`), Start erst nach `ready`, Zustände mit Bestätigung, Ereignisse als WebSocket-Nachrichten (Pins, Seriell, Status) plus Browser-Ereignisse für Funde/Diagnosen/Telemetrie, Systemzeilen wie der Server, Überwachung (Fristen, Heartbeat, `error`/`messageerror`, `terminate()`); `SimulationRuntime.setAnalog`; neuer Einstieg `@unosim/avr-emulator/protocol` ohne avr8js. Worker-Datei und Fabrik folgen mit P3.4 (sonst ungenutzter Code) |
-| P3.4 | erledigt | (dieser PR) | `RuntimeHub`, `createBrowserRuntime` und Worker-Datei (`emulator.worker.ts`, eigener Chunk); Befehls-Router an der Grenze der Controller (alle Aufrufstellen: Controller, Lifecycle, externe API, Serial- und Pin-Bedienung), Start mit `artifact` aus dem Compile-Ergebnis, Rückfall auf den Host-Mock ohne Abbild, Reset ohne Neukompilieren, Stop/Pause/Fortsetzen ohne Verbindungsprüfung bei Browser-Läufen, `WebSocketManager.injectMessage`; Host-Mock-Weg unverändert (alle bestehenden Tests grün). Bedienelemente und Verbindungsverhalten (`canSimulate` je Backend) folgen mit P3.5 |
-| P3.7 | erledigt | (dieser PR) | `mergeIoRegistry`: statische Einträge und Zeilen bleiben im Lauf, Laufzeitfunde ergänzen je Pin und Funktion mit Herkunft (`origin`: static/runtime/both), Konflikt bei widersprechendem pinMode; `BrowserRuntime` baut aus den `usage`-Ereignissen den `io_registry`-Snapshot (A0–A5 als Namen); Registry-Tabelle zeigt „+ runtime“; IoRegistry-SSOT ergänzt, `io-registry-precedence.test.ts` auf den neuen Vorrang umgestellt (Liste 7.3) |
-| (Nachtrag P3.10) | erledigt | (dieser PR) | Debug-Tab nannte bei Browser-Läufen `start_simulation`/`stop_simulation` über WebSocket, obwohl der Lauf im Worker ging (Fund beim Ausprobieren): Befehle eines Browser-Laufs erscheinen jetzt als `browser: ` mit Protokoll `WORKER` |
-| (Nachtrag P3.6) | erledigt | (dieser PR) | Debug-Tab: Herkunft `BROWSER` (cyan, Protokoll `WORKER`) für die Laufzeitereignisse eines Browser-Laufs statt `SERVER`; Debug-Leiste oben rechts nennt immer die Laufzeitumgebung (`ENGINE: BROWSER` bzw. `ENGINE: LEGACY`, im Leerlauf die für den nächsten Start gewählte); Wunsch aus dem ersten Ausprobieren |
-| P3.5 | erledigt | (dieser PR) | Browser-Lauf überlebt Verbindungsverlust: `RuntimeHub.browserRunActive`, `getServerCapabilities(backendReachable, browserRunActive)` behält Stop/Pause/Seriell/Pins, der Handler beendet den Lauf bei Verbindungsabbruch nur für Host-Mock-Läufe; Compile/Beispiele/Tutor unverändert offline |
-| P3.6 | erledigt | (dieser PR) | Debug-Leiste: `ENGINE: BROWSER` und Echtzeitfaktor statt Runner-Index (`useBrowserRunInfo`), Diagnosen als Systemzeilen (`diagnostic-lines.ts`) plus strukturiertes Ereignis |
-| P3.8 | erledigt | (dieser PR) | DEV-Hook `__SET_LAST_COMPILED_ARTIFACT__`; die Laufzeit für E2E wählt der localStorage-Schlüssel der Nutzereinstellung (P3.1) vor |
-| P4.1 | erledigt | (dieser PR) | `shared/board-pins.ts`: Pinlisten, Labels, Fähigkeiten (digital/analog/PWM), Pinprüfung `isBoardPin`, `resolveBoardPin`, `boardPinLabel` je Board aus der Pinbelegung des Pakets (Nano mit A6/A7 als reine Analogeingänge); `UNO_PWM_PINS` daraus abgeleitet; `maxPin: 19` des Host-Mocks unverändert |
-| P4.5 | erledigt | (dieser PR) | Tools → Board: Untermenü mit den angebotenen Boards (`planned` sichtbar, nicht wählbar; `preview` mit Hinweis), Auswahl im localStorage (`unoSelectedBoard`, Zugriffe abgesichert) und über `useSimulationRuntimeSelection`; `/api/config` liefert `status` je Board an den Client; Compile-Request und Start tragen das Board; ein Wechsel stoppt den Lauf und verwirft das Artefakt (`discardBuild`) |
-| P4.2 | teilweise erledigt | (dieser PR) | Parser je Board: `PinContext` (`shared/parsers/pin-context.ts`), `analyzeStaticIO*`/`parseStaticIORegistry*` mit `{ board }`, `CodeParser` reicht sein Board durch; Nano: A6/A7 als Pins 20/21, `analogRead(6/7)`, Pinnamen in den Zeilen, PWM-Warnung nennt das Board, neue Warnung für digitale Nutzung von A6/A7; Uno-Ergebnisse unverändert (bestehende Tests). `Serial1`–`Serial3` je Board mit P5.4 erledigt. **Offen bis P6.4:** „kein Hardware-Serial“ (ATtiny85) |
-| P4.3 | teilweise erledigt | (dieser PR) | Client-Zustand je Board: `client/src/lib/active-board.ts` (Board aus Tools → Board; Uno bis zur Wahl), Pin-Typ im Store, `pinToNumber`, Sketch-Analyse und statische Registry mit Board, Registry-Tabelle „alle Pins“ (20 Zeilen Uno, 22 Nano), PWM-/Analog-Beschriftungen der Polling-Engine, Pinnamen im Laufzeit-Snapshot; Pin-Überlagerung des Board-SVG siehe P4.4 |
-| P4.4 | teilweise erledigt | (dieser PR) | `ArduinoBoard` zeichnet das gewählte Board: `client/src/lib/board-view.ts` (Grafiken und Größe je Board), Slider, Klickflächen, Beschriftungen aus dem Pin-Modell; Uno-SVG unverändert; Nano-Grafiken (`ArduinoNano.svg`, `ArduinoNano-overlay.svg`) selbst gezeichnet und reproduzierbar mit `npm run generate:nano-svg` erzeugt (gleiche ID-Konvention wie der Uno, A0–A7); die Komponente läuft nicht in jsdom (hängt schon vorher), ihre Abnahme steht im Nano-E2E (P4.8). **Offen:** ATtiny-Grafik (P6.5), Mobile-Layout je Board prüfen (Mega-Grafik: P5.4) |
-| P4.7 | erledigt | (dieser PR) | iframe-API `1.5.0` (additiv): `GET_STATE` mit Board, Laufzeit und Zustand; `SET_PIN_STATE`/`GET_PIN_STATE`/`BATCH_SET_PIN_STATE` prüfen den Pin gegen das gewählte Board (Batch alles oder nichts); `docs/EXTERNAL_API.md`; ohne Board-Angabe unverändert |
-| P4.8 | erledigt | (dieser PR) | Nano `preview`: Board-Hardware und A6/A7-Proben aus P1.3, Grafiken aus P4.4, E2E-Smoke `e2e/nano.spec.ts` (Tools → Board, Compile für den Nano, Lauf im Browser mit A6/A7 und einem am Board gesetzten Analogwert, Boardwechsel stoppt den Lauf). Ohne `UNOSIM_BOARDS` bietet der Server nur Boards an, die ein angebotenes Backend ausführt: das Docker-Profil (ohne Browser-Backend) bleibt beim Uno, bei ausdrücklicher Liste mit dem Nano startet es nicht |
-| P4.6 | teilweise erledigt | (dieser PR) | Manifestfeld `board` je Beispiel (optional, unbekannte IDs zählen als nicht angegeben), im Katalog; Beispielmenü fragt bei einem anderen als dem gewählten Board nach (Wechseln / trotzdem laden / abbrechen); Boardwechsel stoppt den Lauf und verwirft das Build jetzt auch beim Wechsel aus dem Beispielmenü (Effekt in der Seite); SSOTs ExternalExamples 7.1 und CourseContent. **Offen (anderes Repository, nicht angefasst):** README von UnoSim-Examples mit dem Feld `board` |
-| P3.10 | erledigt (mit ausgewiesenen Ausnahmen) | (dieser PR) | M1-Abnahme: lokaler Standard `browser`, E2E-Suite der Browser-Emulation (7 Szenarien, echter Compile), Benutzerhandbuch „Grenzen der Simulation“ neu, Funde der Abnahme behoben (Eingabefeld, Offline-Steuerung, Compile-Status, Serial-Format); **Hardware-Referenzen: 34 von 51 Beispielen `hardware`, 17 ausgewiesene Ausnahmen** (Messaufbau fehlt), siehe [report-m1-acceptance.md](report-m1-acceptance.md) |
-| P5.1 | erledigt | (dieser PR) | `mcu/atmega2560.ts`: Ports A–L mit INT0–INT7 und PCINT0–2, Timer 0–5, USART0–3, ADC mit 16 Kanälen (MUX5, Referenzen AREF/AVCC/1,1 V/2,56 V), EEPROM 4 KiB, Watchdog, jeweils mit den Vektoren des Mega; Datenraum bis `0x21FF`. Fund beim Test: die Port-Konfigurationen B/C/D von avr8js tragen die Pin-Change-Verdrahtung des 328P (Mega-Vektoren von INT3/INT4 wurden dadurch ausgelöst); das Profil übernimmt nur die Registeradressen. Probe-Fixtures `mega2560-mega_probe/_interrupts/_io`; `io-usage` löst `analogRead(54…69)` und Kanäle > 7 auf |
-| P5.2 | erledigt | (dieser PR) | `MEGA2560` (Pins 0–69, A0–A15 = 54–69, PWM 2–13 und 44–46, vier serielle Ports, LED 13), Pinbelegung im Toolchain-Gate gegen `pins_arduino.h` des Cores geprüft (Port, Bitmaske, Timer, A-Pins); die Tests, die den Mega als „Board ohne Hardwareprofil“ nutzten, nutzen jetzt den ATtiny85 |
-| P5.3 | erledigt | (dieser PR) | `serial_output`/`serial_input` tragen das optionale Feld `port` (0–3; WebSocket-Vertrag additiv, der Host-Mock verwirft Port ≠ 0); `BrowserRuntime` meldet `Serial1`–`Serial3` mit Port und hält ein wartendes CR je Port; `useSerialIO` hält den Text je Port (Port 0 unverändert mit Zeilenliste für den Plotter und Baudraten-Renderer), Systemzeilen erscheinen in jedem Port; Serial Monitor mit Portauswahl (nur bei Boards mit mehr als einer Schnittstelle), Eingabe und „Clear“ wirken auf den angezeigten Port; iframe-API (noch 1.5.0, nicht ausgeliefert): `SERIAL_INPUT.payload.port`, `SERIAL_OUTPUT_EVENT.port`, `GET_STATE.serialPorts`; Router: Analogwerte auf allen Analogeingängen des gewählten Boards (bisher nur Pins 14–19, damit auch A6/A7 des Nano); Fixture `mega2560-mega_serial_ports` (Worker-Host-Tests je Port) |
+| P2.7 | erledigt | #289 | Tutor-Anfragen (`question`, `dialog`) tragen optional `board`; der deterministische Kontext nennt Board, MCU und Datentypgrößen (AVR-ABI), die Systemvorgabe verweist darauf; ohne Board bleibt der Kontext unverändert; `npm run test:tutor-quality` grün (502 Tests); LearningQuestions-SSOT 4.2 |
+| P3.1 | erledigt | #290 | `client/src/lib/runtime/`: Schnittstelle `SimulationRuntime` (`StartRequest`, `RuntimeFeatures`, `RuntimeEvent`), Auswahlregel `selectRuntime` (Nutzerwahl → Server-Default → erste nutzbare), `parseRuntimeConfig`/`loadRuntimeConfig` für `/api/config` mit Rückfall auf Uno + Host-Mock; Hook `useSimulationRuntimeSelection`; Einstellung „Simulation engine“ (Emulation/Legacy) in den Settings, nur sichtbar, wenn mehr als eine Laufzeit nutzbar ist; die Wahl gilt ab dem nächsten Start |
+| P3.2 | erledigt | #295 | `BrowserRuntime`: Worker-Steuerung mit `runId` (`RunGate`), Start erst nach `ready`, Zustände mit Bestätigung, Ereignisse als WebSocket-Nachrichten (Pins, Seriell, Status) plus Browser-Ereignisse für Funde/Diagnosen/Telemetrie, Systemzeilen wie der Server, Überwachung (Fristen, Heartbeat, `error`/`messageerror`, `terminate()`); `SimulationRuntime.setAnalog`; neuer Einstieg `@unosim/avr-emulator/protocol` ohne avr8js. Worker-Datei und Fabrik folgen mit P3.4 (sonst ungenutzter Code) |
+| P3.4 | erledigt | #296 | `RuntimeHub`, `createBrowserRuntime` und Worker-Datei (`emulator.worker.ts`, eigener Chunk); Befehls-Router an der Grenze der Controller (alle Aufrufstellen: Controller, Lifecycle, externe API, Serial- und Pin-Bedienung), Start mit `artifact` aus dem Compile-Ergebnis, Rückfall auf den Host-Mock ohne Abbild, Reset ohne Neukompilieren, Stop/Pause/Fortsetzen ohne Verbindungsprüfung bei Browser-Läufen, `WebSocketManager.injectMessage`; Host-Mock-Weg unverändert (alle bestehenden Tests grün). Bedienelemente und Verbindungsverhalten (`canSimulate` je Backend) folgen mit P3.5 |
+| P3.7 | erledigt | #297 | `mergeIoRegistry`: statische Einträge und Zeilen bleiben im Lauf, Laufzeitfunde ergänzen je Pin und Funktion mit Herkunft (`origin`: static/runtime/both), Konflikt bei widersprechendem pinMode; `BrowserRuntime` baut aus den `usage`-Ereignissen den `io_registry`-Snapshot (A0–A5 als Namen); Registry-Tabelle zeigt „+ runtime“; IoRegistry-SSOT ergänzt, `io-registry-precedence.test.ts` auf den neuen Vorrang umgestellt (Liste 7.3) |
+| (Nachtrag P3.10) | erledigt | #301 | Debug-Tab nannte bei Browser-Läufen `start_simulation`/`stop_simulation` über WebSocket, obwohl der Lauf im Worker ging (Fund beim Ausprobieren): Befehle eines Browser-Laufs erscheinen jetzt als `browser: ` mit Protokoll `WORKER` |
+| (Nachtrag P3.6) | erledigt | #302 | Debug-Tab: Herkunft `BROWSER` (cyan, Protokoll `WORKER`) für die Laufzeitereignisse eines Browser-Laufs statt `SERVER`; Debug-Leiste oben rechts nennt immer die Laufzeitumgebung (`ENGINE: BROWSER` bzw. `ENGINE: LEGACY`, im Leerlauf die für den nächsten Start gewählte); Wunsch aus dem ersten Ausprobieren |
+| P3.5 | erledigt | #297 | Browser-Lauf überlebt Verbindungsverlust: `RuntimeHub.browserRunActive`, `getServerCapabilities(backendReachable, browserRunActive)` behält Stop/Pause/Seriell/Pins, der Handler beendet den Lauf bei Verbindungsabbruch nur für Host-Mock-Läufe; Compile/Beispiele/Tutor unverändert offline |
+| P3.6 | erledigt | #297 | Debug-Leiste: `ENGINE: BROWSER` und Echtzeitfaktor statt Runner-Index (`useBrowserRunInfo`), Diagnosen als Systemzeilen (`diagnostic-lines.ts`) plus strukturiertes Ereignis |
+| P3.8 | erledigt | #297 | DEV-Hook `__SET_LAST_COMPILED_ARTIFACT__`; die Laufzeit für E2E wählt der localStorage-Schlüssel der Nutzereinstellung (P3.1) vor |
+| P4.1 | erledigt | #303 | `shared/board-pins.ts`: Pinlisten, Labels, Fähigkeiten (digital/analog/PWM), Pinprüfung `isBoardPin`, `resolveBoardPin`, `boardPinLabel` je Board aus der Pinbelegung des Pakets (Nano mit A6/A7 als reine Analogeingänge); `UNO_PWM_PINS` daraus abgeleitet; `maxPin: 19` des Host-Mocks unverändert |
+| P4.5 | erledigt | #305 | Tools → Board: Untermenü mit den angebotenen Boards (`planned` sichtbar, nicht wählbar; `preview` mit Hinweis), Auswahl im localStorage (`unoSelectedBoard`, Zugriffe abgesichert) und über `useSimulationRuntimeSelection`; `/api/config` liefert `status` je Board an den Client; Compile-Request und Start tragen das Board; ein Wechsel stoppt den Lauf und verwirft das Artefakt (`discardBuild`) |
+| P4.2 | erledigt | #304 | Parser je Board: `PinContext` (`shared/parsers/pin-context.ts`), `analyzeStaticIO*`/`parseStaticIORegistry*` mit `{ board }`, `CodeParser` reicht sein Board durch; Nano: A6/A7 als Pins 20/21, `analogRead(6/7)`, Pinnamen in den Zeilen, PWM-Warnung nennt das Board, neue Warnung für digitale Nutzung von A6/A7; Uno-Ergebnisse unverändert (bestehende Tests). `Serial1`–`Serial3` je Board mit P5.4 erledigt. ATtiny85-Regeln (kein `Serial1`–`Serial3`, Serial-Pins, RESET-Pin) mit P6.4 (#336) |
+| P4.3 | teilweise erledigt | #306 | Client-Zustand je Board: `client/src/lib/active-board.ts` (Board aus Tools → Board; Uno bis zur Wahl), Pin-Typ im Store, `pinToNumber`, Sketch-Analyse und statische Registry mit Board, Registry-Tabelle „alle Pins“ (20 Zeilen Uno, 22 Nano), PWM-/Analog-Beschriftungen der Polling-Engine, Pinnamen im Laufzeit-Snapshot; Pin-Überlagerung des Board-SVG siehe P4.4 |
+| P4.4 | erledigt | #306 | `ArduinoBoard` zeichnet das gewählte Board: `client/src/lib/board-view.ts` (Grafiken und Größe je Board), Slider, Klickflächen, Beschriftungen aus dem Pin-Modell; Uno-SVG unverändert; Nano-Grafiken (`ArduinoNano.svg`, `ArduinoNano-overlay.svg`) selbst gezeichnet und reproduzierbar mit `npm run generate:nano-svg` erzeugt (gleiche ID-Konvention wie der Uno, A0–A7); die Komponente läuft nicht in jsdom (hängt schon vorher), ihre Abnahme steht im Nano-E2E (P4.8). ATtiny-Grafik mit P6.5, Mobile-Layout mit dem Rest P4.4 erledigt |
+| P4.7 | erledigt | #306 | iframe-API `1.5.0` (additiv): `GET_STATE` mit Board, Laufzeit und Zustand; `SET_PIN_STATE`/`GET_PIN_STATE`/`BATCH_SET_PIN_STATE` prüfen den Pin gegen das gewählte Board (Batch alles oder nichts); `docs/EXTERNAL_API.md`; ohne Board-Angabe unverändert |
+| P4.8 | erledigt | #307 | Nano `preview`: Board-Hardware und A6/A7-Proben aus P1.3, Grafiken aus P4.4, E2E-Smoke `e2e/nano.spec.ts` (Tools → Board, Compile für den Nano, Lauf im Browser mit A6/A7 und einem am Board gesetzten Analogwert, Boardwechsel stoppt den Lauf). Ohne `UNOSIM_BOARDS` bietet der Server nur Boards an, die ein angebotenes Backend ausführt: das Docker-Profil (ohne Browser-Backend) bleibt beim Uno, bei ausdrücklicher Liste mit dem Nano startet es nicht |
+| P4.6 | teilweise erledigt | #308 | Manifestfeld `board` je Beispiel (optional, unbekannte IDs zählen als nicht angegeben), im Katalog; Beispielmenü fragt bei einem anderen als dem gewählten Board nach (Wechseln / trotzdem laden / abbrechen); Boardwechsel stoppt den Lauf und verwirft das Build jetzt auch beim Wechsel aus dem Beispielmenü (Effekt in der Seite); SSOTs ExternalExamples 7.1 und CourseContent. **Offen (anderes Repository, nicht angefasst):** README von UnoSim-Examples mit dem Feld `board` |
+| P3.10 | erledigt (mit ausgewiesenen Ausnahmen) | #299 | M1-Abnahme: lokaler Standard `browser`, E2E-Suite der Browser-Emulation (7 Szenarien, echter Compile), Benutzerhandbuch „Grenzen der Simulation“ neu, Funde der Abnahme behoben (Eingabefeld, Offline-Steuerung, Compile-Status, Serial-Format); **Hardware-Referenzen: 34 von 51 Beispielen `hardware`, 17 ausgewiesene Ausnahmen** (Messaufbau fehlt), siehe [report-m1-acceptance.md](report-m1-acceptance.md) |
+| P5.1 | erledigt | #312 | `mcu/atmega2560.ts`: Ports A–L mit INT0–INT7 und PCINT0–2, Timer 0–5, USART0–3, ADC mit 16 Kanälen (MUX5, Referenzen AREF/AVCC/1,1 V/2,56 V), EEPROM 4 KiB, Watchdog, jeweils mit den Vektoren des Mega; Datenraum bis `0x21FF`. Fund beim Test: die Port-Konfigurationen B/C/D von avr8js tragen die Pin-Change-Verdrahtung des 328P (Mega-Vektoren von INT3/INT4 wurden dadurch ausgelöst); das Profil übernimmt nur die Registeradressen. Probe-Fixtures `mega2560-mega_probe/_interrupts/_io`; `io-usage` löst `analogRead(54…69)` und Kanäle > 7 auf |
+| P5.2 | erledigt | #312 | `MEGA2560` (Pins 0–69, A0–A15 = 54–69, PWM 2–13 und 44–46, vier serielle Ports, LED 13), Pinbelegung im Toolchain-Gate gegen `pins_arduino.h` des Cores geprüft (Port, Bitmaske, Timer, A-Pins); die Tests, die den Mega als „Board ohne Hardwareprofil“ nutzten, nutzen jetzt den ATtiny85 |
+| P5.3 | erledigt | #313 | `serial_output`/`serial_input` tragen das optionale Feld `port` (0–3; WebSocket-Vertrag additiv, der Host-Mock verwirft Port ≠ 0); `BrowserRuntime` meldet `Serial1`–`Serial3` mit Port und hält ein wartendes CR je Port; `useSerialIO` hält den Text je Port (Port 0 unverändert mit Zeilenliste für den Plotter und Baudraten-Renderer), Systemzeilen erscheinen in jedem Port; Serial Monitor mit Portauswahl (nur bei Boards mit mehr als einer Schnittstelle), Eingabe und „Clear“ wirken auf den angezeigten Port; iframe-API (noch 1.5.0, nicht ausgeliefert): `SERIAL_INPUT.payload.port`, `SERIAL_OUTPUT_EVENT.port`, `GET_STATE.serialPorts`; Router: Analogwerte auf allen Analogeingängen des gewählten Boards (bisher nur Pins 14–19, damit auch A6/A7 des Nano); Fixture `mega2560-mega_serial_ports` (Worker-Host-Tests je Port) |
| P5.4 | erledigt | #314, #315, #318, #319 | Mega `preview`: Grafiken (`ArduinoMega.svg`/`-overlay.svg`, `npm run generate:mega-svg`, 70 Pins, Logo-Platz je Board), `usePinPollingEngine` und Analog-Dialog je Board (A0–A15, Pins 0–53; bisher fest Uno), Parser (`Serial1`–`Serial3` je Board), Korpus: 7 Mega-Proben (Speicher, PWM, ADC mit MUX5, vier serielle Ports, Interrupts, Pins auf H/J/K/L, Flash über 64 KB und Code über 128 KB) und 14 Beispiele als `examples-v3.0.0-mega` (alle `derived`, Fund: `it08-05` gibt A0 als Pin 54 aus), E2E `e2e/mega.spec.ts` (echter Compile, Lauf im Browser, Serial/Serial1, A15 am Board gesetzt), Toolchain-Gate: Compile-Artefakt für den Mega, Benutzerhandbuch „Board wählen“; Leistung 3,5–4,6 × (Uno 3,6–5,2 ×), Nano-Korpus-Auszug (6 Beispiele, #318), Portleiste des Serial Monitors (#319); Abnahmebericht [report-m2-acceptance.md](report-m2-acceptance.md) |
-| M2 | erledigt (mit ausgewiesenen Ausnahmen) | (dieser PR) | Nano und Mega 2560 als `preview` freigegeben, Proben, Korpus-Auszug und E2E je Board grün; **keine Aufnahme an echter Nano-/Mega-Hardware** (28 Fälle `derived`), Bundle-Reserve 1,8 kB, Mobile-Layout je Board ungeprüft, siehe [report-m2-acceptance.md](report-m2-acceptance.md) |
-| P3.9 | erledigt | (dieser PR) | Browser-Laufzeit und Artefakt-Schema laden erst beim ersten Browser-Start (`import()`), der Emulator liegt nur im Worker-Chunk; `scripts/check-bundle-budget.mjs` prüft, dass avr8js (zwei Enum-Namen als Marker) in keinem Chunk des initialen Ladepfads steht und in mindestens einem Chunk vorkommt; Grenzwert 710 kB; seit P4.2 gilt er für den ganzen initialen Ladepfad (alle statisch geladenen Chunks) statt nur für den Index-Chunk, damit Aufteilen keine Bytes verbirgt; die Tabellen „Messages“ und „I/O Registry“ laden mit ihrem Tab nach (initialer Pfad 703,1 kB) |
-| P3.3 | erledigt | (dieser PR) | `HostMockRuntime` / `createHostMockRuntime`: Host-Mock hinter `SimulationRuntime`, sendet dieselben WebSocket-Nachrichten wie die Controller (Start mit Timeout, Code, Headern, Entry-File unbuffered; Pause/Resume/Seriell/Pin gepuffert, Stop unbuffered) und reicht die Laufereignisse unverändert weiter; noch nicht in die Controller eingehängt (P3.4), alle bestehenden Tests unverändert grün |
-| Nachtrag Serial-Flut | erledigt | (dieser PR) | Ein Sketch ohne `delay()` (`Serial.println(millis())`) ließ den Browser nach Sekunden abstürzen: der Text des Serial-Monitors wuchs unbegrenzt und wurde bei jedem Zeichen kopiert und zerlegt. `client/src/utils/serial-text-limit.ts` behält die letzten 40 000 Zeichen (ab Zeilenanfang); Messung mit Chromium: Bildrate bleibt bei 121 fps (vorher auf ~50 fps), Heap unter ca. 65 MB (vorher bis 185 MB und steigend). Das 60-s-Zeitlimit des Laufs gilt unverändert |
-| Nachtrag Editor-Eingabe | erledigt | (dieser PR) | Enter im Editor setzte die nächste Zeile ohne Einrückung fort: `autoIndent` stand auf `none` und die Sprache `arduino-cpp` hatte keine Sprachkonfiguration. Jetzt `autoIndent: "full"`, 2 Leerzeichen, und `client/src/lib/editor/arduino-language-configuration.ts` (Klammern, automatisches Schließen von `{ [ ( " '` und `/*`, Kommentar-Fortsetzung; Einrückung der nächsten Zeile nach `if`/`else`/`for`/`while`/`do` ohne Klammer); „Format Code“ als `client/src/lib/editor/format-code.ts` herausgelöst. Tests: `e2e/editor-typing.spec.ts` (echtes Monaco, Tastatureingabe: Einrückung nach `{`, verschachtelt, nach Kommentar, mitten in der Zeile, Tab, Klammern/Anführungszeichen, `setValue` unverändert), `tests/client/editor-format-code.test.ts`. **Offen:** `case`-Marken (die Einrückungsregeln von Monaco können erste und spätere Marke nicht unterscheiden), `else` zu einem verschachtelten klammerlosen `if`, „Format Code“ rückt klammerlose Anweisungen nicht ein |
-| Bundle-Reserve (vor P6.5) | erledigt | (dieser PR) | Die Boardansicht (`ArduinoBoard` samt `usePinPollingEngine`) lädt mit eigenem Chunk aus `PinMonitorView` (`lazy`, Platzhalter gleicher Größe); die Grafiken lagen schon als SVG in `public/`. Initialer Ladepfad 708,2 → 689,1 kB (`npm run check:bundle`), Reserve 20,9 kB bei unverändertem Limit 710 kB; E2E 51/51 grün, visuelle Snapshots unverändert |
-| P7.4 | erledigt | (dieser PR) | ARCHITECTURE „Host-Mock als Rückfallebene“: Rückfallwege für Betreiber und Nutzer, Kriterien für den Abbau (Plan 9.4: ein Semester ohne Rückfall, Korpus stabil grün; frühestens nach `v2.0.0`, eigene ADR), Umfang des Abbaus mit Pfaden, was bleibt; OPEN_POINTS `HOSTMOCK-REMOVAL` (DEFERRED); „Runtime-Profile“ an Browser-Emulation und Compile-Sandbox angepasst. Die V1-Rückkehr (9.3) prüft P7.2 |
-| P7.2 | erledigt | (dieser PR) | Compose reicht Laufzeit- und Board-Variablen aus `.env` durch, leere Werte gelten als nicht gesetzt (`runtime-settings.ts`); Gateway-Deployment-Test in der Pilot-Konfiguration (`/api/config` mit `browser` als Standard, Nano-Compile mit gültigem Artefakt über das Gateway), Installer-Test mit dem Standard; INSTALL_SERVER (Compile-Sandbox, Pilot, Readiness, Rollback), INSTALL_UBUNTU_VBOX (Pilot einschalten), Release-Runbook (Images, Docker-Gate mit Compile-Vertrag, Deployment-Gate, Rückfall); Rückkehr V2 → v1.0.1 → V2 auf derselben `.env` und demselben `storage/` geprüft, siehe [report-p7-2-v1-rollback.md](report-p7-2-v1-rollback.md). Standard im Docker-Profil bleibt `host-mock` bis M4 |
-| P7.1 | erledigt bis auf die Messung am Referenz-Host (wartet auf Mensch) | (dieser PR) | Compile-Phase in `npm run capacity:calibrate` (vier Schritte, Tabelle im Bericht, `--skip-compile`), Compile-Container tragen im Testprofil das Label des Kapazitätslaufs; CAPACITY_MODEL „Compile capacity (browser emulation)“: Modell, Kenngrößen nach 4.12, Messung auf M2 Pro (30 gleichzeitige neue Compiles p95 6,3 s, ≈ 275 Compiles/min, unverändertes Beispiel 8 ms); Messung auf der Referenz-VM: [human-tests.md, Teil 3](human-tests.md) |
-| P2.3 | erledigt | (dieser PR) | ATTinyCore 1.5.2 gepinnt (Version, HTTPS-Archiv, SHA-256), Installation ohne Board-Manager ins Sketchbook mit `platform.local.txt` gegen die Post-Build-Hooks (Fund: sie schreiben in Core und Sketch; HEX identisch mit und ohne), Prüfskript und Canary-Test; Compile-Sandbox-Image mit ATTinyCore und vorkompiliertem Core je FQBN; CI (Toolchain- und E2E-Job, Cache-Schlüssel, Deployment-Prüfschritt); Toolchain-Gate `toolchain-cores.test.ts` (alle vier Boards); INSTALL_LOCAL, README (Lizenzen LGPL 2.1). Abweichung: kein Board-Manager, weil `drazzy.com` und `azduino.com` abgelaufene Zertifikate liefern |
-| P6.1 | erledigt | (dieser PR, mit P2.3) | ATTinyCore im Compile-Sandbox-Image; Vertragstest kompiliert für den ATtiny85 in der Sandbox |
-| Anleitung | bereit | (dieser PR) | [human-tests.md](human-tests.md): P0.3 mit `npm run spike:p03` (Messseite für Browser auf Geräten), P1.14-Aufnahmen mit `npm run record:hardware`, P7.1 Compile-Kapazität auf dem Referenz-Host |
+| M2 | erledigt (mit ausgewiesenen Ausnahmen) | #320 | Nano und Mega 2560 als `preview` freigegeben, Proben, Korpus-Auszug und E2E je Board grün; **keine Aufnahme an echter Nano-/Mega-Hardware** (28 Fälle `derived`), Bundle-Reserve 1,8 kB, Mobile-Layout je Board ungeprüft, siehe [report-m2-acceptance.md](report-m2-acceptance.md) |
+| P3.9 | erledigt | #296 | Browser-Laufzeit und Artefakt-Schema laden erst beim ersten Browser-Start (`import()`), der Emulator liegt nur im Worker-Chunk; `scripts/check-bundle-budget.mjs` prüft, dass avr8js (zwei Enum-Namen als Marker) in keinem Chunk des initialen Ladepfads steht und in mindestens einem Chunk vorkommt; Grenzwert 710 kB; seit P4.2 gilt er für den ganzen initialen Ladepfad (alle statisch geladenen Chunks) statt nur für den Index-Chunk, damit Aufteilen keine Bytes verbirgt; die Tabellen „Messages“ und „I/O Registry“ laden mit ihrem Tab nach (initialer Pfad 703,1 kB) |
+| P3.3 | erledigt | #290 | `HostMockRuntime` / `createHostMockRuntime`: Host-Mock hinter `SimulationRuntime`, sendet dieselben WebSocket-Nachrichten wie die Controller (Start mit Timeout, Code, Headern, Entry-File unbuffered; Pause/Resume/Seriell/Pin gepuffert, Stop unbuffered) und reicht die Laufereignisse unverändert weiter; noch nicht in die Controller eingehängt (P3.4), alle bestehenden Tests unverändert grün |
+| Nachtrag Serial-Flut | erledigt | #309 | Ein Sketch ohne `delay()` (`Serial.println(millis())`) ließ den Browser nach Sekunden abstürzen: der Text des Serial-Monitors wuchs unbegrenzt und wurde bei jedem Zeichen kopiert und zerlegt. `client/src/utils/serial-text-limit.ts` behält die letzten 40 000 Zeichen (ab Zeilenanfang); Messung mit Chromium: Bildrate bleibt bei 121 fps (vorher auf ~50 fps), Heap unter ca. 65 MB (vorher bis 185 MB und steigend). Das 60-s-Zeitlimit des Laufs gilt unverändert |
+| Nachtrag Editor-Eingabe | erledigt | #310 | Enter im Editor setzte die nächste Zeile ohne Einrückung fort: `autoIndent` stand auf `none` und die Sprache `arduino-cpp` hatte keine Sprachkonfiguration. Jetzt `autoIndent: "full"`, 2 Leerzeichen, und `client/src/lib/editor/arduino-language-configuration.ts` (Klammern, automatisches Schließen von `{ [ ( " '` und `/*`, Kommentar-Fortsetzung; Einrückung der nächsten Zeile nach `if`/`else`/`for`/`while`/`do` ohne Klammer); „Format Code“ als `client/src/lib/editor/format-code.ts` herausgelöst. Tests: `e2e/editor-typing.spec.ts` (echtes Monaco, Tastatureingabe: Einrückung nach `{`, verschachtelt, nach Kommentar, mitten in der Zeile, Tab, Klammern/Anführungszeichen, `setValue` unverändert), `tests/client/editor-format-code.test.ts`. **Offen:** `case`-Marken (die Einrückungsregeln von Monaco können erste und spätere Marke nicht unterscheiden), `else` zu einem verschachtelten klammerlosen `if`, „Format Code“ rückt klammerlose Anweisungen nicht ein |
+| Bundle-Reserve (vor P6.5) | erledigt | #326 | Die Boardansicht (`ArduinoBoard` samt `usePinPollingEngine`) lädt mit eigenem Chunk aus `PinMonitorView` (`lazy`, Platzhalter gleicher Größe); die Grafiken lagen schon als SVG in `public/`. Initialer Ladepfad 708,2 → 689,1 kB (`npm run check:bundle`), Reserve 20,9 kB bei unverändertem Limit 710 kB; E2E 51/51 grün, visuelle Snapshots unverändert |
+| P7.4 | erledigt | #330 | ARCHITECTURE „Host-Mock als Rückfallebene“: Rückfallwege für Betreiber und Nutzer, Kriterien für den Abbau (Plan 9.4: ein Semester ohne Rückfall, Korpus stabil grün; frühestens nach `v2.0.0`, eigene ADR), Umfang des Abbaus mit Pfaden, was bleibt; OPEN_POINTS `HOSTMOCK-REMOVAL` (DEFERRED); „Runtime-Profile“ an Browser-Emulation und Compile-Sandbox angepasst. Die V1-Rückkehr (9.3) prüft P7.2 |
+| P7.2 | erledigt | #331 | Compose reicht Laufzeit- und Board-Variablen aus `.env` durch, leere Werte gelten als nicht gesetzt (`runtime-settings.ts`); Gateway-Deployment-Test in der Pilot-Konfiguration (`/api/config` mit `browser` als Standard, Nano-Compile mit gültigem Artefakt über das Gateway), Installer-Test mit dem Standard; INSTALL_SERVER (Compile-Sandbox, Pilot, Readiness, Rollback), INSTALL_UBUNTU_VBOX (Pilot einschalten), Release-Runbook (Images, Docker-Gate mit Compile-Vertrag, Deployment-Gate, Rückfall); Rückkehr V2 → v1.0.1 → V2 auf derselben `.env` und demselben `storage/` geprüft, siehe [report-p7-2-v1-rollback.md](report-p7-2-v1-rollback.md). Standard im Docker-Profil bleibt `host-mock` bis M4 |
+| P7.1 | erledigt bis auf die Messung am Referenz-Host (wartet auf Mensch) | #332 | Compile-Phase in `npm run capacity:calibrate` (vier Schritte, Tabelle im Bericht, `--skip-compile`), Compile-Container tragen im Testprofil das Label des Kapazitätslaufs; CAPACITY_MODEL „Compile capacity (browser emulation)“: Modell, Kenngrößen nach 4.12, Messung auf M2 Pro (30 gleichzeitige neue Compiles p95 6,3 s, ≈ 275 Compiles/min, unverändertes Beispiel 8 ms); Messung auf der Referenz-VM: [human-tests.md, Teil 3](human-tests.md) |
+| P2.3 | erledigt | #333 | ATTinyCore 1.5.2 gepinnt (Version, HTTPS-Archiv, SHA-256), Installation ohne Board-Manager ins Sketchbook mit `platform.local.txt` gegen die Post-Build-Hooks (Fund: sie schreiben in Core und Sketch; HEX identisch mit und ohne), Prüfskript und Canary-Test; Compile-Sandbox-Image mit ATTinyCore und vorkompiliertem Core je FQBN; CI (Toolchain- und E2E-Job, Cache-Schlüssel, Deployment-Prüfschritt); Toolchain-Gate `toolchain-cores.test.ts` (alle vier Boards); INSTALL_LOCAL, README (Lizenzen LGPL 2.1). Abweichung: kein Board-Manager, weil `drazzy.com` und `azduino.com` abgelaufene Zertifikate liefern |
+| P6.1 | erledigt | #333 (mit P2.3) | ATTinyCore im Compile-Sandbox-Image; Vertragstest kompiliert für den ATtiny85 in der Sandbox |
+| P6.2 | erledigt | #334 | `mcu/attiny85.ts`: Port B mit INT0 (PB2) und PCINT0, Timer 0 mit eigenen Registern und Ein-Wort-Vektoren, `ATtinyTimer1`, ADC mit 4 Kanälen und REFS2 an Bit 4 (avr8js-Fehler), EEPROM 512 B mit Schreibzeit für 8 MHz, Watchdog, Taktteiler, kein USART; RAMEND 0x25F. Tests `attiny85-profile.test.ts` (13) mit drei ATTinyCore-Fixtures (Ergebnisse im EEPROM). Funde: GIMSK/GIFR werden von INT0 und PCINT geteilt, avr8js verdrahtet nur je einen (behoben); `ATtinyTimer1` erzeugt in der PWM-Konfiguration von ATTinyCore kein OC1x-Signal (offen, Register stimmen); Spike-Spanne für `delay(587)` war zu eng (585–589 ms abgeleitet). Registriert mit P6.4. Upstream-Meldungen an avr8js: wartet auf Mensch |
+| P6.3 | erledigt (Modell) | #335 | `io/software-uart.ts`: TX-Decoder an PB0 (nur Ausgang, nicht OC0A) mit Baudraten-Erkennung (größte Standardrate, 90 % der Abstände ganzzahlige Bitvielfache) und gemessener Bitbreite, Rückfall auf die eingestellte Rate bei Ruhe, Startbit-Prüfung in Bitmitte; RX über ein Komparatormodell (ACSR, Bandlücke/AIN0 gegen PB1, Vektor 7), Empfänger aktiv mit `ACIE`; Schnittstelle wie `AVRUSART` für `SerialPorts`. Tests `software-uart.test.ts` (7: 9600/19200, UTF-8, Rückfall, Echo, gesperrte Eingabe, synthetische Flanken). Anbindung an Serial-Schicht und Serial-Monitor-Baudrate mit P6.4, Benutzerhandbuch mit P6.5 |
+| P6.4 | erledigt | #336 | `ATTINY85_BOARD` (Pins 0–5 = PB0–PB5, A0–A3 als Zweitnamen von 5/2/4/3, PWM 0/1/4, RESET PB5, `LED_BUILTIN` 1, `Serial` als Software-Port 0/1), Profil und Hardware registriert; `SerialPorts` mit USART oder `SoftwareUart` (`setFallbackBaud`); PWM-Tastverhältnis für Timer 1 des ATtiny85 (`pwm.ts`, `pwmChannelRegisters`); Shared: `analogName`, Parser rechnen `A` = `0x80 \| n` und `analogRead(n)` als Kanal wie ATTinyCore; Regeln RESET-Pin und Serial-Pins; Pinbelegung im Toolchain-Gate gegen `variants/tinyX5` (Konstanten über den Compiler ausgewertet). Freigegebene Teständerungen (Nutzer, 2026-10-23): fünf Tests nutzen für „Board ohne Hardware“ eine unbekannte ID statt `attiny85`. Board bleibt `planned` bis P6.5 |
+| P6.5 | erledigt | #337 | ATtiny85 `preview`: DIP-8-Ansicht (`npm run generate:attiny85-svg`), LED „L“ und Analog-Beschriftungen nach `LED_BUILTIN` bzw. Sketch-Namen des Boards, Analogdialog „2 (A1)“; Startbefehl mit `serialBaud` aus `Serial.begin()` (SSOT SimulationRuntime); 5 Korpus-Proben (`derived`); `e2e/attiny85.spec.ts` (3); Benutzerhandbuch „Board wählen“ mit Halbduplex, RESET-Pin, Serial-Pins; Funde: Baudraten-Erkennung rastete auf 19200 statt 9600 ein (jetzt Rahmenprüfung über alle passenden Raten), Byte 0x00 aus `digitalWrite(0)` ohne `Serial` (TX nur bei aktivem `Serial`). Freigegebene Teständerungen (Nutzer, 2026-10-23): sieben Tests auf Status `preview` des ATtiny85 umgestellt |
+| M3 | erledigt (mit ausgewiesenen Ausnahmen) | #337 | ATtiny85 als `preview` freigegeben, Proben und E2E grün, 11 × Echtzeit; **keine Aufnahme an echter ATtiny85-Hardware** (alle Fälle `derived`), Timer-1-PWM-Signal in avr8js offen, Meldungen an avr8js warten auf Mensch; siehe [report-m3-acceptance.md](report-m3-acceptance.md) |
+| (Rest P4.4) Mobile-Layout je Board | erledigt | #338 | Geprüft mit Screenshots aller vier Boards bei 390 × 844: die schwebende Panel-Navigation überdeckte unten rechts Pins (Uno A0–A5, ATtiny85 PB0–PB2). Die mobile Board-Ansicht hält jetzt unten 20rem für die Navigation frei (`index.css`), das Board steht darüber; Uno, Nano, Mega und ATtiny85 sind frei bedienbar |
+| Anleitung | bereit | #290 | [human-tests.md](human-tests.md): P0.3 mit `npm run spike:p03` (Messseite für Browser auf Geräten), P1.14-Aufnahmen mit `npm run record:hardware`, P7.1 Compile-Kapazität auf dem Referenz-Host |
Status: offen · in Arbeit · erledigt · wartet auf Mensch · blockiert
diff --git a/docs/proposals/report-m3-acceptance.md b/docs/proposals/report-m3-acceptance.md
new file mode 100644
index 000000000..fd71aa244
--- /dev/null
+++ b/docs/proposals/report-m3-acceptance.md
@@ -0,0 +1,88 @@
+# M3-Abnahme: ATtiny85 im Browser (P6.5)
+
+> **Nicht normativ.** Abnahmebericht zu Meilenstein M3 aus
+> [browser-emulation-plan.md](browser-emulation-plan.md); wird mit dem Plan
+> gelöscht. Muster: [report-m2-acceptance.md](report-m2-acceptance.md).
+
+## Ergebnis
+
+**Erfüllt, mit ausgewiesenen Ausnahmen.** Der ATtiny85 (ATTinyCore 1.5.2,
+8 MHz intern) ist als `preview` freigegeben: Toolchain im Compile-Sandbox-Image,
+MCU-Profil, Software-UART, Board-Hardware und Parserregeln, Boardansicht,
+Korpus-Proben und E2E sind grün. **Keine Aufnahme an einem echten ATtiny85**:
+alle ATtiny-Fälle sind `derived`.
+
+## Kriterien aus Plan Phase 6
+
+| Kriterium | Stand | Beleg |
+|---|---|---|
+| P6.1 Toolchain im Compile-Sandbox-Image | erfüllt | #333: ATTinyCore 1.5.2 mit gepinnter SHA-256 im Image, Core je FQBN vorkompiliert; Vertragstest kompiliert für den ATtiny85 in der Sandbox; Toolchain-Gate `toolchain-cores.test.ts` für alle vier Boards |
+| P6.2 MCU-Profil | erfüllt | #334: Port B mit INT0/PCINT0, Timer 0 und 1, ADC mit REFS2 an Bit 4, EEPROM, Watchdog; `attiny85-profile.test.ts` (13 Fälle) |
+| P6.3 Software-UART | erfüllt | #335 und dieser PR: TX-Dekodierung mit Baudraten-Erkennung, RX über das Komparatormodell; `software-uart.test.ts` (8 Fälle) |
+| P6.4 Board-Hardware und Regeln | erfüllt | #336: `ATTINY85_BOARD`, Serial-Port der Art `software`, PWM-Beobachtung Timer 1, Parser rechnen `A` wie ATTinyCore, Warnungen RESET-Pin und Serial-Pins; Pinbelegung im Toolchain-Gate gegen `variants/tinyX5` |
+| P6.5 Freigabe `preview` | erfüllt | dieser PR: `shared/boards.ts` `attiny85` `preview`, Laufzeit `browser`; Boardansicht DIP-8 (`npm run generate:attiny85-svg`); Benutzerhandbuch „Board wählen“ mit den Grenzen des Boards |
+| Proben grün | erfüllt | 5 Korpus-Proben (`attiny_basics`, `attiny_adc_read`, `attiny_pwm_duty`, `attiny_serial_echo`, `attiny_interrupts`), dazu die Paket-Tests von Profil, UART und Board |
+| E2E grün | erfüllt | `e2e/attiny85.spec.ts`, 3 Szenarien (unten), 3 von 3 in 8,7 s; ganze Suite 54 von 54 |
+| Bundle | erfüllt | `npm run check:bundle`: initialer Ladepfad 691,0 kB von 710 kB (die Boardansicht lädt seit #326 mit eigenem Chunk); avr8js nur im Worker-Chunk |
+
+## End-to-End-Lauf im Browser (Playwright, echter Compile mit ATTinyCore)
+
+| Szenario | Ergebnis |
+|---|---|
+| Tools → Board bietet den ATtiny85 mit Preview-Hinweis; die Boardansicht zeigt den DIP-8 mit den Pins 0–4 interaktiv, PB5 (RESET) nicht | grün |
+| Compile für den ATtiny85, Lauf im Browser, Ausgabe über den Software-UART, A1 (Pin 2, Dialog „2 (A1)“) am Board auf 512 gesetzt und im Sketch gelesen | grün |
+| Serielle Eingabe über den Komparator: „42“ → „got 42“ | grün |
+
+## Leistung (Headless, `npm run emulate`, Median von 7 Läufen, 1500 ms simuliert)
+
+Gemessen am 23.10.2026 auf Apple M2 Pro, Node 24.20.0:
+
+| Fixture | Board | Geschwindigkeit |
+|---|---|---|
+| `attiny85-tiny_serial` (Serial über Software-UART) | ATtiny85 | 11,1 × Echtzeit |
+| `attiny85-tiny_probe` | ATtiny85 | 11,2 × |
+| `uno-blink` (Vergleich) | Uno | 4,4 × |
+
+Befehl: `npm run emulate -- packages/avr-emulator/tests/fixtures/.hex --board --ms 1500 --repeat 7`.
+Der ATtiny85 läuft mit 8 MHz, also mit halb so vielen Takten je Sekunde wie der Uno.
+
+## Referenzen: was die Fälle beweisen
+
+| Fälle | Klasse | Grundlage |
+|---|---|---|
+| 5 Korpus-Proben | `derived` | Datenblatt ATtiny25/45/85, `avr/iotnx5.h`, Quelltext von ATTinyCore 1.5.2 (`wiring.c`, `wiring_analog.c`, `TinySoftwareSerial.cpp`, `pins_arduino.h` von tinyX5) und Rechnung; Begründung je Fall in `case.json` |
+| Paket-Fixtures `attiny85-*` | `derived` | wie oben; Ausgaben des Spikes P0.5 für die `Serial`-Probe |
+
+Keine Aufnahme an einem echten ATtiny85. Die Klasse `hardware` braucht einen
+Messaufbau (ATtiny85 mit Programmer und USB-Seriell-Adapter an PB0/PB1); das
+ist Arbeit für einen Menschen, eine Anleitung dafür fehlt noch.
+
+## Funde der Abnahme (alle behoben, mit Test)
+
+| Fund | Behebung |
+|---|---|
+| ATTinyCore-Post-Build-Hooks schreiben in Core und Sketch; in der Compile-Sandbox (read-only) bricht der Compile ab | `platform.local.txt` schaltet die Hooks ab, das HEX bleibt byteweise gleich (#333) |
+| Index und `micronucleus` des Board-Managers liefern abgelaufene TLS-Zertifikate | Installation aus dem Release-Archiv mit gepinnter SHA-256 (#333) |
+| avr8js liest REFS2 an Bit 3 | eigener Getter im Profil (#334, aus Spike P0.5) |
+| avr8js verdrahtet die gemeinsamen Register GIMSK/GIFR nur für INT0 bzw. PCINT | Hooks im Profil umhüllt (#334) |
+| `analogRead(A1)` wurde im Parser zu Pin 4 (Pin- und Kanalnummer überlagern sich) | Parser rechnen `A` = `0x80 \| n` wie ATTinyCore (#336) |
+| LED „L“ und Analog-Beschriftungen der Boardansicht waren fest auf Pin 13 bzw. `A`-Labels | `LED_BUILTIN` und Sketch-Namen des Boards (dieser PR) |
+| Baudraten-Erkennung rastete auf ein Vielfaches ein (19200 statt 9600) | Entscheidung erst, wenn jede passende Rate genug Rahmen hat; die Rate ohne Rahmenfehler gewinnt (dieser PR, Probe `attiny_basics`) |
+| `digitalWrite(0, LOW)` ohne `Serial` erzeugte ein Byte 0x00 | TX wird nur dekodiert, solange `Serial` aktiv ist (dieser PR, Probe `attiny_pwm_duty`) |
+| Spanne für `delay(587)` aus dem Spike (586–588 ms) zu eng | abgeleitet 585–589 ms (#334) |
+
+## Bekannte Restpunkte (nicht M3-blockierend)
+
+- **Keine Aufnahme an echter ATtiny85-Hardware** (alle Fälle `derived`).
+- **Timer-1-PWM-Signal:** avr8js 0.21.1 erzeugt in der PWM-Konfiguration von
+ ATTinyCore kein Signal an OC1A/OC1B. Das Tastverhältnis in UnoSim kommt aus
+ den Registern und stimmt; ein Sketch, der den Pegel des Pins liest, sieht LOW
+ (README des Pakets).
+- **Meldungen an avr8js** (REFS2, GIMSK/GIFR, Timer-1-PWM): nach außen
+ gerichtet, wartet auf einen Menschen.
+- **Halbduplex** des Software-UART ist Eigenschaft des Boards und im
+ Benutzerhandbuch beschrieben.
+- **Mobile-Layout** der ATtiny-Ansicht wie bei Nano und Mega nicht gesondert
+ geprüft.
+- **Portleiste des Serial Monitors** (Mega, #319): weiterhin von keinem
+ Menschen angesehen.
diff --git a/e2e/attiny85.spec.ts b/e2e/attiny85.spec.ts
new file mode 100644
index 000000000..7d1663110
--- /dev/null
+++ b/e2e/attiny85.spec.ts
@@ -0,0 +1,95 @@
+import { expect, test, type Page } from "@playwright/test";
+
+// Smoke test of the ATtiny85 (plan P6.5, status `preview`): choosing it in Tools → Board, the DIP-8 board view
+// (pins 0–4 interactive, RESET on PB5 not), a real compile with ATTinyCore and a run in the browser emulation:
+// Serial through the software UART on PB0, an analog value set on PB2 (A1), and Serial input over the comparator.
+
+const ENGINE_KEY = "unoSimulationEngine";
+const BOARD_KEY = "unoSelectedBoard";
+const COMPILE_TIMEOUT = process.env.CI ? 120_000 : 60_000;
+
+const SKETCH = `void setup() {
+ Serial.begin(9600);
+ pinMode(LED_BUILTIN, OUTPUT);
+}
+void loop() {
+ Serial.print("A1=");
+ Serial.println(analogRead(A1));
+ digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
+ delay(200);
+}`;
+
+async function open(page: Page, board?: "attiny85"): Promise {
+ await page.addInitScript(([engine, key, value]) => {
+ globalThis.localStorage.setItem(engine, "browser");
+ if (value) globalThis.localStorage.setItem(key, value);
+ }, [ENGINE_KEY, BOARD_KEY, board ?? ""]);
+ await page.goto("/");
+ await page.waitForFunction(() => Boolean((globalThis as unknown as Record).__MONACO_EDITOR__), undefined, { timeout: 15_000 });
+}
+
+async function startSketch(page: Page, code: string): Promise {
+ await page.evaluate((sketch) => {
+ const editor = (globalThis as unknown as Record).__MONACO_EDITOR__ as { setValue: (value: string) => void };
+ editor.setValue(sketch);
+ }, code);
+ await page.getByRole("button", { name: /start simulation/i }).click();
+ await expect(page.getByRole("button", { name: /stop simulation/i })).toBeVisible({ timeout: COMPILE_TIMEOUT });
+}
+
+// Serial of the ATtiny85 is half duplex: the core blocks interrupts while it sends, so input that arrives
+// during output is lost, as on the chip. This sketch only answers.
+const ECHO_SKETCH = `void setup() {
+ Serial.begin(9600);
+ Serial.println("ready");
+}
+void loop() {
+ if (Serial.available()) {
+ int n = Serial.parseInt(); // read first: printing blocks the receiver
+ Serial.print("got ");
+ Serial.println(n);
+ }
+}`;
+
+const serial = (page: Page) => page.locator('[data-testid="serial-output"]');
+
+test.describe("ATtiny85 (preview)", () => {
+ test("Tools → Board offers the ATtiny85 with the preview notice and shows the DIP-8 board", async ({ page }) => {
+ await open(page);
+ await page.getByRole("menuitem", { name: "Tools" }).click();
+ await page.getByTestId("menu-board").click();
+ const tiny = page.getByRole("menuitemradio", { name: /ATtiny85.*\(preview\)/ });
+ await expect(tiny).toBeVisible();
+ await tiny.click();
+
+ await expect(page.getByText("ATtiny85 Board")).toBeAttached({ timeout: 10_000 });
+ for (const pin of [0, 1, 2, 3, 4]) await expect(page.locator(`#pin-${pin}-click`)).toHaveCount(1);
+ await expect(page.locator("#pin-5-click")).toHaveCount(0); // PB5 is RESET
+ await expect(page.locator("#pin-13-click")).toHaveCount(0);
+ expect(await page.evaluate(() => globalThis.localStorage.getItem("unoSelectedBoard"))).toBe("attiny85");
+ });
+
+ test("compiles with ATTinyCore, prints through the software UART and reads A1 set on the board", async ({ page }) => {
+ await open(page, "attiny85");
+ await startSketch(page, SKETCH);
+ await expect(serial(page)).toContainText(/A1=\d+/, { timeout: 15_000 });
+
+ // A1 is PB2, pin 2 on the board: the pin opens a dialog with a slider and a number field.
+ await page.locator("#pin-2-click").click();
+ await expect(page.getByText("2 (A1)")).toBeVisible();
+ await page.locator('input[type="number"]').fill("512");
+ await page.getByRole("button", { name: "Confirm" }).click();
+ await expect(serial(page)).toContainText("A1=512", { timeout: 10_000 });
+ });
+
+ test("sends Serial input to the sketch through the analog comparator", async ({ page }) => {
+ await open(page, "attiny85");
+ await startSketch(page, ECHO_SKETCH);
+ await expect(serial(page)).toContainText("ready", { timeout: 15_000 });
+
+ const input = page.getByPlaceholder("Send to Arduino...");
+ await input.fill("42");
+ await input.press("Enter");
+ await expect(serial(page)).toContainText("got 42", { timeout: 10_000 });
+ });
+});
diff --git a/package.json b/package.json
index ba77e9ace..89751d24c 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"emulate": "tsx scripts/emulate.ts",
"generate:nano-svg": "node scripts/generate-nano-svg.mjs",
"generate:mega-svg": "node scripts/generate-mega-svg.mjs",
+ "generate:attiny85-svg": "node scripts/generate-attiny85-svg.mjs",
"record:hardware": "tsx scripts/record-hardware.ts",
"spike:p03": "node spikes/p0-3/build.mjs --serve",
"fixtures:runtime": "tsx scripts/runtime-fixtures.ts",
diff --git a/packages/avr-emulator/README.md b/packages/avr-emulator/README.md
index fa3a48f99..04702053d 100644
--- a/packages/avr-emulator/README.md
+++ b/packages/avr-emulator/README.md
@@ -240,6 +240,21 @@ at least 90 % line coverage for `src/`.
reports the diagnostic `serial-input-discarded`; the corpus case
`probes/serial_before_begin` is recorded on the board.
- **Watchdog reset:** avr8js resets only PC and SP; see the diagnostics section.
+- **ADC reference on the ATtiny85** (`src/mcu/attiny85.ts`): avr8js reads
+ REFS2 at bit 3 of ADMUX, the ATtiny85 has it at bit 4 (bit 3 is MUX3). With
+ `analogReference(INTERNAL2V56)` avr8js would measure against VCC (spike
+ P0.5: 102 instead of 200 for 0.5 V). The profile reads REFS2 itself.
+- **Shared interrupt registers on the ATtiny85:** INT0 and the pin-change
+ interrupt share GIMSK and GIFR. avr8js hooks GIMSK only for INT0 and gives
+ GIFR to the pin-change interrupt alone, so enabling PCIE did not raise a
+ pending pin change and writing 1 to INTF0 did not clear INT0. The profile
+ wraps both hooks.
+- **Timer 1 PWM on the ATtiny85 (open):** `ATtinyTimer1` models PWM mode as
+ phase-correct counting and drives no OC1A/OC1B waveform when CTC1 is set,
+ which is how ATTinyCore configures it for `analogWrite` on PB1/PB4. On the
+ chip the counter runs from 0 to OCR1C, OC1x is set at 0 and cleared at OCR1x
+ (COM1x = 10). The registers are right, so a duty cycle derived from them is
+ right; a sketch that reads the pin level of a timer-1 PWM pin sees LOW.
## ATmega2560 profile
@@ -267,6 +282,55 @@ Known differences (not modelled by avr8js): compare-match C interrupts of the
them); sketches above 64 KB of PROGMEM may push the core's pin tables out of
the `lpm` range, as on the board (spike P0.6).
+## ATtiny85 profile
+
+`src/mcu/attiny85.ts` (plan P6.2, spike P0.5): one port (B), timer 0 with its
+own registers and vectors, avr8js' `ATtinyTimer1`, a 4-channel ADC (ADC0–3 on
+PB5, PB2, PB4, PB3; references VCC, AREF, 1.1 V, 2.56 V), 512 bytes EEPROM
+with write times for 8 MHz, watchdog and clock prescaler; no USART (the core's
+`Serial` is a software UART). Vectors have one word each (RJMP), vector n at
+word n. Data space up to RAMEND 0x25F (`new CPU(progMem, 0x160)`). Tests:
+`tests/attiny85-profile.test.ts` with the fixtures `attiny85-tiny_*.hex`.
+
+Board hardware `ATTINY85_BOARD` (`src/boards/attiny85-board.ts`, P6.4): pins
+0–5 = PB0–PB5, 8 MHz, `LED_BUILTIN` 1; A0–A3 are second names of pins 5, 2, 4,
+3 (`analogName`); PWM on 0 (timer 0 A), 1 (timer 1 A) and 4 (timer 1 B), the
+duty cycle of timer 1 read from TCCR1/GTCCR/OCR1x/OCR1C (`pwm.ts`); PB5 is
+RESET; `Serial` is a serial port of kind `software` on 0 (TX) and 1 (RX), for
+which `SerialPorts` creates a `SoftwareUart` (`setFallbackBaud` sets the rate
+it uses until the line reveals the sketch's). Checked against
+`variants/tinyX5/pins_arduino.h` in the toolchain gate.
+
+## Software UART (ATtiny85)
+
+`src/io/software-uart.ts` (plan P6.3): the ATtiny85 has no USART; ATTinyCore's
+`Serial` (`TinySoftwareSerial`) sends on PB0 and receives on PB1 through the
+analog comparator. `SoftwareUart` offers the members of `AVRUSART` that
+`SerialPorts` uses (`onByteTransmit`, `writeByte`, `rxEnable`, `rxBusy`,
+`baudRate`, …), so the ATtiny85 board can use the same serial port (P6.4).
+
+- **TX:** edges of PB0 while it is an output and timer 0 does not drive it
+ (OC0A). The baud rate is the largest standard rate of which at least 90 % of
+ at least 8 in-frame edge distances are whole bit multiples (±20 %); the bit
+ width is then averaged from them (the core's delay loop runs a few percent
+ slow: 863 cycles per bit at 9600 baud and 8 MHz instead of 833). A start bit
+ must still be low at its middle, so the pulses of the pin set-up are no
+ bytes. Until the rate is found, an idle line (12 bit times) is decoded with
+ `configuredBaud` (the Serial Monitor's).
+- **RX:** `writeByte` drives PB1 bit by bit in simulated time; the comparator
+ compares it with the bandgap (`ACBG`, as the core sets it) or AIN0 and
+ raises its interrupt (vector 7) on the configured edge. The receiver counts
+ as enabled while the comparator interrupt is enabled (`Serial.begin()`).
+ ACME (ADC multiplexer as negative input) is not modelled.
+- **Limits of the board, not of the model:** half duplex (the core disables
+ interrupts while it sends, so input during output is lost); no reception
+ when the sketch uses PB1 otherwise; no output on PB0 while it is PWM.
+
+Tests: `tests/software-uart.test.ts` (9600 and 19200 baud, UTF-8, the
+fallback rate, echo over the comparator, input refused before `Serial.begin()`,
+synthetic waveforms) with the fixtures `attiny85-tiny_serial*.hex` and
+`attiny85-tiny_echo.hex`.
+
## License
MIT, like UnoSim.
diff --git a/packages/avr-emulator/src/boards/attiny85-board.ts b/packages/avr-emulator/src/boards/attiny85-board.ts
new file mode 100644
index 000000000..9a1d4f65e
--- /dev/null
+++ b/packages/avr-emulator/src/boards/attiny85-board.ts
@@ -0,0 +1,36 @@
+import type { BoardHardware, PinHardware } from "./types";
+
+// ATTinyCore 1.5.2, variants/tinyX5/pins_arduino.h (8 MHz internal): pins 0–5 are PB0–PB5,
+// A0–A3 are second names of pins 5, 2, 4, 3 (ADC0–3); PWM on 0 (OC0A), 1 (OC1A, TIMER1_PWM)
+// and 4 (OC1B); `Serial` is TinySoftwareSerial with TX on PB0 and RX on PB1; LED_BUILTIN is 1.
+const ANALOG: Readonly> = { 5: 0, 2: 1, 4: 2, 3: 3 };
+const PWM: Readonly> = {
+ 0: { timer: 0, channel: "A" },
+ 1: { timer: 1, channel: "A" },
+ 4: { timer: 1, channel: "B" },
+};
+
+// PB5 is RESET (the fuse that would free it is not set by the core); PB0/PB1 carry `Serial`.
+const ROLE: Readonly> = { 0: "serial", 1: "serial", 5: "reset" };
+
+function pin(number: number): PinHardware {
+ const channel = ANALOG[number];
+ return {
+ pin: number,
+ label: `PB${number}`,
+ port: { port: "B", bit: number },
+ ...(channel === undefined ? {} : { adcChannel: channel, analogName: `A${channel}` }),
+ ...(PWM[number] ? { pwm: PWM[number] } : {}),
+ ...(ROLE[number] ? { role: ROLE[number] } : {}),
+ };
+}
+
+export const ATTINY85_BOARD: BoardHardware = {
+ id: "attiny85",
+ mcu: "attiny85",
+ clockHz: 8_000_000,
+ vccVolts: 5,
+ ledBuiltin: 1,
+ pins: Array.from({ length: 6 }, (_, number) => pin(number)),
+ serialPorts: [{ index: 0, kind: "software", rxPin: 1, txPin: 0 }],
+};
diff --git a/packages/avr-emulator/src/boards/index.ts b/packages/avr-emulator/src/boards/index.ts
index 82750547c..72615fc6e 100644
--- a/packages/avr-emulator/src/boards/index.ts
+++ b/packages/avr-emulator/src/boards/index.ts
@@ -6,6 +6,7 @@
export { BOARD_HARDWARE_IDS, BOARD_MCU, isBoardHardwareId } from "./ids";
export type { BoardHardwareId } from "./ids";
export { NANO, UNO } from "./atmega328p-boards";
+export { ATTINY85_BOARD } from "./attiny85-board";
export { MEGA2560 } from "./mega2560-board";
export { boardHardware, hasBoardHardware } from "./registry";
export type { BoardHardware, PinHardware, PortName, SerialPortHardware } from "./types";
diff --git a/packages/avr-emulator/src/boards/registry.ts b/packages/avr-emulator/src/boards/registry.ts
index 4666f908c..d0aea8a53 100644
--- a/packages/avr-emulator/src/boards/registry.ts
+++ b/packages/avr-emulator/src/boards/registry.ts
@@ -1,10 +1,11 @@
import { UNO, NANO } from "./atmega328p-boards";
+import { ATTINY85_BOARD } from "./attiny85-board";
import { MEGA2560 } from "./mega2560-board";
import type { BoardHardwareId } from "./ids";
import type { BoardHardware } from "./types";
-/** Board hardware with an MCU profile. ATtiny85 (P6.4) follows. */
-const BOARDS: Partial> = { uno: UNO, nano: NANO, mega2560: MEGA2560 };
+/** Board hardware with an MCU profile. */
+const BOARDS: Partial> = { uno: UNO, nano: NANO, mega2560: MEGA2560, attiny85: ATTINY85_BOARD };
export function boardHardware(id: BoardHardwareId): BoardHardware {
const board = BOARDS[id];
diff --git a/packages/avr-emulator/src/boards/types.ts b/packages/avr-emulator/src/boards/types.ts
index dcf03ddd1..8d5ac1b17 100644
--- a/packages/avr-emulator/src/boards/types.ts
+++ b/packages/avr-emulator/src/boards/types.ts
@@ -11,19 +11,28 @@ export interface PinHardware {
/** Missing for analog-only inputs such as A6/A7 on the Nano. */
readonly port?: { readonly port: PortName; readonly bit: number };
readonly adcChannel?: number;
+ /**
+ * Analog name when it is a second name of a digital pin (ATtiny85: pin 2 is
+ * also `A1`); boards with separate analog pin numbers (Uno: A0 = 14) carry
+ * it in `label`.
+ */
+ readonly analogName?: string;
readonly pwm?: { readonly timer: number; readonly channel: "A" | "B" | "C" };
readonly role?: "reset" | "serial";
}
-export interface SerialPortHardware {
+interface SerialPortPins {
/** Port index in the protocol: 0 = `Serial`, 1 = `Serial1`, … */
readonly index: number;
- readonly kind: "usart";
- readonly usart: number;
readonly rxPin: number;
readonly txPin: number;
}
+/** A hardware USART, or the core's software UART on a board without one (ATtiny85, `io/software-uart.ts`). */
+export type SerialPortHardware =
+ | (SerialPortPins & { readonly kind: "usart"; readonly usart: number })
+ | (SerialPortPins & { readonly kind: "software"; readonly usart?: undefined });
+
export interface BoardHardware {
readonly id: BoardHardwareId;
readonly mcu: McuId;
diff --git a/packages/avr-emulator/src/index.ts b/packages/avr-emulator/src/index.ts
index c8ed6cab3..46b96bebb 100644
--- a/packages/avr-emulator/src/index.ts
+++ b/packages/avr-emulator/src/index.ts
@@ -7,6 +7,7 @@
export { BOARD_HARDWARE_IDS, BOARD_MCU, isBoardHardwareId } from "./boards/ids";
export type { BoardHardwareId } from "./boards/ids";
export { NANO, UNO } from "./boards/atmega328p-boards";
+export { ATTINY85_BOARD } from "./boards/attiny85-board";
export { MEGA2560 } from "./boards/mega2560-board";
export { boardHardware, hasBoardHardware } from "./boards/registry";
export type { BoardHardware, PinHardware, PortName, SerialPortHardware } from "./boards/types";
@@ -22,15 +23,18 @@ export { IoUsageObserver } from "./io/io-usage";
export type { IoFunction, IoUsage } from "./io/io-usage";
export { PinModel } from "./io/pin-model";
export { SERIAL_INPUT_LIMITS, SerialPorts } from "./io/serial-ports";
+export { SoftwareUart } from "./io/software-uart";
+export type { SoftwareUartPins } from "./io/software-uart";
export type { SerialFrameConfig, SerialOutput } from "./io/serial-ports";
export { Utf8StreamDecoder, encodeUtf8 } from "./io/utf8";
export type { Level, PinChanges, PinMode, PinSnapshot } from "./io/pin-model";
-export { pwmDuty, pwmRegisterAddresses } from "./io/pwm";
+export { pwmChannelRegisters, pwmDuty, pwmRegisterAddresses } from "./io/pwm";
export type { PwmChannel, TimerRegisters } from "./io/pwm";
export type { Machine, MachineOptions } from "./machine";
export { ArrayEepromBackend, erasedEeprom } from "./mcu/eeprom";
export { ATMEGA2560 } from "./mcu/atmega2560";
export { ATMEGA328P } from "./mcu/atmega328p";
+export { ATTINY85 } from "./mcu/attiny85";
export { mcuProfile } from "./mcu/registry";
export type { McuPeripherals, McuProfile } from "./mcu/types";
export { encodeBase64, decodeBase64 } from "./image/base64";
diff --git a/packages/avr-emulator/src/io/io-usage.ts b/packages/avr-emulator/src/io/io-usage.ts
index 8d19f7777..5162e6a8c 100644
--- a/packages/avr-emulator/src/io/io-usage.ts
+++ b/packages/avr-emulator/src/io/io-usage.ts
@@ -5,6 +5,7 @@ import {
bitsOf, ioBitInstructionBit, isSetBitInstruction, isTwoWordInstruction, loadDestination, modifierMask, singleBit, skipBitTest, storeSource,
} from "./avr-decode";
import { chainWrite } from "./hooks";
+import { pwmChannelRegisters } from "./pwm";
import type { PinMode } from "./pin-model";
export type IoFunction = "pinMode" | "digitalWrite" | "digitalRead" | "analogRead" | "analogWrite";
@@ -212,18 +213,16 @@ export class IoUsageObserver {
const { cpu, board, mcu } = this.machine;
for (const pin of board.pins) {
if (!pin.pwm) continue;
- const timer = mcu.timerRegisters[pin.pwm.timer];
- const comBit = { A: 7, B: 5, C: 3 }[pin.pwm.channel];
- const ocr = { A: timer.OCRA, B: timer.OCRB, C: timer.OCRC }[pin.pwm.channel];
+ const { control, comHighBit: comBit, ocr } = pwmChannelRegisters(mcu.timerRegisters[pin.pwm.timer], pin.pwm.channel);
const onAnalogWrite = () => {
this.pendingOutputs = this.pendingOutputs.filter((p) => p.pin !== pin.pin);
this.record("analogWrite", pin.pin);
};
- chainWrite(cpu, timer.TCCRA, (value, old) => {
+ chainWrite(cpu, control, (value, old) => {
if (value & ~old & (1 << comBit)) onAnalogWrite();
});
chainWrite(cpu, ocr, () => {
- if (cpu.data[timer.TCCRA] & (1 << comBit)) onAnalogWrite();
+ if (cpu.data[control] & (1 << comBit)) onAnalogWrite();
});
}
}
diff --git a/packages/avr-emulator/src/io/pwm.ts b/packages/avr-emulator/src/io/pwm.ts
index 7c066b7cf..1014ea9ac 100644
--- a/packages/avr-emulator/src/io/pwm.ts
+++ b/packages/avr-emulator/src/io/pwm.ts
@@ -1,5 +1,5 @@
-/** Registers of one timer, as far as PWM observation needs them. */
-export interface TimerRegisters {
+/** Registers of one timer of the ATmega kind, as far as PWM observation needs them. */
+export interface AvrTimerRegisters {
readonly bits: 8 | 16;
readonly TCCRA: number;
readonly TCCRB: number;
@@ -11,12 +11,43 @@ export interface TimerRegisters {
readonly ICR: number;
}
+/**
+ * Timer 1 of the ATtiny85: PWM1A and COM1A in TCCR1, PWM1B and COM1B in GTCCR,
+ * counting from 0 to OCR1C (datasheet 12.2).
+ */
+export interface AttinyTimer1Registers {
+ readonly kind: "attiny-timer1";
+ readonly TCCR1: number;
+ readonly GTCCR: number;
+ readonly OCR1A: number;
+ readonly OCR1B: number;
+ readonly OCR1C: number;
+}
+
+export type TimerRegisters = AvrTimerRegisters | AttinyTimer1Registers;
+
+const isAttinyTimer1 = (timer: TimerRegisters): timer is AttinyTimer1Registers => "kind" in timer;
+
export type PwmChannel = "A" | "B" | "C";
const COM_SHIFT: Readonly> = { A: 6, B: 4, C: 2 };
+/**
+ * Where a channel's compare-output mode and compare value live: `control` holds
+ * the COM bits with the upper one at `comHighBit`, `ocr` the compare value.
+ */
+export function pwmChannelRegisters(timer: TimerRegisters, channel: PwmChannel): { control: number; comHighBit: number; ocr: number } {
+ if (isAttinyTimer1(timer)) {
+ return channel === "B"
+ ? { control: timer.GTCCR, comHighBit: 5, ocr: timer.OCR1B }
+ : { control: timer.TCCR1, comHighBit: 5, ocr: timer.OCR1A };
+ }
+ return { control: timer.TCCRA, comHighBit: { A: 7, B: 5, C: 3 }[channel], ocr: { A: timer.OCRA, B: timer.OCRB, C: timer.OCRC }[channel] };
+}
+
/** All data-space addresses whose writes can change a duty cycle of this timer. */
export function pwmRegisterAddresses(timer: TimerRegisters): number[] {
+ if (isAttinyTimer1(timer)) return [timer.TCCR1, timer.GTCCR, timer.OCR1A, timer.OCR1B, timer.OCR1C];
const wide = (address: number) => (timer.bits === 16 ? [address, address + 1] : [address]);
return [timer.TCCRA, timer.TCCRB, ...[timer.OCRA, timer.OCRB, timer.OCRC, timer.ICR].filter((a) => a !== 0).flatMap(wide)];
}
@@ -27,6 +58,7 @@ export function pwmRegisterAddresses(timer: TimerRegisters): number[] {
* with PWM (normal or CTC mode, output disconnected). No waveform is modelled.
*/
export function pwmDuty(data: Uint8Array, timer: TimerRegisters, channel: PwmChannel): number | undefined {
+ if (isAttinyTimer1(timer)) return attinyTimer1Duty(data, timer, channel);
const com = (data[timer.TCCRA] >> COM_SHIFT[channel]) & 3;
if (com < 2) return undefined; // disconnected (toggle mode carries no duty cycle)
const ocrAddress = { A: timer.OCRA, B: timer.OCRB, C: timer.OCRC }[channel];
@@ -38,7 +70,21 @@ export function pwmDuty(data: Uint8Array, timer: TimerRegisters, channel: PwmCha
return com === 3 ? 255 - duty : duty;
}
-function pwmTop(data: Uint8Array, timer: TimerRegisters): number | undefined {
+/** PWM1x selects PWM for the channel; OCR1C is TOP; COM 10 clears on match, 11 sets (inverted). */
+function attinyTimer1Duty(data: Uint8Array, timer: AttinyTimer1Registers, channel: PwmChannel): number | undefined {
+ if (channel === "C") return undefined;
+ const control = channel === "A" ? data[timer.TCCR1] : data[timer.GTCCR];
+ const pwmEnabled = (control & 0x40) !== 0; // PWM1A in TCCR1, PWM1B in GTCCR: both bit 6
+ const com = (control >> 4) & 3;
+ if (!pwmEnabled || com < 2) return undefined;
+ const top = data[timer.OCR1C];
+ if (top === 0) return undefined;
+ const ocr = channel === "A" ? data[timer.OCR1A] : data[timer.OCR1B];
+ const duty = Math.round((Math.min(ocr, top) * 255) / top);
+ return com === 3 ? 255 - duty : duty;
+}
+
+function pwmTop(data: Uint8Array, timer: AvrTimerRegisters): number | undefined {
const wgmLow = data[timer.TCCRA] & 3;
if (timer.bits === 8) {
const mode = wgmLow | ((data[timer.TCCRB] >> 1) & 4);
diff --git a/packages/avr-emulator/src/io/serial-ports.ts b/packages/avr-emulator/src/io/serial-ports.ts
index 7b4bf87f3..167d9b5ff 100644
--- a/packages/avr-emulator/src/io/serial-ports.ts
+++ b/packages/avr-emulator/src/io/serial-ports.ts
@@ -1,8 +1,15 @@
import type { AVRUSART } from "avr8js";
import type { SerialPortHardware } from "../boards/types";
import type { Machine } from "../machine";
+import { SoftwareUart } from "./software-uart";
import { Utf8StreamDecoder, encodeUtf8 } from "./utf8";
+/** What a serial port needs of its transport: a hardware USART or the core's software UART (ATtiny85). */
+type SerialChannel = Pick<
+ AVRUSART,
+ "onByteTransmit" | "onRxComplete" | "onConfigurationChange" | "rxEnable" | "rxBusy" | "writeByte" | "baudRate" | "bitsPerChar" | "stopBits" | "parityEnabled" | "parityOdd"
+>;
+
/** Input limits as in UnoSim today (plan P1.7): per message and pending per port. */
export const SERIAL_INPUT_LIMITS = { maxMessageBytes: 4 * 1024, maxPendingBytes: 1024 * 1024 } as const;
@@ -21,7 +28,9 @@ export interface SerialOutput {
interface Port {
readonly hardware: SerialPortHardware;
- readonly usart: AVRUSART;
+ readonly usart: SerialChannel;
+ /** Set for a software UART: its fallback baud rate follows the Serial Monitor. */
+ readonly software?: SoftwareUart;
readonly decoder: Utf8StreamDecoder;
output: string;
/** Bytes waiting for the receiver, oldest first. */
@@ -50,9 +59,10 @@ export class SerialPorts {
private readonly onDiscard: (port: number, bytes: number) => void = () => undefined,
) {
for (const hardware of machine.board.serialPorts) {
- const usart = machine.peripherals.usarts[hardware.usart];
- if (!usart) throw new RangeError(`${machine.mcu.id} has no USART${hardware.usart}`);
- const port: Port = { hardware, usart, decoder: new Utf8StreamDecoder(), output: "", rx: [], rxHead: 0 };
+ const software = hardware.kind === "software" ? softwareUart(machine, hardware) : undefined;
+ const usart = software ?? (hardware.kind === "usart" ? machine.peripherals.usarts[hardware.usart] : undefined);
+ if (!usart) throw new RangeError(`${machine.mcu.id} has no USART${hardware.kind === "usart" ? hardware.usart : ""}`);
+ const port: Port = { hardware, usart, ...(software ? { software } : {}), decoder: new Utf8StreamDecoder(), output: "", rx: [], rxHead: 0 };
usart.onByteTransmit = (byte) => {
port.output += port.decoder.decode([byte]);
};
@@ -97,7 +107,17 @@ export class SerialPorts {
return port.rx.length - port.rxHead;
}
- /** Frame format the sketch configured (UBRR, U2X, UCSRC). */
+ /**
+ * Baud rate of a software UART until its output reveals the sketch's
+ * (`Serial.begin(baud)` sets no register); the Serial Monitor's setting.
+ * Ignored for a hardware USART, whose rate is in UBRR.
+ */
+ setFallbackBaud(portIndex: number, baud: number): void {
+ const { software } = this.port(portIndex);
+ if (software) software.configuredBaud = baud;
+ }
+
+ /** Frame format the sketch configured (UBRR, U2X, UCSRC; for a software UART the rate it found, 8N1). */
frameConfig(portIndex: number): SerialFrameConfig {
const { usart } = this.port(portIndex);
return { baud: usart.baudRate, dataBits: usart.bitsPerChar, parity: parityOf(usart), stopBits: usart.stopBits };
@@ -130,7 +150,17 @@ export class SerialPorts {
}
}
-function parityOf(usart: AVRUSART): SerialFrameConfig["parity"] {
+/** The core's software UART on the pins of `hardware`; both must be on the same port. */
+function softwareUart(machine: Machine, hardware: SerialPortHardware): SoftwareUart {
+ const pinOf = (number: number) => machine.board.pins.find((pin) => pin.pin === number)?.port;
+ const tx = pinOf(hardware.txPin);
+ const rx = pinOf(hardware.rxPin);
+ const io = tx && machine.peripherals.ports.get(tx.port);
+ if (!tx || !rx || !io || tx.port !== rx.port) throw new RangeError(`serial port ${hardware.index} needs TX and RX on one port`);
+ return new SoftwareUart(machine.cpu, io, machine.board.clockHz, { txPin: tx.bit, rxPin: rx.bit });
+}
+
+function parityOf(usart: SerialChannel): SerialFrameConfig["parity"] {
if (!usart.parityEnabled) return "none";
return usart.parityOdd ? "odd" : "even";
}
diff --git a/packages/avr-emulator/src/io/software-uart.ts b/packages/avr-emulator/src/io/software-uart.ts
new file mode 100644
index 000000000..db80cfe9a
--- /dev/null
+++ b/packages/avr-emulator/src/io/software-uart.ts
@@ -0,0 +1,328 @@
+import type { AVRIOPort, CPU } from "avr8js";
+
+/**
+ * `Serial` of the ATtiny85 (plan P6.3, spike P0.5): the core's
+ * `TinySoftwareSerial` bit-bangs TX on a pin and receives through the analog
+ * comparator. It offers the members of `AVRUSART` that `SerialPorts` uses, so
+ * a board without a hardware USART gets the same serial port.
+ *
+ * - **TX:** edges of the TX pin while `Serial` is active (after
+ * `Serial.begin()`, which also switches the comparator on), the pin is an
+ * output and timer 0 does not drive it as PWM; a sketch that toggles PB0
+ * without `Serial` sends nothing. The baud rate is in no register: among the
+ * standard rates of which the edge distances are whole bit multiples, the one
+ * whose frames end with a stop bit wins (a multiple of the true rate fits
+ * the distances too, but breaks the frames), the bit width is then measured
+ * from the distances (the core's delay loop runs a few percent slow). Until
+ * enough edges fix it, the configured baud rate (the sketch's) decodes the
+ * line once it is idle.
+ * - **RX:** a waveform on the RX pin (AIN1) in simulated time; the comparator
+ * (ACSR) compares it with the bandgap (ACBG) or AIN0 and raises its
+ * interrupt. The receiver counts as enabled while the comparator interrupt
+ * is (after `Serial.begin()`). Half duplex: the core disables interrupts
+ * while it sends, so bits arriving then are lost, as on the chip.
+ */
+export interface SoftwareUartPins {
+ readonly txPin: number;
+ readonly rxPin: number;
+}
+
+/** Register addresses of the ATtiny85 the UART model reads (data space). */
+const PINB = 0x36;
+const DDRB = 0x37;
+const PORTB = 0x38;
+const TCCR0A = 0x4a;
+const ACSR = 0x28;
+/** COM0A bits: timer 0 drives PB0 (OC0A). */
+const COM0A_MASK = 0xc0;
+const ACD = 0x80;
+const ACBG = 0x40;
+const ACO = 0x20;
+const ACI = 0x10;
+const ACIE = 0x08;
+const ACIS_MASK = 0x03;
+const BANDGAP_VOLTS = 1.1;
+const VCC_VOLTS = 5;
+
+/** Standard rates, fastest last; the largest fitting bit time wins. */
+const STANDARD_BAUDS = [300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200] as const;
+/** In-frame edge distances needed before the baud rate counts as found. */
+const MIN_FIT_INTERVALS = 8;
+/**
+ * Share of them that must be whole multiples of the bit time. The pin set-up
+ * makes a few short pulses and the core's delay loop stretches single bits
+ * (measured at 19200 baud: 3 of 39 distances off by more than 20 %).
+ */
+const MIN_FIT_SHARE = 0.9;
+/** Complete frames whose stop bits confirm a rate before it is used. */
+const MIN_FRAMES = 4;
+/** Idle bits between two received bytes, after the stop bit. */
+const RX_IDLE_BITS = 2;
+
+type Edge = readonly [cycle: number, level: 0 | 1];
+
+export class SoftwareUart {
+ onByteTransmit: ((byte: number) => void) | null = null;
+ onRxComplete: (() => void) | null = null;
+ onConfigurationChange: (() => void) | null = null;
+
+ readonly bitsPerChar = 8;
+ readonly stopBits = 1;
+ readonly parityEnabled = false;
+ readonly parityOdd = false;
+
+ /** Baud rate used until the TX line reveals the sketch's (`Serial.begin(baud)` is not visible otherwise). */
+ configuredBaud = 9600;
+
+ private detectedBaud: number | null = null;
+ /** Measured cycles per TX bit once the baud rate is found. */
+ private txBitCycles: number | null = null;
+ private edges: Edge[] = [];
+ private txLevel: 0 | 1 = 1;
+ private frameStart: number | null = null;
+ private idleCheck: (() => void) | null = null;
+ private rxLevel: 0 | 1 = 1;
+ private rxBusyValue = false;
+ private rxEnabledValue = false;
+
+ constructor(
+ private readonly cpu: CPU,
+ private readonly port: AVRIOPort,
+ private readonly clockHz: number,
+ private readonly pins: SoftwareUartPins = { txPin: 0, rxPin: 1 },
+ ) {
+ port.addListener(() => this.onPortChange());
+ this.installComparator();
+ this.setRx(1);
+ }
+
+ /** The detected baud rate of the TX line, else the configured one. */
+ get baudRate(): number {
+ return this.detectedBaud ?? this.configuredBaud;
+ }
+
+ get rxEnable(): boolean {
+ return this.rxEnabledValue;
+ }
+
+ get rxBusy(): boolean {
+ return this.rxBusyValue;
+ }
+
+ /** Sends one byte as a waveform on the RX pin; false while a byte is on the line or the receiver is off. */
+ writeByte(byte: number): boolean {
+ if (!this.rxEnabledValue || this.rxBusyValue) return false;
+ this.rxBusyValue = true;
+ const bit = this.clockHz / this.baudRate;
+ const levels: Array<0 | 1> = [0, ...Array.from({ length: 8 }, (_, index) => ((byte >> index) & 1) as 0 | 1), 1];
+ levels.forEach((level, index) => {
+ this.cpu.addClockEvent(() => this.setRx(level), Math.max(1, Math.round(index * bit)));
+ });
+ this.cpu.addClockEvent(() => {
+ this.rxBusyValue = false;
+ this.onRxComplete?.();
+ }, Math.round((levels.length + RX_IDLE_BITS) * bit));
+ return true;
+ }
+
+ // ---------- TX ----------
+
+ private onPortChange(): void {
+ const { data } = this.cpu;
+ const mask = 1 << this.pins.txPin;
+ const timerDriven = this.pins.txPin === 0 && (data[TCCR0A] & COM0A_MASK) !== 0;
+ if (!this.rxEnabledValue || !(data[DDRB] & mask) || timerDriven) return;
+ const level: 0 | 1 = data[PORTB] & mask ? 1 : 0;
+ if (level === this.txLevel) return;
+ this.txLevel = level;
+ this.onEdge([this.cpu.cycles, level]);
+ }
+
+ private onEdge(edge: Edge): void {
+ this.edges.push(edge);
+ if (this.txBitCycles === null) {
+ this.tryDetectBaud();
+ this.scheduleIdleCheck();
+ return;
+ }
+ if (this.frameStart === null && edge[1] === 0) this.startFrame(edge[0]);
+ }
+
+ private startFrame(start: number): void {
+ const bit = this.txBitCycles as number;
+ this.frameStart = start;
+ this.cpu.addClockEvent(() => this.finishFrame(), Math.max(1, Math.ceil(start + 9.5 * bit - this.cpu.cycles)));
+ }
+
+ private finishFrame(): void {
+ if (this.frameStart === null || this.txBitCycles === null) return;
+ const start = this.frameStart;
+ const valid = this.isStartBit(start, this.txBitCycles);
+ if (valid) this.emit(this.frameByte(start, this.txBitCycles));
+ // A pulse that was no start bit ends at itself; the next falling edge may be a real one.
+ const end = valid ? start + 9.5 * this.txBitCycles : start;
+ this.frameStart = null;
+ // Keep only the edges after this frame; a start bit among them opens the next one.
+ this.edges = this.edges.filter(([cycle]) => cycle > end);
+ const next = this.edges.find(([, level]) => level === 0);
+ if (next) this.startFrame(next[0]);
+ }
+
+ /** Decodes every complete frame among the buffered edges with `bit` cycles per bit; keeps an open frame. */
+ private decodeBuffered(bit: number, now: number): void {
+ let index = 0;
+ while (index < this.edges.length) {
+ const [start, level] = this.edges[index];
+ if (level !== 0) {
+ index++;
+ continue;
+ }
+ const end = start + 9.5 * bit;
+ if (end > now) break;
+ if (!this.isStartBit(start, bit)) {
+ index++;
+ continue;
+ }
+ this.emit(this.frameByte(start, bit));
+ while (index < this.edges.length && this.edges[index][0] <= end) index++;
+ }
+ this.edges = this.edges.slice(index);
+ }
+
+ /** A start bit is still low at its middle; a shorter pulse (pin set-up) is no byte. */
+ private isStartBit(start: number, bit: number): boolean {
+ return this.levelAt(start + bit / 2) === 0;
+ }
+
+ private frameByte(start: number, bit: number): number {
+ let byte = 0;
+ for (let index = 0; index < 8; index++) byte |= this.levelAt(start + bit * (1.5 + index)) << index;
+ return byte;
+ }
+
+ /** Line level at `cycle` from the buffered edges (idle high before the first). */
+ private levelAt(cycle: number): 0 | 1 {
+ let level: 0 | 1 = 1;
+ for (const [at, value] of this.edges) {
+ if (at > cycle) break;
+ level = value;
+ }
+ return level;
+ }
+
+ private tryDetectBaud(): void {
+ const intervals = this.edges.slice(1).map(([cycle], index) => cycle - this.edges[index][0]);
+ let best: { baud: number; bit: number; errors: number } | undefined;
+ for (const baud of STANDARD_BAUDS) {
+ const nominal = this.clockHz / baud;
+ const inFrame = intervals.filter((distance) => distance < nominal * 9.5);
+ if (inFrame.length < MIN_FIT_INTERVALS) continue;
+ const fitting = inFrame.filter((distance) => {
+ const bits = Math.round(distance / nominal);
+ return bits >= 1 && Math.abs(distance - bits * nominal) < nominal * 0.2;
+ });
+ if (fitting.length < inFrame.length * MIN_FIT_SHARE) continue;
+ const bit = fitting.reduce((sum, distance) => sum + distance / Math.round(distance / nominal), 0) / fitting.length;
+ const frames = this.frameCheck(bit);
+ // Every fitting rate is judged on enough frames: a faster one completes its shorter frames first.
+ if (frames.complete < MIN_FRAMES) return;
+ // Fewest framing errors wins; on a tie the slower rate (the larger bit time).
+ if (!best || frames.errors < best.errors) best = { baud, bit, errors: frames.errors };
+ }
+ if (best?.errors === 0) this.lock(best.baud, best.bit);
+ }
+
+ /** Complete frames among the buffered edges at `bit` cycles per bit, and how many of them lack their stop bit. */
+ private frameCheck(bit: number): { complete: number; errors: number } {
+ let complete = 0;
+ let errors = 0;
+ let index = 0;
+ while (index < this.edges.length) {
+ const [start, level] = this.edges[index];
+ if (level !== 0 || !this.isStartBit(start, bit)) {
+ index++;
+ continue;
+ }
+ const end = start + 9.5 * bit;
+ if (end > this.cpu.cycles) break;
+ complete++;
+ if (this.levelAt(end) !== 1) errors++;
+ while (index < this.edges.length && this.edges[index][0] <= end) index++;
+ }
+ return { complete, errors };
+ }
+
+ private lock(baud: number, bitCycles: number): void {
+ this.detectedBaud = baud;
+ this.txBitCycles = bitCycles;
+ if (this.idleCheck) this.cpu.clearClockEvent(this.idleCheck);
+ this.idleCheck = null;
+ this.decodeBuffered(bitCycles, this.cpu.cycles);
+ const open = this.edges.find(([, level]) => level === 0);
+ if (open) this.startFrame(open[0]);
+ this.onConfigurationChange?.();
+ }
+
+ /** Before the baud rate is found, an idle line is decoded with the configured one. */
+ private scheduleIdleCheck(): void {
+ if (this.idleCheck) this.cpu.clearClockEvent(this.idleCheck);
+ const bit = this.clockHz / this.configuredBaud;
+ this.idleCheck = this.cpu.addClockEvent(() => {
+ this.idleCheck = null;
+ if (this.txBitCycles === null) this.decodeBuffered(bit, this.cpu.cycles);
+ }, Math.round(12 * bit));
+ }
+
+ private emit(byte: number): void {
+ this.onByteTransmit?.(byte);
+ }
+
+ // ---------- RX: analog comparator ----------
+
+ /** Analog comparator interrupt, vector 7. */
+ private readonly comparatorInterrupt = { address: 7, flagRegister: ACSR, flagMask: ACI, enableRegister: ACSR, enableMask: ACIE };
+
+ private comparatorOutput(): 0 | 1 {
+ const { data } = this.cpu;
+ const acsr = data[ACSR];
+ if (acsr & ACD) return 0;
+ const ain0 = data[PINB] & (1 << this.pins.txPin) ? VCC_VOLTS : 0;
+ const positive = acsr & ACBG ? BANDGAP_VOLTS : ain0;
+ const negative = this.rxLevel ? VCC_VOLTS : 0; // AIN1, ACME is not modelled
+ return positive > negative ? 1 : 0;
+ }
+
+ private updateComparator(previous: 0 | 1): void {
+ const { data } = this.cpu;
+ const output = this.comparatorOutput();
+ data[ACSR] = (data[ACSR] & ~ACO) | (output ? ACO : 0);
+ const mode = data[ACSR] & ACIS_MASK; // 0 toggle, 2 falling, 3 rising
+ const edge = output !== previous && (mode === 0 || (mode === 3 && output === 1) || (mode === 2 && output === 0));
+ if (edge && !(data[ACSR] & ACD)) this.cpu.setInterruptFlag(this.comparatorInterrupt);
+ }
+
+ private installComparator(): void {
+ const { cpu } = this;
+ cpu.writeHooks[ACSR] = (value) => {
+ const previous: 0 | 1 = cpu.data[ACSR] & ACO ? 1 : 0;
+ // ACO is read-only; ACI is cleared by writing 1.
+ cpu.data[ACSR] = (value & ~(ACO | ACI)) | (cpu.data[ACSR] & (ACO | ACI));
+ if (value & ACI) cpu.clearInterruptByFlag(this.comparatorInterrupt, value);
+ cpu.updateInterruptEnable(this.comparatorInterrupt, value);
+ this.updateComparator(previous);
+ const enabled = (value & ACIE) !== 0 && (value & ACD) === 0;
+ if (enabled !== this.rxEnabledValue) {
+ this.rxEnabledValue = enabled;
+ this.onConfigurationChange?.();
+ }
+ return true;
+ };
+ }
+
+ private setRx(level: 0 | 1): void {
+ const previous = this.comparatorOutput();
+ this.rxLevel = level;
+ if (!(this.cpu.data[DDRB] & (1 << this.pins.rxPin))) this.port.setPin(this.pins.rxPin, level === 1);
+ this.updateComparator(previous);
+ }
+}
diff --git a/packages/avr-emulator/src/mcu/attiny85.ts b/packages/avr-emulator/src/mcu/attiny85.ts
new file mode 100644
index 000000000..121d0a884
--- /dev/null
+++ b/packages/avr-emulator/src/mcu/attiny85.ts
@@ -0,0 +1,210 @@
+import {
+ ADCMuxInputType, ADCReference, ATtinyTimer1, AVRADC, AVRClock, AVREEPROM, AVRIOPort, AVRTimer, AVRWatchdog,
+ adcConfig, attinyTimer1Config, eepromConfig, timer0Config, watchdogConfig,
+} from "avr8js";
+import type { ADCConfig, AVRPortConfig, AVRTimerConfig, CPU } from "avr8js";
+import type { TimerRegisters } from "../io/pwm";
+import { ArrayEepromBackend } from "./eeprom";
+import { MCU_FLASH_BYTES } from "./mcus";
+import type { McuProfile } from "./types";
+
+/**
+ * ATtiny85 (plan P6.2; spike P0.5). One port (B), timer 0, the ATtiny timer 1,
+ * a 4-channel ADC, EEPROM, watchdog, no USART (the core's `Serial` is a
+ * software UART, P6.3). Addresses are the I/O addresses of `avr/iotnx5.h` plus
+ * 0x20; the vector table has one word per entry (RJMP), so vector n sits at
+ * word address n.
+ */
+const VECTOR = {
+ int0: 1,
+ pcint0: 2,
+ timer1CompA: 3,
+ timer1Ovf: 4,
+ timer0Ovf: 5,
+ eepromReady: 6,
+ analogComparator: 7, // receive model of the software UART (P6.3)
+ adc: 8,
+ timer1CompB: 9,
+ timer0CompA: 10,
+ timer0CompB: 11,
+ wdt: 12,
+} as const;
+
+const PINB = 0x36;
+const DDRB = 0x37;
+const PORTB = 0x38;
+const ADCL = 0x24;
+const ADCH = 0x25;
+const ADCSRA = 0x26;
+const ADMUX = 0x27;
+const ADCSRB = 0x23;
+const DIDR0 = 0x34;
+const PCMSK = 0x35;
+const MCUSR = 0x54;
+const MCUCR = 0x55;
+const GIFR = 0x5a;
+const GIMSK = 0x5b;
+const WDTCR = 0x41;
+const CLKPR = 0x46;
+/** GIMSK/GIFR bits: INT0 and the pin-change interrupt share both registers. */
+const INT0_BIT = 6;
+const PCIE_BIT = 5;
+
+/** INT0 on PB2 (ISC01:ISC00 in MCUCR bits 1:0); pin change on PB0–PB5. */
+const portB: AVRPortConfig = {
+ PIN: PINB,
+ DDR: DDRB,
+ PORT: PORTB,
+ pinChange: { PCIE: PCIE_BIT, PCICR: GIMSK, PCIFR: GIFR, PCMSK, pinChangeInterrupt: VECTOR.pcint0, mask: 0x3f, offset: 0 },
+ externalInterrupts: [null, null, { EICR: MCUCR, EIMSK: GIMSK, EIFR: GIFR, index: INT0_BIT, iscOffset: 0, interrupt: VECTOR.int0 }],
+};
+
+type InterruptConfig = Parameters[0];
+
+/** The same interrupts as avr8js keeps them inside the port, for the shared registers below. */
+const int0Interrupt: InterruptConfig = { address: VECTOR.int0, flagRegister: GIFR, flagMask: 1 << INT0_BIT, enableRegister: GIMSK, enableMask: 1 << INT0_BIT };
+const pcintInterrupt: InterruptConfig = { address: VECTOR.pcint0, flagRegister: GIFR, flagMask: 1 << PCIE_BIT, enableRegister: GIMSK, enableMask: 1 << PCIE_BIT };
+
+/**
+ * avr8js hooks GIMSK only for the external interrupts and gives GIFR to the
+ * pin-change interrupt alone (the ATmega has separate registers for both).
+ * Here a write to GIMSK also enables PCINT0, and a 1 written to GIFR also
+ * clears INTF0.
+ */
+function shareInterruptRegisters(cpu: CPU): void {
+ const maskHook = cpu.writeHooks[GIMSK];
+ cpu.writeHooks[GIMSK] = (value, oldValue, address, mask) => {
+ const handled = maskHook?.(value, oldValue, address, mask) ?? false;
+ cpu.updateInterruptEnable(pcintInterrupt, value);
+ return handled;
+ };
+ const flagHook = cpu.writeHooks[GIFR];
+ cpu.writeHooks[GIFR] = (value, oldValue, address, mask) => {
+ cpu.clearInterruptByFlag(int0Interrupt, value);
+ return flagHook?.(value, oldValue, address, mask) ?? true;
+ };
+}
+
+/** Timer 0: own registers and vectors; OC0A on PB0, OC0B on PB1, T0 on PB2. */
+const timer0: AVRTimerConfig = {
+ ...timer0Config,
+ TCCRA: 0x4a,
+ TCCRB: 0x53,
+ TCNT: 0x52,
+ OCRA: 0x49,
+ OCRB: 0x48,
+ TIFR: 0x58,
+ TIMSK: 0x59,
+ TOV: 1 << 1,
+ OCFA: 1 << 4,
+ OCFB: 1 << 3,
+ TOIE: 1 << 1,
+ OCIEA: 1 << 4,
+ OCIEB: 1 << 3,
+ ovfInterrupt: VECTOR.timer0Ovf,
+ compAInterrupt: VECTOR.timer0CompA,
+ compBInterrupt: VECTOR.timer0CompB,
+ compPortA: PORTB,
+ compPinA: 0,
+ compPortB: PORTB,
+ compPinB: 1,
+ externalClockPort: PORTB,
+ externalClockPin: 2,
+};
+
+const adc: ADCConfig = {
+ ...adcConfig,
+ ADMUX,
+ ADCSRA,
+ ADCSRB,
+ ADCL,
+ ADCH,
+ DIDR0,
+ adcInterrupt: VECTOR.adc,
+ numChannels: 4,
+ muxInputMask: 0x0f,
+ // ADC0–3 on PB5, PB2, PB4, PB3; 1.1 V, GND and the temperature sensor; differential inputs are not modelled.
+ muxChannels: {
+ 0: { type: ADCMuxInputType.SingleEnded, channel: 0 },
+ 1: { type: ADCMuxInputType.SingleEnded, channel: 1 },
+ 2: { type: ADCMuxInputType.SingleEnded, channel: 2 },
+ 3: { type: ADCMuxInputType.SingleEnded, channel: 3 },
+ 12: { type: ADCMuxInputType.Constant, voltage: 1.1 },
+ 13: { type: ADCMuxInputType.Constant, voltage: 0 },
+ 15: { type: ADCMuxInputType.Temperature },
+ },
+ // Index REFS2:REFS1:REFS0 (datasheet table 17-3): VCC, AREF (PB0), 1.1 V, reserved, VCC, AREF, 2.56 V, 2.56 V.
+ adcReferences: [
+ ADCReference.AVCC, ADCReference.AREF, ADCReference.Internal1V1, ADCReference.Reserved,
+ ADCReference.AVCC, ADCReference.AREF, ADCReference.Internal2V56, ADCReference.Internal2V56,
+ ],
+};
+
+/** avr8js 0.21.1 reads REFS2 at bit 3 (MUX3 on the ATtiny85); here it is bit 4 (spike P0.5: 102 instead of 200 at 2.56 V). */
+class Attiny85ADC extends AVRADC {
+ private readonly dataSpace: Uint8Array;
+
+ constructor(cpu: CPU, config: ADCConfig) {
+ super(cpu, config);
+ this.dataSpace = cpu.data;
+ }
+
+ override get referenceVoltageType(): ADCReference {
+ const value = this.dataSpace[ADMUX];
+ return adc.adcReferences[((value >> 6) & 0x03) | (value & 0x10 ? 0x04 : 0)] ?? ADCReference.Reserved;
+ }
+}
+
+const timerRegisters = ({ bits, TCCRA, TCCRB, OCRA, OCRB, OCRC, ICR }: AVRTimerConfig): TimerRegisters => ({ bits, TCCRA, TCCRB, OCRA, OCRB, OCRC, ICR });
+
+/** avr8js times EEPROM writes for 16 MHz; 1.8 ms each for erase and write at the clock of the board. */
+const eepromCycles = (clockHz: number): number => Math.round(clockHz * 0.0018);
+
+export const ATTINY85: McuProfile = {
+ id: "attiny85",
+ flashBytes: MCU_FLASH_BYTES.attiny85,
+ memory: { sramStart: 0x60, sramBytes: 512 },
+ eepromBytes: 512,
+ adcRegisters: {
+ ADCSRA,
+ ADMUX,
+ ADCL,
+ ADCH,
+ channel: (data) => data[ADMUX] & 0x0f,
+ },
+ sleepEnable: { register: MCUCR, mask: 0x20 }, // MCUCR.SE
+ timerRegisters: [
+ timerRegisters(timer0),
+ {
+ kind: "attiny-timer1",
+ TCCR1: attinyTimer1Config.TCCR1,
+ GTCCR: attinyTimer1Config.GTCCR,
+ OCR1A: attinyTimer1Config.OCR1A,
+ OCR1B: attinyTimer1Config.OCR1B,
+ OCR1C: attinyTimer1Config.OCR1C,
+ },
+ ],
+ createPeripherals(cpu, clockHz, eeprom) {
+ const clock = new AVRClock(cpu, clockHz, { CLKPR });
+ const port = new AVRIOPort(cpu, portB);
+ shareInterruptRegisters(cpu);
+ return {
+ ports: new Map([["B", port]]),
+ timers: [new AVRTimer(cpu, timer0), new ATtinyTimer1(cpu, attinyTimer1Config)],
+ usarts: [],
+ adc: new Attiny85ADC(cpu, adc),
+ eeprom: new AVREEPROM(cpu, new ArrayEepromBackend(eeprom), {
+ ...eepromConfig,
+ EECR: 0x3c,
+ EEDR: 0x3d,
+ EEARL: 0x3e,
+ EEARH: 0x3f,
+ eepromReadyInterrupt: VECTOR.eepromReady,
+ eraseCycles: eepromCycles(clockHz),
+ writeCycles: eepromCycles(clockHz),
+ }),
+ watchdog: new AVRWatchdog(cpu, { ...watchdogConfig, MCUSR, WDTCSR: WDTCR, watchdogInterrupt: VECTOR.wdt }, clock),
+ clock,
+ };
+ },
+};
diff --git a/packages/avr-emulator/src/mcu/registry.ts b/packages/avr-emulator/src/mcu/registry.ts
index 5e9108734..b75d4b934 100644
--- a/packages/avr-emulator/src/mcu/registry.ts
+++ b/packages/avr-emulator/src/mcu/registry.ts
@@ -1,10 +1,10 @@
import { ATMEGA2560 } from "./atmega2560";
import { ATMEGA328P } from "./atmega328p";
+import { ATTINY85 } from "./attiny85";
import type { McuId } from "./mcus";
import type { McuProfile } from "./types";
-/** ATtiny85 (P6.2) follows. */
-const PROFILES: Partial> = { atmega328p: ATMEGA328P, atmega2560: ATMEGA2560 };
+const PROFILES: Partial> = { atmega328p: ATMEGA328P, atmega2560: ATMEGA2560, attiny85: ATTINY85 };
export function mcuProfile(id: McuId): McuProfile {
const profile = PROFILES[id];
diff --git a/packages/avr-emulator/src/mcu/types.ts b/packages/avr-emulator/src/mcu/types.ts
index 3ac86ebdf..62f114529 100644
--- a/packages/avr-emulator/src/mcu/types.ts
+++ b/packages/avr-emulator/src/mcu/types.ts
@@ -1,4 +1,4 @@
-import type { AVRADC, AVRClock, AVREEPROM, AVRIOPort, AVRTimer, AVRUSART, AVRWatchdog, CPU } from "avr8js";
+import type { ATtinyTimer1, AVRADC, AVRClock, AVREEPROM, AVRIOPort, AVRTimer, AVRUSART, AVRWatchdog, CPU } from "avr8js";
import type { PortName } from "../boards/types";
import type { TimerRegisters } from "../io/pwm";
import type { MemoryLayout } from "./memory";
@@ -6,7 +6,8 @@ import type { McuId } from "./mcus";
export interface McuPeripherals {
readonly ports: ReadonlyMap;
- readonly timers: readonly AVRTimer[];
+ /** By timer number; timer 1 of the ATtiny85 is the ATtiny-specific one of avr8js. */
+ readonly timers: readonly (AVRTimer | ATtinyTimer1)[];
readonly usarts: readonly AVRUSART[];
readonly adc: AVRADC;
readonly eeprom: AVREEPROM;
diff --git a/packages/avr-emulator/src/protocol/commands.ts b/packages/avr-emulator/src/protocol/commands.ts
index 29d756f53..0a4ea2664 100644
--- a/packages/avr-emulator/src/protocol/commands.ts
+++ b/packages/avr-emulator/src/protocol/commands.ts
@@ -24,9 +24,10 @@ const parserTable: Record Co
return { ok: true, command: { type: "load", runId, board, artifactBoard, buildId, sha256, flash, symbols: symbols as unknown as ImageSymbols } };
},
start(fields, runId) {
- const { timeoutMs } = fields;
+ const { timeoutMs, serialBaud } = fields;
if (!isInteger(timeoutMs, COMMAND_LIMITS.minTimeoutMs, COMMAND_LIMITS.maxTimeoutMs)) return fail("invalid timeoutMs");
- return { ok: true, command: { type: "start", runId, timeoutMs } };
+ if (serialBaud !== undefined && !isInteger(serialBaud, COMMAND_LIMITS.minSerialBaud, COMMAND_LIMITS.maxSerialBaud)) return fail("invalid serialBaud");
+ return { ok: true, command: { type: "start", runId, timeoutMs, ...(serialBaud === undefined ? {} : { serialBaud }) } };
},
pause: (_fields, runId) => ({ ok: true, command: { type: "pause", runId } }),
resume: (_fields, runId) => ({ ok: true, command: { type: "resume", runId } }),
diff --git a/packages/avr-emulator/src/protocol/messages.ts b/packages/avr-emulator/src/protocol/messages.ts
index 3d1c41e07..e9e39f86c 100644
--- a/packages/avr-emulator/src/protocol/messages.ts
+++ b/packages/avr-emulator/src/protocol/messages.ts
@@ -18,6 +18,9 @@ export const COMMAND_LIMITS = {
/** Largest flash of any supported MCU (ATmega2560). */
maxFlashBytes: 256 * 1024,
maxBuildIdLength: 256,
+ /** Fallback baud rate of a software UART (`serialBaud` of `start`). */
+ minSerialBaud: 300,
+ maxSerialBaud: 1_000_000,
} as const;
/** Commands from the UI to the worker; `load`, `start` and `reset` begin a new run (plan 4.5). */
@@ -36,7 +39,16 @@ export type Command =
readonly flash: ArrayBuffer;
readonly symbols: ImageSymbols;
}
- | { readonly type: "start"; readonly runId: number; readonly timeoutMs: number }
+ | {
+ readonly type: "start";
+ readonly runId: number;
+ readonly timeoutMs: number;
+ /**
+ * Baud rate a software UART (ATtiny85) decodes with until the line reveals
+ * the sketch's: the `Serial.begin()` value of the sketch. Hardware USARTs ignore it.
+ */
+ readonly serialBaud?: number;
+ }
| { readonly type: "pause" | "resume" | "stop" | "reset"; readonly runId: number }
| { readonly type: "serialIn"; readonly runId: number; readonly port: number; readonly text: string }
| { readonly type: "setDigital"; readonly runId: number; readonly pin: number; readonly level: 0 | 1 }
diff --git a/packages/avr-emulator/src/worker/host.ts b/packages/avr-emulator/src/worker/host.ts
index 957036189..eda37aa8f 100644
--- a/packages/avr-emulator/src/worker/host.ts
+++ b/packages/avr-emulator/src/worker/host.ts
@@ -80,6 +80,9 @@ export class WorkerHost {
case "start":
this.withEmulator("idle", (emulator) => {
emulator.configureTimeout(command.timeoutMs);
+ if (command.serialBaud !== undefined) {
+ for (const port of emulator.serial.portIndexes) emulator.serial.setFallbackBaud(port, command.serialBaud);
+ }
emulator.start();
this.batcher.addStatus("running");
});
diff --git a/packages/avr-emulator/tests/atmega328p-boards.test.ts b/packages/avr-emulator/tests/atmega328p-boards.test.ts
index 1c782b8f3..2dd998abe 100644
--- a/packages/avr-emulator/tests/atmega328p-boards.test.ts
+++ b/packages/avr-emulator/tests/atmega328p-boards.test.ts
@@ -1,6 +1,7 @@
import { CPU } from "avr8js";
import { describe, expect, it } from "vitest";
import { ATMEGA328P, NANO, UNO, boardHardware, hasBoardHardware, mcuProfile } from "../src/index";
+import type { BoardHardwareId, McuId } from "../src/index";
import { machineFromFixture, runSerial } from "./machine-helpers";
describe("board hardware Uno and Nano", () => {
@@ -36,9 +37,10 @@ describe("board hardware Uno and Nano", () => {
it("knows the boards with a profile and refuses the others for now", () => {
expect(boardHardware("nano")).toBe(NANO);
expect(hasBoardHardware("uno")).toBe(true);
- expect(hasBoardHardware("attiny85")).toBe(false);
- expect(() => boardHardware("attiny85")).toThrow(/no hardware profile yet/);
- expect(() => mcuProfile("attiny85")).toThrow(/no profile yet/);
+ // Every board id has hardware since P6.4; an id without it is still refused.
+ expect(hasBoardHardware("unknown" as BoardHardwareId)).toBe(false);
+ expect(() => boardHardware("unknown" as BoardHardwareId)).toThrow(/no hardware profile yet/);
+ expect(() => mcuProfile("unknown" as McuId)).toThrow(/no profile yet/);
});
});
diff --git a/packages/avr-emulator/tests/attiny85-board.test.ts b/packages/avr-emulator/tests/attiny85-board.test.ts
new file mode 100644
index 000000000..c8638dbdb
--- /dev/null
+++ b/packages/avr-emulator/tests/attiny85-board.test.ts
@@ -0,0 +1,96 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+import { ATTINY85, ATTINY85_BOARD, Emulator, boardHardware, buildSimulationImage, hasBoardHardware, loadSimulationImage, mcuProfile } from "../src/index";
+import { FakeClock } from "./fake-clock";
+
+function emulator(fixture: string): Emulator {
+ const hex = readFileSync(join(__dirname, "fixtures", `attiny85-${fixture}.hex`), "utf8");
+ return new Emulator(loadSimulationImage(buildSimulationImage({ board: "attiny85", hex })), { clock: new FakeClock(), timeoutMs: 60_000 });
+}
+
+const text = (e: Emulator) => e.takeSerialOutput().map((output) => output.text).join("").replaceAll("\r\n", "\n");
+
+/** Advances in 25 ms steps until `done` holds or `maxMs` passed (emulation is costly under coverage). */
+function advanceUntil(e: Emulator, done: () => boolean, maxMs: number): void {
+ for (let ms = 0; ms < maxMs && !done(); ms += 25) e.advance(25);
+}
+
+describe("board hardware ATtiny85 (plan P6.4)", () => {
+ it("is registered with its MCU profile", () => {
+ expect(hasBoardHardware("attiny85")).toBe(true);
+ expect(boardHardware("attiny85")).toBe(ATTINY85_BOARD);
+ expect(mcuProfile("attiny85")).toBe(ATTINY85);
+ expect(ATTINY85_BOARD).toMatchObject({ mcu: "attiny85", clockHz: 8_000_000, vccVolts: 5, ledBuiltin: 1 });
+ });
+
+ it("numbers the pins 0–5 as PB0–PB5 with A0–A3 as second names of 5, 2, 4, 3", () => {
+ expect(ATTINY85_BOARD.pins.map((p) => [p.pin, p.label, p.port?.bit, p.analogName ?? "", p.adcChannel ?? -1])).toEqual([
+ [0, "PB0", 0, "", -1],
+ [1, "PB1", 1, "", -1],
+ [2, "PB2", 2, "A1", 1],
+ [3, "PB3", 3, "A3", 3],
+ [4, "PB4", 4, "A2", 2],
+ [5, "PB5", 5, "A0", 0],
+ ]);
+ });
+
+ it("has PWM on 0 (timer 0 A), 1 (timer 1 A) and 4 (timer 1 B), RESET on 5 and Serial on 0/1", () => {
+ expect(ATTINY85_BOARD.pins.filter((p) => p.pwm).map((p) => `${p.pin}:${p.pwm?.timer}${p.pwm?.channel}`)).toEqual(["0:0A", "1:1A", "4:1B"]);
+ expect(ATTINY85_BOARD.pins.find((p) => p.pin === 5)?.role).toBe("reset");
+ expect(ATTINY85_BOARD.pins.filter((p) => p.role === "serial").map((p) => p.pin)).toEqual([0, 1]);
+ expect(ATTINY85_BOARD.serialPorts).toEqual([{ index: 0, kind: "software", rxPin: 1, txPin: 0 }]);
+ });
+});
+
+describe("ATtiny85 in the emulator", () => {
+ it("prints Serial through the software UART as port 0", () => {
+ const e = emulator("tiny_serial");
+ e.setAnalog(2, 512); // A1
+ e.setAnalog(3, 102); // A3: 0.5 V
+ let output = "";
+ const take = () => e.takeSerialOutput().map((chunk) => chunk.text).join("");
+ advanceUntil(e, () => (output += take()).includes("DONE"), 1500);
+ const lines = output.replaceAll("\r\n", "\n").split("\n");
+ expect(lines[0]).toBe("int 32767 + 1 = -32768");
+ expect(lines).toContain("A1 VCC = 512");
+ expect(lines).toContain("DONE");
+ expect(e.serial.frameConfig(0)).toEqual({ baud: 9600, dataBits: 8, parity: "none", stopBits: 1 });
+ });
+
+ it("receives Serial input after Serial.begin() and answers", () => {
+ const e = emulator("tiny_echo");
+ e.advance(200);
+ expect(text(e)).toBe("ready\n");
+ // Without a line ending: a "\n" would stay in the buffer and the next parseInt() return 0, as on the board.
+ expect(e.sendSerial(0, "21")).toBe(true);
+ e.advance(1500);
+ expect(text(e)).toBe("got 21\ntwice 42\n");
+ });
+
+ it("decodes with the Serial Monitor's baud rate until the line reveals the sketch's", () => {
+ const e = emulator("tiny_serial_ff");
+ e.serial.setFallbackBaud(0, 9600);
+ e.advance(100);
+ // The byte arrives; 0xFF is never valid UTF-8, so the decoder shows U+FFFD.
+ expect(e.takeSerialOutput()).toEqual([{ port: 0, text: "\ufffd" }]);
+ expect(e.serial.frameConfig(0).baud).toBe(9600);
+ });
+
+ describe("tiny_probe", () => {
+ const e = emulator("tiny_probe");
+ e.setAnalog(2, 512);
+ // The probe writes its results to EEPROM and ends with the two analogWrite calls after the marker.
+ advanceUntil(e, () => e.machine.peripherals.adc.channelValues.length > 0 && e.pins.state(0).pwm !== undefined, 1000);
+
+ it("shows the analogWrite duty cycles of timer 1 (PB4) and timer 0 (PB0)", () => {
+ expect(e.pins.state(4)).toMatchObject({ mode: "OUTPUT", pwm: 200, level: 1 });
+ expect(e.pins.state(0)).toMatchObject({ mode: "OUTPUT", pwm: 77, level: 0 });
+ expect(e.usage.snapshot().filter((usage) => usage.fn === "analogWrite").map((usage) => usage.pin).sort()).toEqual([0, 4]);
+ });
+
+ it("reads analog inputs by their pin number", () => {
+ expect(e.machine.peripherals.adc.channelValues[1]).toBeCloseTo((512.5 / 1024) * 5, 3);
+ });
+ });
+});
diff --git a/packages/avr-emulator/tests/attiny85-profile.test.ts b/packages/avr-emulator/tests/attiny85-profile.test.ts
new file mode 100644
index 000000000..20d2f9bc0
--- /dev/null
+++ b/packages/avr-emulator/tests/attiny85-profile.test.ts
@@ -0,0 +1,194 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { CPU, PinState, avrInstruction } from "avr8js";
+import { describe, expect, it } from "vitest";
+import { ATTINY85, avr8jsSramParameter, buildSimulationImage, erasedEeprom, loadSimulationImage, ramEnd } from "../src/index";
+import type { McuPeripherals } from "../src/index";
+
+const CLOCK_HZ = 8_000_000;
+const GIFR = 0x5a;
+const GIMSK = 0x5b;
+const PCMSK = 0x35;
+const MCUCR = 0x55;
+
+interface Tiny {
+ readonly cpu: CPU;
+ readonly peripherals: McuPeripherals;
+ readonly eeprom: Uint8Array;
+}
+
+/** A CPU with the ATtiny85 profile, loaded with a fixture or empty. */
+function tiny(fixture?: string): Tiny {
+ const progMem = fixture
+ ? loadSimulationImage(buildSimulationImage({ board: "attiny85", hex: readFileSync(join(__dirname, "fixtures", `attiny85-${fixture}.hex`), "utf8") })).progMem
+ : new Uint16Array(ATTINY85.flashBytes / 2);
+ const cpu = new CPU(progMem, avr8jsSramParameter(ATTINY85.memory));
+ const eeprom = erasedEeprom(ATTINY85.eepromBytes);
+ return { cpu, peripherals: ATTINY85.createPeripherals(cpu, CLOCK_HZ, eeprom), eeprom };
+}
+
+function run({ cpu }: Tiny, ms: number, until?: () => boolean): void {
+ const end = cpu.cycles + (ms * CLOCK_HZ) / 1000;
+ while (cpu.cycles < end && !until?.()) {
+ for (let i = 0; i < 200; i++) {
+ avrInstruction(cpu);
+ cpu.tick();
+ }
+ }
+}
+
+const word = (eeprom: Uint8Array, address: number) => eeprom[address] | (eeprom[address + 1] << 8);
+
+/** Share of samples in which `pin` of port B is high, over `periods` timer periods of 256 timer counts at prescaler 64. */
+function highShare({ cpu, peripherals }: Tiny, pin: number, periods = 20): number {
+ const port = peripherals.ports.get("B");
+ const step = 64;
+ const end = cpu.cycles + periods * 256 * 64;
+ let high = 0;
+ let samples = 0;
+ let next = cpu.cycles;
+ while (cpu.cycles < end) {
+ avrInstruction(cpu);
+ cpu.tick();
+ if (cpu.cycles >= next) {
+ samples++;
+ if (port?.pinState(pin) === PinState.High) high++;
+ next += step;
+ }
+ }
+ return high / samples;
+}
+
+describe("ATtiny85 profile (plan P6.2)", () => {
+ it("has 8 KiB flash, 512 bytes SRAM from 0x60 (RAMEND 0x25F) and 512 bytes EEPROM", () => {
+ const { cpu } = tiny();
+ expect(ATTINY85.flashBytes).toBe(8192);
+ expect(ramEnd(ATTINY85.memory)).toBe(0x25f);
+ expect(cpu.data).toHaveLength(0x260);
+ expect(cpu.SP).toBe(0x25f);
+ expect(ATTINY85.eepromBytes).toBe(512);
+ });
+
+ it("has one port, timers 0 and 1, no hardware USART", () => {
+ const { peripherals } = tiny();
+ expect([...peripherals.ports.keys()]).toEqual(["B"]);
+ expect(peripherals.timers).toHaveLength(2);
+ expect(peripherals.usarts).toEqual([]);
+ });
+
+ describe("tiny_probe (ATTinyCore 1.5.2, 8 MHz)", () => {
+ const probe = tiny("tiny_probe");
+ probe.peripherals.adc.channelValues[1] = 2.5; // A1 = PB2
+ probe.peripherals.adc.channelValues[3] = 0.5; // A3 = PB3
+ run(probe, 1500, () => probe.eeprom[12] === 0xa5);
+ run(probe, 5); // the two analogWrite calls after the marker
+
+ it("runs setup to the end", () => {
+ expect(probe.eeprom[12]).toBe(0xa5);
+ });
+
+ it("starts with SP at RAMEND and sees RAMEND 0x25F", () => {
+ expect(word(probe.eeprom, 10)).toBe(0x25f);
+ // setup() runs two return addresses below RAMEND (main, then setup's frame).
+ expect(word(probe.eeprom, 0)).toBeLessThan(0x25f);
+ expect(word(probe.eeprom, 0)).toBeGreaterThan(0x240);
+ });
+
+ it("times delay(587) with timer 0 at 8 MHz like the core (585–589 ms)", () => {
+ // millis() advances only on the timer 0 overflow, every 2.048 ms at prescaler 64, so
+ // each reading lags by up to 2.048 ms: the difference is the elapsed ≥ 587 ms ± 2.048 ms.
+ expect(word(probe.eeprom, 2)).toBeGreaterThanOrEqual(585);
+ expect(word(probe.eeprom, 2)).toBeLessThanOrEqual(589);
+ });
+
+ it("reads the ADC against VCC, 1.1 V and 2.56 V (REFS2 at bit 4)", () => {
+ expect(word(probe.eeprom, 4)).toBe(Math.floor((2.5 / 5) * 1024)); // 512
+ expect(word(probe.eeprom, 6)).toBe(Math.floor((0.5 / 1.1) * 1024)); // 465
+ expect(word(probe.eeprom, 8)).toBe(Math.floor((0.5 / 2.56) * 1024)); // 200; avr8js alone gives 102
+ });
+
+ it("sets up PWM on PB4 with timer 1 (OCR1B, OCR1C as TOP, PWM1B, COM1B clear on match)", () => {
+ const { data } = probe.cpu;
+ expect(data[0x4b]).toBe(200); // OCR1B
+ expect(data[0x4d]).toBe(255); // OCR1C = TOP
+ expect(data[0x4c] & 0x70).toBe(0x60); // GTCCR: PWM1B, COM1B1:0 = 10
+ // avr8js 0.21.1 drives no OC1B waveform in this mode (README, known differences); the duty
+ // shown in UnoSim comes from these registers, not from the pin.
+ });
+
+ it("drives fast PWM on PB0 with timer 0", () => {
+ expect(probe.cpu.data[0x49]).toBe(77); // OCR0A
+ expect(highShare(probe, 0)).toBeCloseTo(78 / 256, 1);
+ });
+ });
+
+ describe("interrupts on the shared registers GIMSK and GIFR", () => {
+ it("counts INT0 rising edges on PB2 and pin changes on PB3", () => {
+ const probe = tiny("tiny_interrupts");
+ const port = probe.peripherals.ports.get("B");
+ run(probe, 20);
+ for (let i = 0; i < 3; i++) {
+ port?.setPin(2, true);
+ run(probe, 5);
+ port?.setPin(2, false);
+ run(probe, 5);
+ }
+ for (let i = 0; i < 2; i++) {
+ port?.setPin(3, true);
+ run(probe, 5);
+ port?.setPin(3, false);
+ run(probe, 5);
+ }
+ run(probe, 20);
+ expect(probe.eeprom[0]).toBe(3); // rising edges only
+ expect(probe.eeprom[1]).toBe(4); // every change
+ });
+
+ it("enables a pending pin-change interrupt when GIMSK is written", () => {
+ const { cpu, peripherals } = tiny();
+ cpu.writeData(PCMSK, 1 << 3);
+ peripherals.ports.get("B")?.setPin(3, true);
+ expect(cpu.data[GIFR] & (1 << 5)).toBeTruthy(); // PCIF set while PCIE is off
+ expect(cpu.nextInterrupt).toBe(-1);
+
+ cpu.writeData(GIMSK, 1 << 5);
+ expect(cpu.nextInterrupt).toBe(2); // PCINT0 vector
+ });
+
+ it("clears a pending INT0 when 1 is written to INTF0 in GIFR", () => {
+ const { cpu, peripherals } = tiny();
+ cpu.writeData(MCUCR, 0x03); // rising edge
+ cpu.writeData(GIMSK, 1 << 6);
+ peripherals.ports.get("B")?.setPin(2, true);
+ expect(cpu.data[GIFR] & (1 << 6)).toBeTruthy();
+ expect(cpu.nextInterrupt).toBe(1); // INT0 vector
+
+ cpu.writeData(GIFR, 1 << 6);
+ expect(cpu.data[GIFR] & (1 << 6)).toBe(0);
+ expect(cpu.nextInterrupt).toBe(-1);
+ });
+ });
+
+ it("resets on a 15 ms watchdog timeout and reports it in MCUSR", () => {
+ const probe = tiny("tiny_watchdog");
+ run(probe, 100);
+ expect(probe.eeprom[0]).toBeGreaterThanOrEqual(4); // first boot plus resets every ~16 ms
+ expect(probe.eeprom[1] & 0x08).toBe(0x08); // WDRF
+ });
+
+ it("takes about 3.6 ms for an EEPROM write at 8 MHz (erase and write 1.8 ms each)", () => {
+ const { cpu, eeprom } = tiny();
+ cpu.writeData(0x3e, 7); // EEARL
+ cpu.writeData(0x3d, 0x42); // EEDR
+ cpu.writeData(0x3c, 0x04); // EEMPE
+ cpu.writeData(0x3c, 0x06); // EEMPE | EEPE
+ expect(eeprom[7]).toBe(0x42);
+ expect(cpu.data[0x3c] & 0x02).toBeTruthy(); // busy
+ const done = cpu.cycles + 2 * 14_400;
+ while (cpu.cycles < done + 10) {
+ cpu.cycles++;
+ cpu.tick();
+ }
+ expect(cpu.data[0x3c] & 0x02).toBe(0);
+ });
+});
diff --git a/packages/avr-emulator/tests/attiny85-worker.test.ts b/packages/avr-emulator/tests/attiny85-worker.test.ts
new file mode 100644
index 000000000..054be0f9c
--- /dev/null
+++ b/packages/avr-emulator/tests/attiny85-worker.test.ts
@@ -0,0 +1,33 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+import { COMMAND_LIMITS, type RunEvent, WorkerHost, buildSimulationImage, decodeBase64, parseCommand } from "../src/index";
+import { FakeClock } from "./fake-clock";
+
+describe("serialBaud of the start command (plan P6.5)", () => {
+ it("is optional and checked against its limits", () => {
+ expect(parseCommand({ type: "start", runId: 2, timeoutMs: 60_000, serialBaud: 9600 })).toEqual({ ok: true, command: { type: "start", runId: 2, timeoutMs: 60_000, serialBaud: 9600 } });
+ expect(parseCommand({ type: "start", runId: 2, timeoutMs: 60_000 })).toEqual({ ok: true, command: { type: "start", runId: 2, timeoutMs: 60_000 } });
+ for (const serialBaud of [COMMAND_LIMITS.minSerialBaud - 1, COMMAND_LIMITS.maxSerialBaud + 1, 9600.5, "9600"]) {
+ expect(parseCommand({ type: "start", runId: 2, timeoutMs: 60_000, serialBaud })).toMatchObject({ ok: false });
+ }
+ });
+
+ it("lets the ATtiny85 decode a byte too short for baud detection with the sketch's rate", () => {
+ const image = buildSimulationImage({ board: "attiny85", hex: readFileSync(join(__dirname, "fixtures", "attiny85-tiny_serial_ff.hex"), "utf8") });
+ const bytes = decodeBase64(image.flashBase64);
+ const events: RunEvent[] = [];
+ let host: WorkerHost | undefined;
+ const clock = new FakeClock(() => host?.simTimeMs ?? 0);
+ host = new WorkerHost({ clock, post: (event) => events.push(event) });
+ host.handleMessage({
+ type: "load", runId: 1, board: "attiny85", artifactBoard: "attiny85", buildId: "build-1", sha256: image.sha256, symbols: image.symbols,
+ flash: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
+ });
+ host.handleMessage({ type: "start", runId: 2, timeoutMs: 10_000, serialBaud: 9600 });
+ clock.runUntil(300);
+
+ const serial = events.filter((event) => event.type === "serial").map((event) => (event as { text: string }).text).join("");
+ expect(serial).toBe("�"); // the byte 0xFF, never valid UTF-8
+ });
+});
diff --git a/packages/avr-emulator/tests/fixtures/README.md b/packages/avr-emulator/tests/fixtures/README.md
index caaf4b2c9..1fa114dad 100644
--- a/packages/avr-emulator/tests/fixtures/README.md
+++ b/packages/avr-emulator/tests/fixtures/README.md
@@ -50,3 +50,18 @@ the ports with extended I/O addresses (H, J, K, L).
The reproducible fixture set of the runtime corpus (plan 7.4) is separate.
`uno-eeprom_counter.hex` (`sketches/eeprom_counter`) prints EEPROM cell 0 and stores the next value; the worker keeps the EEPROM per board between runs (a run sees what the run before wrote, 255 on the first).
+
+`attiny85-tiny_probe.hex`, `attiny85-tiny_interrupts.hex` and
+`attiny85-tiny_watchdog.hex` are `sketches/tiny_*/` built with ATTinyCore 1.5.2
+(`ATTinyCore:avr:attinyx5:chip=85,clock=8internal`, `scripts/install-attinycore.sh`),
+same `arduino-cli` and avr-gcc. They write their results to EEPROM, so the
+profile tests need no software-UART decoder. Derived expectations: RAMEND
+0x25F from the datasheet; `delay(587)` measured by `millis()` 585–589 ms
+(each reading lags by up to one timer-0 overflow, 2.048 ms at 8 MHz and
+prescaler 64); ADC `floor(V / V_ref · 1024)`; timer-0 fast PWM duty
+`(OCR0A + 1) / 256`; boots counted after 15 ms watchdog resets with WDRF in MCUSR.
+`attiny85-tiny_serial.hex` (the `Serial` probe of spike P0.5), `attiny85-tiny_echo.hex`,
+`attiny85-tiny_serial_19200.hex` and `attiny85-tiny_serial_ff.hex` (`sketches/tiny_*`,
+same toolchain) check the software UART: outputs as measured in the spike,
+`parseInt` echo, baud detection at 19200 and the fallback rate for a byte
+0xFF (`derived`).
diff --git a/packages/avr-emulator/tests/fixtures/attiny85-tiny_echo.hex b/packages/avr-emulator/tests/fixtures/attiny85-tiny_echo.hex
new file mode 100644
index 000000000..7a25fc957
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/attiny85-tiny_echo.hex
@@ -0,0 +1,103 @@
+:100000000FC033C032C031C030C073C12EC0BBC1BD
+:100010002CC02BC02AC029C028C027C026C0A902D6
+:1000200011241FBECFE5D2E0DEBFCDBF20E0A2E8A5
+:10003000B0E001C01D92A13CB207E1F710E0A0E6DC
+:10004000B0E0E2E2F6E002C005900D92A238B107FE
+:10005000D9F710E0C0E1D0E003C02197FE01D9D26A
+:10006000CF30D107D1F7DFD1DAC2CACF08952FB789
+:10007000F894FC01458556853AE06095089420F493
+:1000800078B3752378BB04C078B3742B78BB0000B9
+:10009000C4D1C3D1C2D1C1D166953A9581F72FBFE2
+:1000A00081E090E00895CF93DF936DE08FEA90E0D8
+:1000B000DEDFEC016AE08FEA90E0D9DF8C0F9D1F54
+:1000C000DF91CF910895DC011F96ED91FC9120A165
+:1000D00031A182A193A12817390779F0A2A1B3A178
+:1000E000AE0FBF1F8C9122A133A12F5F3F4F2F7105
+:1000F000332733A322A390E008958FEF9FEF089555
+:10010000DC011F96ED91FC9120A131A182A193A168
+:100110002817390739F082A193A1E80FF91F8081D0
+:1001200090E008958FEF9FEF08952FB7F894809196
+:10013000AB009091AC00A091AD00B091AE002FBF8C
+:100140008093B7009093B800A093B900B093BA0021
+:100150008FEA90E0D5DF97FF26C02FB7F894809103
+:10016000AB009091AC00A091AD00B091AE002FBF5C
+:100170004091B7005091B8006091B9007091BA00F9
+:10018000841B950BA60BB70B4091B3005091B400A4
+:100190006091B5007091B60084179507A607B70760
+:1001A000B8F28FEF9FEF0895DC011F96ED91FC915F
+:1001B00080A191A122A133A1821B930B8F7199275A
+:1001C0000895CF92DF92EF92FF920F931F93CF93F8
+:1001D000DF936C01EB017B01E40EF51E10E000E003
+:1001E000CE15DF0561F06991D601ED91FC9101908A
+:1001F000F081E02DC6010995080F191FF1CFC80144
+:10020000DF91CF911F910F91FF90EF90DF90CF90F2
+:100210000895FC0101900020E9F73197AF01481BD8
+:10022000590BBC018FEA90E0CCCF4F925F926F9256
+:100230007F92AF92BF92CF92DF92EF92FF921F9385
+:10024000CF93DF93CDB7DEB7A1970FB6F894DEBF9B
+:100250000FBECDBF6B017C01B12CA12CF7FE0DC0F0
+:100260006DE28FEA90E003DF5C01F094E094D094BB
+:10027000C094C11CD11CE11CF11C1C2D7D2DC7019B
+:1002800019A27E0121E2E20EF11C2AE0422E512C3D
+:10029000612C712C612FA301920199D1822F6AE008
+:1002A00074D1181B1A301CF5105DF70112937F01F1
+:1002B000122F732FCA01232B242B252B59F7CF0183
+:1002C000A8DF8A0D9B1DA1960FB6F894DEBF0FBE66
+:1002D000CDBFDF91CF911F91FF90EF90DF90CF9036
+:1002E000BF90AF907F906F905F904F900895195C92
+:1002F000DCCF1F920F920FB60F9211242F933F93D2
+:100300008F939F93AF93BF938091AB009091AC007C
+:10031000A091AD00B091AE003091AA0026E0230F6D
+:100320002D3768F129E8230F0396A11DB11D2093F5
+:10033000AA008093AB009093AC00A093AD00B09363
+:10034000AE008091A6009091A700A091A800B09166
+:10035000A9000196A11DB11D8093A6009093A7004E
+:10036000A093A800B093A900BF91AF919F918F91E6
+:100370003F912F910F900FBE0F901F9018950296EE
+:10038000A11DB11DD4CF1F920F920FB60F92112451
+:100390002F933F934F935F937F938F939F93AF934D
+:1003A000BF93EF93FF934091BB0020E038E035D03E
+:1003B00034D033D032D031D0889476B3742309F05E
+:1003C000089427953A9509F0F3CF27D026D0E091ED
+:1003D000BE00F091BF0080A191A101968F71992775
+:1003E00042A153A18417950739F0A0A1B1A1AE0F86
+:1003F000BF1F2C9391A380A3449AFF91EF91BF91CB
+:10040000AF919F918F917F915F914F913F912F91EC
+:100410000F900FBE0F901F9018958091C000982FDD
+:100420009A95F1F7089583E08ABD83BF789489B7E0
+:10043000826089BF80E48CBD8FEF8DBD87EC80BF6B
+:1004400086E886B982E48093C0004398469A47982C
+:10045000419A409A87B32091BB00922F90959823A0
+:1004600097BB88B3822B88BB97B32091BC00922B9B
+:1004700097BB88B3822B88BB449A439A80E790E06D
+:10048000C8DE11DE8FEA90E08FDE892BD9F34DDED6
+:10049000EC0197FD5AC08D32910521F0C0970A9763
+:1004A00008F04DC0C12CD12C760110E0C130D1052F
+:1004B000E1F0CD32D10509F446C0CE01C0970A97CC
+:1004C000A0F42AE030E040E050E0C701B60166D079
+:1004D0006E01DD0FEE08FF08C60ED71EE81EF91EDE
+:1004E00080E3C81AD108E108F1088FEA90E0EBDD5B
+:1004F0001CDEEC01C0970A97C8F2C130D105B1F2F9
+:10050000112341F0F094E094D094C094C11CD11C0C
+:10051000E11CF11C86E790E07CDEB6010D2C000C9E
+:10052000880B990B82DEBFDD8BE790E072DECC0C8E
+:10053000DD1CB601DD0C880B990B77DEA2CF8FEAAC
+:1005400090E0C1DDA4CF11E0D0CFC12CD12C760139
+:10055000E1CFEFEAF0E01382128288EE93E0A0E0B0
+:10056000B0E084839583A683B78384E690E091838B
+:10057000808382E890E0908B878782E0848781E0A7
+:1005800085878EEF8687118A0895002480FD060EE8
+:10059000660F11F08695D1F7802D0895EE27FF277D
+:1005A000AA27BB2708C0A20FB31FE41FF51F220F05
+:1005B000331F441F551F969587957795679598F338
+:1005C0007040A9F7009799F7BD01CF010895A1E206
+:1005D0001A2EAA1BBB1BFD010DC0AA1FBB1FEE1FBD
+:1005E000FF1FA217B307E407F50720F0A21BB30B08
+:1005F000E40BF50B661F771F881F991F1A9469F784
+:1006000060957095809590959B01AC01BD01CF01DF
+:100610000895EE0FFF1F0590F491E02D0994F894D2
+:02062000FFCF0A
+:10062200000000003700E100D400630080003600C3
+:10063200726561647900676F74200074776963651D
+:02064200200096
+:00000001FF
diff --git a/packages/avr-emulator/tests/fixtures/attiny85-tiny_interrupts.hex b/packages/avr-emulator/tests/fixtures/attiny85-tiny_interrupts.hex
new file mode 100644
index 000000000..6bafbd6b1
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/attiny85-tiny_interrupts.hex
@@ -0,0 +1,42 @@
+:100000001AC0ADC0D9C032C031C05FC02FC02EC031
+:100010002DC02CC02BC02AC029C028C027C000007A
+:100020000000380000000000370002020202020255
+:1000300001020408102011241FBECFE5D2E0DEBF6C
+:10004000CDBF10E0A0E6B0E0ECE7F2E002C0059022
+:100050000D92A236B107D9F720E0A2E6B0E001C0C8
+:100060001D92AF36B207E1F7B8D006C1C9CF809173
+:1000700065008F5F809365000895CF93DF9390E0D4
+:10008000FC01E05DFF4F2491865D9F4FFC01849150
+:100090008823C1F090E0880F991FFC01EC5DFF4FB1
+:1000A000C591D491FC01E25EFF4FA591B4919FB739
+:1000B000F8948881E22FE0958E2388838C91E82341
+:1000C000EC939FBFDF91CF9108951F920F920FB6CF
+:1000D0000F9211242F933F938F939F93AF93BF93CE
+:1000E00080916B0090916C00A0916D00B0916E00BA
+:1000F00030916A0026E0230F2D3768F129E8230F9D
+:100100000396A11DB11D20936A0080936B0090930C
+:100110006C00A0936D00B0936E008091660090918A
+:100120006700A0916800B09169000196A11DB11D02
+:100130008093660090936700A0936800B093690075
+:10014000BF91AF919F918F913F912F910F900FBED3
+:100150000F901F9018950296A11DB11DD4CF1F922C
+:100160000F920FB60F9211242F933F934F935F93EB
+:100170006F937F938F939F93AF93BF93EF93FF936F
+:100180008091620090916300892B29F0E0916200D8
+:10019000F09163000995FF91EF91BF91AF919F910D
+:1001A0008F917F916F915F914F913F912F910F9020
+:1001B0000FBE0F901F9018951F920F920FB60F92BF
+:1001C00011248F93809164008F5F809364008F91DE
+:1001D0000F900FBE0F901F90189583E08ABD83BFCC
+:1001E000789489B7826089BF80E48CBD8FEF8DBD24
+:1001F00087EC80BF86E886B982E03FDF83E03DDFA1
+:100200008FB7F89427E330E03093630020936200C7
+:100210008FBF85B7836085BF8BB780648BBFAB9A78
+:100220008BB780628BBF90916500809161009817B9
+:1002300039F0609165006093610090E080E00ED03D
+:100240009091640080916000981769F36091640058
+:100250006093600081E090E001D0E5CF262FE19926
+:10026000FECF1CBA9FBB8EBB2DBB0FB6F894E29A93
+:0C027000E19A0FBE01960895F894FFCFAC
+:02027C00FFFF82
+:00000001FF
diff --git a/packages/avr-emulator/tests/fixtures/attiny85-tiny_probe.hex b/packages/avr-emulator/tests/fixtures/attiny85-tiny_probe.hex
new file mode 100644
index 000000000..90d061ee3
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/attiny85-tiny_probe.hex
@@ -0,0 +1,61 @@
+:100000001DC02CC02BC02AC029C0F6C027C026C0E6
+:1000100025C024C023C022C021C020C01FC00103AE
+:10002000000004000000000038000000000037005D
+:1000300002020202020201020408102011241FBE63
+:10004000CFE5D2E0DEBFCDBF20E0A0E6B0E001C04A
+:100050001D92AA36B207E1F719D1ABC1D1CF909169
+:1000600069009295990F990F907C8F77982B97B98B
+:1000700097B180916900880F880F8071892B87B9AB
+:10008000369A3699FECF84B195B108953FB7F8946A
+:100090008091650090916600A0916700B091680022
+:1000A00022B708B601FE05C02F3F19F00196A11D29
+:1000B000B11D3FBFBA2FA92F982F8827BC01CD01B2
+:1000C000620F711D811D911D43E0660F771F881F10
+:1000D000991F4A95D1F7089590E0FC01EA5CFF4F23
+:1000E0002491805D9F4FFC018491882399F090E0DA
+:1000F000880F991FFC01E65DFF4FA591B491FC01AB
+:10010000EC5DFF4F859194918FB7F894EC91E22BC1
+:10011000EC938FBF089590E0FC01E25EFF4F3491B5
+:10012000FC01EA5CFF4F2491FC01E05DFF4FE4918C
+:10013000EE23B9F0332329F03130A1F48AB58F775B
+:100140008ABDF0E0EE0FFF1FEC5DFF4FA591B4916B
+:100150008FB7611118C0F894EC9120952E232C9341
+:100160008FBF0895323019F48AB58F7DE9CF3330CF
+:1001700021F480B78F7D80BFE4CF343011F78CB588
+:100180008F7D8CBDDECFF894EC912E2BE8CFE82F3D
+:10019000F0E0E25EFF4FE491E13029F48AB5806837
+:1001A0008ABD69BD0895E23029F48AB580628ABDAE
+:1001B00068BD0895E33029F480B7806280BF6EBDCA
+:1001C0000895E43029F48CB580628CBD6BBD089530
+:1001D0006038710514F460E09ECF61E0FDCF1F939D
+:1001E000CF93DF93EC01172FD6D0612FCE0101966C
+:1001F000DF91CF911F91CFC01F920F920FB60F9238
+:1002000011242F933F938F939F93AF93BF9380912C
+:10021000610090916200A0916300B0916400309100
+:10022000600026E0230F2D3768F129E8230F03969D
+:10023000A11DB11D20936000809361009093620026
+:10024000A0936300B0936400809165009091660074
+:10025000A0916700B09168000196A11DB11D809327
+:10026000650090936600A0936700B0936800BF910B
+:10027000AF919F918F913F912F910F900FBE0F9053
+:100280001F9018950296A11DB11DD4CF83E08ABDA1
+:1002900083BF789489B7826089BF80E48CBD8FEF7B
+:1002A0008DBD87EC80BF86E886B90DB71EB78FB7C6
+:1002B000F8948090610090906200A0906300B090EC
+:1002C00064008FBFE3DEEB018BE4C82E82E0D82E02
+:1002D000E12CF12CDBDE6C1B7D0B683E734008F4D7
+:1002E00053C0C114D104E104F10409F045C08FB733
+:1002F000F894C0906100D0906200E0906300F090AC
+:1003000064008FBFB80190E080E069DFB601681932
+:10031000790982E090E063DF81E8A1DEBC0184E03E
+:1003200090E05DDF82E08093690083E898DEBC01A5
+:1003300086E090E054DF87E08093690083E88FDEF9
+:10034000BC0188E090E04BDF109269006FE572E03D
+:100350008AE090E044DF65EA8CE090E01CD084E025
+:10036000BBDE68EC70E084E012DF80E0B5DE6DE4B7
+:1003700070E080E00CDFFFCF81E0C81AD108E1080F
+:10038000F108C851DC4FA6CFC114D104E104F10437
+:1003900009F0A0CFACCF262FE199FECF1CBA9FBBAE
+:1003A0008EBB2DBB0FB6F894E29AE19A0FBE019670
+:0603B0000895F894FFCF50
+:00000001FF
diff --git a/packages/avr-emulator/tests/fixtures/attiny85-tiny_serial.hex b/packages/avr-emulator/tests/fixtures/attiny85-tiny_serial.hex
new file mode 100644
index 000000000..a62731ef5
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/attiny85-tiny_serial.hex
@@ -0,0 +1,135 @@
+:100000001EC042C041C040C03FC010C23DC058C227
+:100010003BC03AC039C038C037C036C035C0010314
+:10002000000004000000000038000000000037005D
+:1000300002020202020201020408102096031124A7
+:100040001FBECFE5D2E0DEBFCDBF21E0A0EFB0E024
+:1000500001C01D92A033B207E1F710E0A0E6B0E0C6
+:10006000EAECF7E002C005900D92A03FB107D9F786
+:1000700010E0CFE1D0E003C02197FE019ED3CE3146
+:10008000D107D1F76DD29FC3BBCF90911D0192953F
+:10009000990F990F907C8F77982B97B997B1809192
+:1000A0001D01880F880F8071892B87B9369A369980
+:1000B000FECF84B195B108953FB7F89480911901AE
+:1000C00090911A01A0911B01B0911C0122B708B6B2
+:1000D00001FE05C02F3F19F00196A11DB11D3FBFC4
+:1000E000BA2FA92F982F8827BC01CD01620F711D4F
+:1000F000811D911D43E0660F771F881F991F4A9548
+:10010000D1F7089508952FB7F894FC0145855685D9
+:100110003AE06095089420F478B3752378BB04C066
+:1001200078B3742B78BB000015D214D213D212D23C
+:1001300066953A9581F72FBF81E090E00895CF93BF
+:10014000DF936DE08EE191E0DEDFEC016AE08EE1AD
+:1001500091E0D9DF8C0F9D1FDF91CF910895DC01D5
+:100160001F96ED91FC9120A131A182A193A12817A6
+:10017000390779F0A2A1B3A1AE0FBF1F8C9122A1C4
+:1001800033A12F5F3F4F2F71332733A322A390E07A
+:1001900008958FEF9FEF0895DC011F96ED91FC917C
+:1001A00020A131A182A193A12817390739F082A19A
+:1001B00093A1E80FF91F808190E008958FEF9FEFE2
+:1001C0000895DC011F96ED91FC9180A191A122A1DF
+:1001D00033A1821B930B8F7199270895CF92DF92E1
+:1001E000EF92FF920F931F93CF93DF936C01EB017C
+:1001F0007B01E40EF51E10E000E0CE15DF0561F096
+:100200006991D601ED91FC910190F081E02DC6013C
+:100210000995080F191FF1CFC801DF91CF911F91E8
+:100220000F91FF90EF90DF90CF90089590E0FC0148
+:10023000EA5CFF4F2491805D9F4FFC0184918823ED
+:1002400099F090E0880F991FFC01E65DFF4FA591A2
+:10025000B491FC01EC5DFF4F859194918FB7F894B8
+:10026000EC91E22BEC938FBF089590E0FC01E25EED
+:10027000FF4F3491FC01EA5CFF4F2491FC01E05DEB
+:10028000FF4FE491EE23B9F0332329F03130A1F48C
+:100290008AB58F778ABDF0E0EE0FFF1FEC5DFF4F50
+:1002A000A591B4918FB7611118C0F894EC91209585
+:1002B0002E232C938FBF0895323019F48AB58F7D89
+:1002C000E9CF333021F480B78F7D80BFE4CF343065
+:1002D00011F78CB58F7D8CBDDECFF894EC912E2B71
+:1002E000E8CFE82FF0E0E25EFF4FE491E13029F43F
+:1002F0008AB580688ABD69BD0895E23029F48AB55F
+:1003000080628ABD68BD0895E33029F480B78062B9
+:1003100080BF6EBD0895E43029F48CB580628CBD39
+:100320006BBD08956038710514F460E09ECF61E004
+:10033000FDCFFC0101900020E9F73197AF01481B88
+:10034000590BBC018EE191E049CF8F929F92AF9201
+:10035000BF92EF92FF920F931F93CF93DF93CDB78E
+:10036000DEB7A1970FB6F894DEBF0FBECDBF162F34
+:10037000042F19A2423008F40AE07E0121E2E20EC5
+:10038000F11C802E912CB12CA12C612FA501940180
+:10039000F2D1802F622FE6D1181B1A30F4F4105DD1
+:1003A000F70112937F01122F732FCA01232B242BE5
+:1003B000252B59F7CF01BDDFA1960FB6F894DEBF0C
+:1003C0000FBECDBFDF91CF911F910F91FF90EF90A6
+:1003D000BF90AF909F908F900895195CE1CFCF921E
+:1003E000DF92EF92FF92CF93DF936B017C01D0E01D
+:1003F000C0E0F7FE0DC06DE28EE191E084DEEC011D
+:10040000F094E094D094C094C11CD11CE11CF11C68
+:100410004AE0C701B60199DF8C0F9D1FDF91CF9194
+:10042000FF90EF90DF90CF90089583CF1F920F92AF
+:100430000FB60F9211242F933F938F939F93AF93F7
+:10044000BF938091150190911601A0911701B09171
+:1004500018013091140126E0230F2D3768F129E8A7
+:10046000230F0396A11DB11D209314018093150144
+:1004700090931601A0931701B09318018091190170
+:1004800090911A01A0911B01B0911C010196A11D30
+:10049000B11D8093190190931A01A0931B01B09391
+:1004A0001C01BF91AF919F918F913F912F910F9020
+:1004B0000FBE0F901F9018950296A11DB11DD4CFAD
+:1004C0001F920F920FB60F9211242F933F934F93C9
+:1004D0005F937F938F939F93AF93BF93EF93FF931C
+:1004E00040912A0120E038E035D034D033D032D0EA
+:1004F00031D0889476B3742309F0089427953A95FF
+:1005000009F0F3CF27D026D0E0912D01F0912E01F4
+:1005100080A191A101968F71992742A153A18417BF
+:10052000950739F0A0A1B1A1AE0FBF1F2C9391A3E5
+:1005300080A3449AFF91EF91BF91AF919F918F91CA
+:100540007F915F914F913F912F910F900FBE0F9030
+:100550001F90189580912F01982F9A95F1F7089583
+:1005600083E08ABD83BF789489B7826089BF80E4C5
+:100570008CBD8FEF8DBD87EC80BF86E886B90DB747
+:100580001EB782E480932F014398469A4798419A78
+:10059000409A87B320912A01922F9095982397BB78
+:1005A00088B3822B88BB97B320912B01922B97BBEA
+:1005B00088B3822B88BB449A439A80E790E035DF6A
+:1005C00060E070E88FEF9FEF0ADFB9DD81E890E02F
+:1005D0002CDF4AE062E070E080E090E0B6DE8FE879
+:1005E00090E0A7DE65E070E080E090E0F8DE60E09B
+:1005F00070E0CB01F4DEA3DD81E990E016DF8AE94B
+:1006000090E097DE9CDD8FB7F89480901501909074
+:100610001601A0901701B09018018FBF4DDDEB01BE
+:100620008BE4C82E82E0D82EE12CF12C45DD6C1B2A
+:100630007D0B683E734008F472C0C114D104E1041C
+:10064000F10409F064C08FB7F894C0901501D09000
+:100650001601E0901701F09018018FBF81EA90E039
+:10066000E4DEC701B601681979098A099B094AE0E5
+:100670006CDE65DD8FEA90E0D8DE81E806DDBC0146
+:10068000990F880B990BABDE5ADD82E080931D0138
+:1006900089EB90E0CADE83E8F8DCBC01990F880B97
+:1006A000990B9DDE4CDD87E080931D0183EC90E08B
+:1006B000BCDE83E8EADCBC01990F880B990B8FDE66
+:1006C0003EDD10921D018EEC90E0AFDE40E16FE563
+:1006D00072E080E090E039DE32DD8AED90E0A5DE68
+:1006E000B80190E080E040E130DE29DD8BEE90E063
+:1006F00020DE25DD84E09ADD68EC70E084E0F1DD49
+:1007000080E094DD6DE470E080E0EBDDFFCF81E020
+:10071000C81AD108E108F108C851DC4F87CFC114CD
+:10072000D104E104F10409F081CF8DCFEEE1F1E0D5
+:100730001382128288EE93E0A0E0B0E08483958378
+:10074000A683B78384E690E09183808380EF90E076
+:10075000908B878782E0848781E085878EEF86870C
+:10076000118A0895002480FD060E660F11F086950B
+:10077000D1F7802D0895A1E21A2EAA1BBB1BFD0103
+:100780000DC0AA1FBB1FEE1FFF1FA217B307E40770
+:10079000F50720F0A21BB30BE40BF50B661F771FC8
+:1007A000881F991F1A9469F76095709580959095A8
+:1007B0009B01AC01BD01CF010895EE0FFF1F059015
+:0A07C000F491E02D0994F894FFCFA6
+:1007CA00000000008300EE00E100AF00CC008200D0
+:1007DA00696E74203332373637202B2031203D2082
+:1007EA00007072696E7428322E3529203D20002E41
+:1007FA0000556D6C6175743A2000C3A4C3B6C3BCBE
+:10080A000064656C61792835383729203D2000411C
+:10081A003120564343203D200041332031563120B8
+:10082A003D200041332032563536203D20005241CA
+:10083A004D454E44203D20307800535020617420AD
+:10084A007365747570203D20307800444F4E450022
+:00000001FF
diff --git a/packages/avr-emulator/tests/fixtures/attiny85-tiny_serial_19200.hex b/packages/avr-emulator/tests/fixtures/attiny85-tiny_serial_19200.hex
new file mode 100644
index 000000000..c8b9cc59e
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/attiny85-tiny_serial_19200.hex
@@ -0,0 +1,61 @@
+:100000000FC033C032C031C030C0D7C02EC01FC1F6
+:100010002CC02BC02AC029C028C027C026C0A901D7
+:1000200011241FBECFE5D2E0DEBFCDBF20E0A0E8A7
+:10003000B0E001C01D92AF3BB207E1F710E0A0E6CF
+:10004000B0E0EAE9F3E002C005900D92A038B107F4
+:10005000D9F710E0C0E1D0E003C02197FE0195D1AF
+:10006000CF30D107D1F743D196C1CACF08952FB76A
+:10007000F894FC01458556853AE06095089420F493
+:1000800078B3752378BB04C078B3742B78BB0000B9
+:1000900028D127D126D125D166953A9581F72FBF52
+:1000A00081E090E00895DC011F96ED91FC9120A184
+:1000B00031A182A193A12817390779F0A2A1B3A198
+:1000C000AE0FBF1F8C9122A133A12F5F3F4F2F7125
+:1000D000332733A322A390E008958FEF9FEF089575
+:1000E000DC011F96ED91FC9120A131A182A193A189
+:1000F0002817390739F082A193A1E80FF91F8081F1
+:1001000090E008958FEF9FEF0895DC011F96ED9129
+:10011000FC9180A191A122A133A1821B930B8F712D
+:1001200099270895CF92DF92EF92FF920F931F933A
+:10013000CF93DF936C01EB017B01E40EF51E10E021
+:1001400000E0CE15DF0561F06991D601ED91FC91DB
+:100150000190F081E02DC6010995080F191FF1CF1C
+:10016000C801DF91CF911F910F91FF90EF90DF9029
+:10017000CF9008950F931F93CF93DF93FC010190CD
+:100180000020E9F73197AF01481B590BBC018DEAFC
+:1001900090E0C8DFEC016DE08DEA90E068DF8C0153
+:1001A0006AE08DEA90E063DFC00FD11F8C0F9D1FC6
+:1001B000DF91CF911F910F9108951F920F920FB66B
+:1001C0000F9211242F933F938F939F93AF93BF93DD
+:1001D0008091A9009091AA00A091AB00B091AC00D1
+:1001E0003091A80026E0230F2D3768F129E8230F6E
+:1001F0000396A11DB11D2093A8008093A9009093A0
+:10020000AA00A093AB00B093AC008091A4009091A1
+:10021000A500A091A600B091A7000196A11DB11D57
+:100220008093A4009093A500A093A600B093A7008C
+:10023000BF91AF919F918F913F912F910F900FBEE2
+:100240000F901F9018950296A11DB11DD4CF1F923B
+:100250000F920FB60F9211242F933F934F935F93FA
+:100260007F938F939F93AF93BF93EF93FF934091AF
+:10027000B90020E038E035D034D033D032D031D09E
+:10028000889476B3742309F0089427953A9509F079
+:10029000F3CF27D026D0E091BC00F091BD0080A123
+:1002A00091A101968F71992742A153A184179507B7
+:1002B00039F0A0A1B1A1AE0FBF1F2C9391A380A3D1
+:1002C000449AFF91EF91BF91AF919F918F917F9150
+:1002D0005F914F913F912F910F900FBE0F901F9004
+:1002E00018958091BE00982F9A95F1F7089583E0B4
+:1002F0008ABD83BF789489B7826089BF80E48CBD52
+:100300008FEF8DBD87EC80BF86E886B98FE1809343
+:10031000BE004398469A4798419A409A87B32091E5
+:10032000B900922F9095982397BB88B3822B88BBF6
+:1003300097B32091BA00922B97BB88B3822B88BBCE
+:10034000449A439A80E790E015DF8BE790E012DF54
+:10035000FFCFEDEAF0E01382128288EE93E0A0E096
+:10036000B0E084839583A683B78384E690E091838D
+:10037000808380E890E0908B878782E0848781E0AB
+:1003800085878EEF8687118A0895EE0FFF1F0590EF
+:0A039000F491E02D0994F894FFCFDA
+:10039A00000000003700920085005300700036000C
+:1003AA006661737420313932303000444F4E450053
+:00000001FF
diff --git a/packages/avr-emulator/tests/fixtures/attiny85-tiny_serial_ff.hex b/packages/avr-emulator/tests/fixtures/attiny85-tiny_serial_ff.hex
new file mode 100644
index 000000000..ac548e3ed
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/attiny85-tiny_serial_ff.hex
@@ -0,0 +1,55 @@
+:100000000FC033C032C031C030C0B4C02EC0FCC03D
+:100010002CC02BC02AC029C028C027C026C08401FC
+:1000200011241FBECFE5D2E0DEBFCDBF20E0A0E7A8
+:10003000B0E001C01D92AF3AB207E1F710E0A0E6D0
+:10004000B0E0E0E5F3E002C005900D92A037B10703
+:10005000D9F710E0C0E1D0E003C02197FE0170D1D4
+:10006000CF30D107D1F720D171C1CACFCF92DF9263
+:10007000EF92FF920F931F93CF93DF936C01EB01ED
+:100080007B01E40EF51E10E000E0CE15DF0561F007
+:100090006991D601ED91FC910190F081E02DC601AE
+:1000A0000995080F191FF1CFC801DF91CF911F915A
+:1000B0000F91FF90EF90DF90CF90089508952FB7A4
+:1000C000F894FC01458556853AE06095089420F443
+:1000D00078B3752378BB04C078B3742B78BB000069
+:1000E000DDD0DCD0DBD0DAD066953A9581F72FBF32
+:1000F00081E090E00895DC011F96ED91FC9120A134
+:1001000031A182A193A12817390779F0A2A1B3A147
+:10011000AE0FBF1F8C9122A133A12F5F3F4F2F71D4
+:10012000332733A322A390E008958FEF9FEF089524
+:10013000DC011F96ED91FC9120A131A182A193A138
+:100140002817390739F082A193A1E80FF91F8081A0
+:1001500090E008958FEF9FEF0895DC011F96ED91D9
+:10016000FC9180A191A122A133A1821B930B8F71DD
+:10017000992708951F920F920FB60F9211242F9373
+:100180003F938F939F93AF93BF938091AB009091D8
+:10019000AC00A091AD00B091AE003091AA0026E075
+:1001A000230F2D3768F129E8230F0396A11DB11DF8
+:1001B0002093AA008093AB009093AC00A093AD0075
+:1001C000B093AE008091A6009091A700A091A800E6
+:1001D000B091A9000196A11DB11D8093A600909336
+:1001E000A700A093A800B093A900BF91AF919F91E1
+:1001F0008F913F912F910F900FBE0F901F901895E8
+:100200000296A11DB11DD4CF1F920F920FB60F926F
+:1002100011242F933F934F935F937F938F939F93DB
+:10022000AF93BF93EF93FF934091A00020E038E09D
+:1002300035D034D033D032D031D0889476B37423D3
+:1002400009F0089427953A9509F0F3CF27D026D0E6
+:10025000E091A300F091A40080A191A101968F717B
+:10026000992742A153A18417950739F0A0A1B1A104
+:10027000AE0FBF1F2C9391A380A3449AFF91EF91DF
+:10028000BF91AF919F918F917F915F914F913F91DE
+:100290002F910F900FBE0F901F9018958091A50081
+:1002A000982F9A95F1F7089583E08ABD83BF7894DB
+:1002B00089B7826089BF80E48CBD8FEF8DBD87ECEC
+:1002C00080BF86E886B982E48093A5004398469A69
+:1002D0004798419A409A87B32091A000922F909519
+:1002E000982397BB88B3822B88BB97B32091A1003A
+:1002F000922B97BB88B3822B88BB449A439A6FEFAB
+:1003000084E990E0DCDEFFCFE4E9F0E013821282C2
+:1003100088EE93E0A0E0B0E084839583A683B78362
+:1003200084E690E09183808380E790E0908B8787DC
+:1003300082E0848781E085878EEF8687118A089521
+:10034000EE0FFF1F0590F491E02D0994F894FFCF74
+:10035000000000005F003600AD007B0098005E00EA
+:00000001FF
diff --git a/packages/avr-emulator/tests/fixtures/attiny85-tiny_watchdog.hex b/packages/avr-emulator/tests/fixtures/attiny85-tiny_watchdog.hex
new file mode 100644
index 000000000..04fda3b4c
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/attiny85-tiny_watchdog.hex
@@ -0,0 +1,23 @@
+:100000000EC01DC01CC01BC01AC01AC018C017C02B
+:1000100016C015C014C013C012C011C010C01124E6
+:100020001FBECFE5D2E0DEBFCDBF20E0A0E6B0E04E
+:1000300001C01D92A936B207E1F74CD08FC0E0CFC6
+:100040001F920F920FB60F9211242F933F938F930D
+:100050009F93AF93BF938091650090916600A091AC
+:100060006700B09168003091640026E0230F2D37BF
+:1000700068F129E8230F0396A11DB11D20936400A8
+:100080008093650090936600A0936700B09368002A
+:100090008091600090916100A0916200B091630036
+:1000A0000196A11DB11D8093600090936100A09303
+:1000B0006200B0936300BF91AF919F918F913F9188
+:1000C0002F910F900FBE0F901F9018950296A11DB3
+:1000D000B11DD4CF83E08ABD83BF789489B7826095
+:1000E00089BF80E48CBD8FEF8DBD87EC80BF86E833
+:1000F00086B9C4B714BE0FB6F894A89581B58861C7
+:1001000081BD11BC0FBE90E080E012D061E0680FAD
+:1001100090E080E015D06C2F81E090E011D088E174
+:1001200098E00FB6F894A89581BD0FBE91BDFFCFA2
+:10013000E199FECF9FBB8EBBE09A99278DB30895BE
+:10014000262FE199FECF1CBA9FBB8EBB2DBB0FB6ED
+:10015000F894E29AE19A0FBE01960895F894FFCFC1
+:00000001FF
diff --git a/packages/avr-emulator/tests/fixtures/sketches/tiny_echo/tiny_echo.ino b/packages/avr-emulator/tests/fixtures/sketches/tiny_echo/tiny_echo.ino
new file mode 100644
index 000000000..59d586d0a
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/sketches/tiny_echo/tiny_echo.ino
@@ -0,0 +1,15 @@
+// ATtiny85 serial probe (plan P6.3; spike P0.5): serial input over the analog comparator (RX = PB1).
+void setup() {
+ Serial.begin(9600);
+ Serial.println("ready");
+}
+
+void loop() {
+ if (Serial.available()) {
+ int n = Serial.parseInt();
+ Serial.print("got ");
+ Serial.println(n);
+ Serial.print("twice ");
+ Serial.println(n * 2);
+ }
+}
diff --git a/packages/avr-emulator/tests/fixtures/sketches/tiny_interrupts/tiny_interrupts.ino b/packages/avr-emulator/tests/fixtures/sketches/tiny_interrupts/tiny_interrupts.ino
new file mode 100644
index 000000000..f8341061d
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/sketches/tiny_interrupts/tiny_interrupts.ino
@@ -0,0 +1,25 @@
+// ATtiny85 profile probe (plan P6.2): INT0 on PB2 (rising) and PCINT0 on PB3
+// count edges the test drives; the counts go to EEPROM bytes 0 and 1.
+#include
+
+volatile uint8_t int0Count = 0;
+volatile uint8_t pcintCount = 0;
+
+void onInt0() { int0Count++; }
+
+ISR(PCINT0_vect) { pcintCount++; }
+
+void setup() {
+ pinMode(2, INPUT);
+ pinMode(3, INPUT);
+ attachInterrupt(0, onInt0, RISING);
+ PCMSK |= _BV(PCINT3);
+ GIMSK |= _BV(PCIE);
+}
+
+void loop() {
+ static uint8_t lastInt0 = 0xff;
+ static uint8_t lastPcint = 0xff;
+ if (int0Count != lastInt0) { lastInt0 = int0Count; EEPROM.write(0, lastInt0); }
+ if (pcintCount != lastPcint) { lastPcint = pcintCount; EEPROM.write(1, lastPcint); }
+}
diff --git a/packages/avr-emulator/tests/fixtures/sketches/tiny_probe/tiny_probe.ino b/packages/avr-emulator/tests/fixtures/sketches/tiny_probe/tiny_probe.ino
new file mode 100644
index 000000000..3e7da1202
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/sketches/tiny_probe/tiny_probe.ino
@@ -0,0 +1,29 @@
+// ATtiny85 profile probe (plan P6.2): results in EEPROM, read by the test.
+// ATTinyCore 1.5.2, ATTinyCore:avr:attinyx5:chip=85,clock=8internal.
+#include
+
+void put16(int address, uint16_t value) {
+ EEPROM.write(address, value & 0xff);
+ EEPROM.write(address + 1, value >> 8);
+}
+
+void setup() {
+ uint16_t spAtSetup = SP;
+ unsigned long t0 = millis();
+ delay(587);
+ unsigned long t1 = millis();
+ put16(0, spAtSetup);
+ put16(2, t1 - t0);
+ put16(4, analogRead(A1)); // PB2, reference VCC
+ analogReference(INTERNAL1V1);
+ put16(6, analogRead(A3)); // PB3
+ analogReference(INTERNAL2V56);
+ put16(8, analogRead(A3));
+ analogReference(DEFAULT);
+ put16(10, RAMEND);
+ EEPROM.write(12, 0xA5); // done
+ analogWrite(4, 200); // timer 1, OC1B
+ analogWrite(0, 77); // timer 0, OC0A
+}
+
+void loop() {}
diff --git a/packages/avr-emulator/tests/fixtures/sketches/tiny_serial/tiny_serial.ino b/packages/avr-emulator/tests/fixtures/sketches/tiny_serial/tiny_serial.ino
new file mode 100644
index 000000000..ef32595b0
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/sketches/tiny_serial/tiny_serial.ino
@@ -0,0 +1,37 @@
+// ATtiny85 serial probe (plan P6.3; spike P0.5 tiny_probe) (ATTinyCore, 8 MHz internal): software Serial
+// on PB0, time, data types, ADC with three references, PWM, memory layout.
+void setup() {
+ uint16_t spAtSetup = SP;
+ Serial.begin(9600);
+ int i = 32767;
+ i++;
+ Serial.print("int 32767 + 1 = ");
+ Serial.println(i);
+ Serial.print("print(2.5) = ");
+ Serial.println(2.5);
+ Serial.print("Umlaut: ");
+ Serial.println("\xC3\xA4\xC3\xB6\xC3\xBC");
+ unsigned long t0 = millis();
+ delay(587);
+ unsigned long t1 = millis();
+ Serial.print("delay(587) = ");
+ Serial.println(t1 - t0);
+ Serial.print("A1 VCC = ");
+ Serial.println(analogRead(A1));
+ analogReference(INTERNAL1V1);
+ Serial.print("A3 1V1 = ");
+ Serial.println(analogRead(A3));
+ analogReference(INTERNAL2V56);
+ Serial.print("A3 2V56 = ");
+ Serial.println(analogRead(A3));
+ analogReference(DEFAULT);
+ Serial.print("RAMEND = 0x");
+ Serial.println(RAMEND, HEX);
+ Serial.print("SP at setup = 0x");
+ Serial.println(spAtSetup, HEX);
+ Serial.println("DONE");
+ analogWrite(4, 200);
+ analogWrite(0, 77);
+}
+
+void loop() {}
diff --git a/packages/avr-emulator/tests/fixtures/sketches/tiny_serial_19200/tiny_serial_19200.ino b/packages/avr-emulator/tests/fixtures/sketches/tiny_serial_19200/tiny_serial_19200.ino
new file mode 100644
index 000000000..81c8067f0
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/sketches/tiny_serial_19200/tiny_serial_19200.ino
@@ -0,0 +1,8 @@
+// ATtiny85 serial probe (plan P6.3): baud rate detection at 19200.
+void setup() {
+ Serial.begin(19200);
+ Serial.println("fast 19200");
+ Serial.println("DONE");
+}
+
+void loop() {}
diff --git a/packages/avr-emulator/tests/fixtures/sketches/tiny_serial_ff/tiny_serial_ff.ino b/packages/avr-emulator/tests/fixtures/sketches/tiny_serial_ff/tiny_serial_ff.ino
new file mode 100644
index 000000000..5b8691c1a
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/sketches/tiny_serial_ff/tiny_serial_ff.ino
@@ -0,0 +1,8 @@
+// ATtiny85 serial probe (plan P6.3): one byte 0xFF has a start bit as its only
+// edge pair, too few to find the baud rate; the decoder uses the configured one.
+void setup() {
+ Serial.begin(9600);
+ Serial.write(0xFF);
+}
+
+void loop() {}
diff --git a/packages/avr-emulator/tests/fixtures/sketches/tiny_watchdog/tiny_watchdog.ino b/packages/avr-emulator/tests/fixtures/sketches/tiny_watchdog/tiny_watchdog.ino
new file mode 100644
index 000000000..e4997ad93
--- /dev/null
+++ b/packages/avr-emulator/tests/fixtures/sketches/tiny_watchdog/tiny_watchdog.ino
@@ -0,0 +1,15 @@
+// ATtiny85 profile probe (plan P6.2): a 15 ms watchdog reset; EEPROM byte 0
+// counts the boots, byte 1 keeps MCUSR of the last boot.
+#include
+#include
+
+void setup() {
+ uint8_t cause = MCUSR;
+ MCUSR = 0;
+ wdt_disable();
+ EEPROM.write(0, EEPROM.read(0) + 1);
+ EEPROM.write(1, cause);
+ wdt_enable(WDTO_15MS);
+}
+
+void loop() {}
diff --git a/packages/avr-emulator/tests/software-uart.test.ts b/packages/avr-emulator/tests/software-uart.test.ts
new file mode 100644
index 000000000..7de713d97
--- /dev/null
+++ b/packages/avr-emulator/tests/software-uart.test.ts
@@ -0,0 +1,142 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { CPU, avrInstruction } from "avr8js";
+import { describe, expect, it } from "vitest";
+import { ATTINY85, SoftwareUart, avr8jsSramParameter, buildSimulationImage, erasedEeprom, loadSimulationImage } from "../src/index";
+
+const CLOCK_HZ = 8_000_000;
+
+/** An ATtiny85 with the software UART on PB0 (TX) and PB1 (RX), as ATTinyCore wires `Serial`. */
+function tinyWithSerial(fixture: string) {
+ const hex = readFileSync(join(__dirname, "fixtures", `attiny85-${fixture}.hex`), "utf8");
+ const cpu = new CPU(loadSimulationImage(buildSimulationImage({ board: "attiny85", hex })).progMem, avr8jsSramParameter(ATTINY85.memory));
+ const peripherals = ATTINY85.createPeripherals(cpu, CLOCK_HZ, erasedEeprom(ATTINY85.eepromBytes));
+ const port = peripherals.ports.get("B");
+ if (!port) throw new Error("port B missing");
+ const uart = new SoftwareUart(cpu, port, CLOCK_HZ);
+ const bytes: number[] = [];
+ uart.onByteTransmit = (byte) => bytes.push(byte);
+ const text = () => new TextDecoder().decode(new Uint8Array(bytes)).replaceAll("\r\n", "\n");
+ const run = (ms: number, until?: () => boolean) => {
+ const end = cpu.cycles + (ms * CLOCK_HZ) / 1000;
+ while (cpu.cycles < end && !until?.()) {
+ for (let i = 0; i < 200; i++) {
+ avrInstruction(cpu);
+ cpu.tick();
+ }
+ }
+ };
+ return { cpu, peripherals, uart, bytes, text, run };
+}
+
+describe("software UART of the ATtiny85 (plan P6.3)", () => {
+ it("decodes TinySoftwareSerial output at 9600 baud, UTF-8 and numbers included", () => {
+ const tiny = tinyWithSerial("tiny_serial");
+ tiny.peripherals.adc.channelValues[1] = 2.5;
+ tiny.peripherals.adc.channelValues[3] = 0.5;
+ tiny.run(2000, () => tiny.text().includes("DONE\n"));
+
+ const lines = tiny.text().split("\n");
+ expect(lines.slice(0, 3)).toEqual(["int 32767 + 1 = -32768", "print(2.5) = 2.50", "Umlaut: äöü"]);
+ expect(Number(lines[3].replace("delay(587) = ", ""))).toBeGreaterThanOrEqual(585);
+ expect(lines.slice(4)).toEqual(["A1 VCC = 512", "A3 1V1 = 465", "A3 2V56 = 200", "RAMEND = 0x25F", expect.stringMatching(/^SP at setup = 0x25[0-9A-F]$/), "DONE", ""]);
+ expect(tiny.uart.baudRate).toBe(9600);
+ });
+
+ it("finds another standard baud rate from the line", () => {
+ const tiny = tinyWithSerial("tiny_serial_19200");
+ tiny.run(500, () => tiny.text().includes("DONE\n"));
+ expect(tiny.text()).toBe("fast 19200\nDONE\n");
+ expect(tiny.uart.baudRate).toBe(19200);
+ });
+
+ it("decodes a byte without enough edges with the configured baud rate once the line is idle", () => {
+ const tiny = tinyWithSerial("tiny_serial_ff");
+ tiny.uart.configuredBaud = 9600;
+ tiny.run(200);
+ expect(tiny.bytes).toEqual([0xff]);
+ });
+
+ it("receives through the analog comparator after Serial.begin() and answers", () => {
+ const tiny = tinyWithSerial("tiny_echo");
+ tiny.run(500, () => tiny.text().includes("ready\n"));
+ expect(tiny.uart.rxEnable).toBe(true);
+
+ const pending = [..."21\n"].map((char) => char.codePointAt(0) as number);
+ tiny.uart.onRxComplete = () => {
+ const next = pending.shift();
+ if (next !== undefined) tiny.uart.writeByte(next);
+ };
+ tiny.uart.writeByte(pending.shift() as number);
+ tiny.run(3000, () => tiny.text().includes("twice"));
+ tiny.run(50);
+
+ expect(tiny.text()).toBe("ready\ngot 21\ntwice 42\n");
+ });
+
+ it("refuses input while the receiver is off or a byte is on the line", () => {
+ const tiny = tinyWithSerial("tiny_echo");
+ expect(tiny.uart.rxEnable).toBe(false); // before Serial.begin()
+ expect(tiny.uart.writeByte(0x31)).toBe(false);
+ tiny.run(500, () => tiny.text().includes("ready\n"));
+ expect(tiny.uart.writeByte(0x31)).toBe(true);
+ expect(tiny.uart.rxBusy).toBe(true);
+ expect(tiny.uart.writeByte(0x32)).toBe(false);
+ });
+});
+
+describe("software UART line decoding (synthetic waveforms)", () => {
+ /** An ATtiny85 without program whose PB0 is set by the test at exact cycles. */
+ function line() {
+ const cpu = new CPU(new Uint16Array(ATTINY85.flashBytes / 2), avr8jsSramParameter(ATTINY85.memory));
+ const port = ATTINY85.createPeripherals(cpu, CLOCK_HZ, erasedEeprom(ATTINY85.eepromBytes)).ports.get("B");
+ if (!port) throw new Error("port B missing");
+ const uart = new SoftwareUart(cpu, port, CLOCK_HZ);
+ const bytes: number[] = [];
+ uart.onByteTransmit = (byte) => bytes.push(byte);
+ cpu.writeData(0x28, 0x4b); // ACSR as Serial.begin() sets it: bandgap, interrupt on, rising edge
+ cpu.writeData(0x37, 0x01); // DDRB: PB0 output
+ cpu.writeData(0x38, 0x01); // idle high
+ const at = (cycle: number, level: 0 | 1) => {
+ while (cpu.cycles < cycle) {
+ cpu.cycles++;
+ cpu.tick();
+ }
+ cpu.writeData(0x38, level);
+ };
+ const frame = (start: number, byte: number, bit: number) => {
+ [0, ...Array.from({ length: 8 }, (_, index) => (byte >> index) & 1), 1].forEach((level, index) => at(Math.round(start + index * bit), level as 0 | 1));
+ return start + 10 * bit;
+ };
+ const idle = (cycles: number) => at(cpu.cycles + cycles, 1);
+ return { cpu, uart, bytes, at, frame, idle };
+ }
+
+ it("keeps a start bit that follows a set-up pulse closely", () => {
+ const { uart, bytes, at, frame, idle } = line();
+ const bit = CLOCK_HZ / 9600;
+ at(1_000, 0);
+ at(1_003, 1); // 3-cycle pulse, as when the core configures the pin
+ let next = 1_100;
+ for (let i = 0; i < 6; i++) next = frame(next, 0x55, bit);
+ idle(20 * bit);
+ expect(uart.baudRate).toBe(9600);
+ expect(bytes).toEqual(Array(6).fill(0x55));
+ });
+
+ it("sends nothing while Serial is not active (PB0 used as an ordinary pin)", () => {
+ const { cpu, bytes, frame, idle } = line();
+ cpu.writeData(0x28, 0x80); // ACSR: comparator off, as before Serial.begin() or after Serial.end()
+ frame(1_000, 0x00, CLOCK_HZ / 9600);
+ idle(40 * (CLOCK_HZ / 9600));
+ expect(bytes).toEqual([]);
+ });
+
+ it("ignores PB0 while timer 0 drives it as PWM (OC0A)", () => {
+ const { cpu, bytes, frame, idle } = line();
+ cpu.writeData(0x4a, 0x83); // TCCR0A: COM0A1, fast PWM
+ frame(1_000, 0x00, CLOCK_HZ / 9600);
+ idle(40 * (CLOCK_HZ / 9600));
+ expect(bytes).toEqual([]);
+ });
+});
diff --git a/scripts/deployment/gateway-deployment.ts b/scripts/deployment/gateway-deployment.ts
index f10cb2000..cd993633c 100644
--- a/scripts/deployment/gateway-deployment.ts
+++ b/scripts/deployment/gateway-deployment.ts
@@ -3,6 +3,7 @@ import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises
import { tmpdir } from "node:os";
import { join } from "node:path";
import WebSocket from "ws";
+import { enabledBoards } from "../../shared/boards";
import { simulationArtifactSchema } from "../../shared/simulation-artifact";
import {
blinkSketch,
@@ -192,7 +193,10 @@ async function teardown(run: {
async function verifyBrowserEmulationPilot(gateway: { port: number; ca: Buffer }): Promise {
const config = parseJson((await httpsRequest(gateway, "/api/config")).body);
assert.deepEqual(config.runtime, { backends: ["browser", "host-mock"], default: "browser" });
- assert.deepEqual(config.boards.map((board: { id: string }) => board.id), ["uno", "nano", "mega2560"]);
+ // The boards the catalog offers with the browser backend (every `preview` and `available` board).
+ const expectedBoards = enabledBoards(undefined, "uno", ["browser", "host-mock"]).map((board) => board.id);
+ assert.deepEqual(config.boards.map((board: { id: string }) => board.id), expectedBoards);
+ assert.ok(expectedBoards.includes("nano"), "the pilot must offer a board that only the browser runs");
const compileResponse = await httpsRequest(gateway, "/api/compile", {
method: "POST",
diff --git a/scripts/generate-attiny85-svg.mjs b/scripts/generate-attiny85-svg.mjs
new file mode 100644
index 000000000..bd01734af
--- /dev/null
+++ b/scripts/generate-attiny85-svg.mjs
@@ -0,0 +1,136 @@
+// Draws the ATtiny85 for the board view (plan P6.5): `client/public/ATtiny85.svg` (a DIP-8 chip on a small
+// carrier board) and `ATtiny85-overlay.svg` (the interactive parts, same id convention as the Uno:
+// pin--click/-state/-frame, led-l/-on, reset-click). Drawn from scratch, no third-party artwork.
+// 4 units per mm. Pin numbers are ATTinyCore's (0–5 = PB0–PB5); PB5 is RESET and not interactive.
+// node scripts/generate-attiny85-svg.mjs
+import { writeFileSync } from "node:fs";
+import { join } from "node:path";
+
+const MM = 4;
+const WIDTH = 32 * MM; // 128
+const HEIGHT = 24 * MM; // 96
+const MARGIN = 4;
+const PITCH = 2.54 * MM;
+const BOARD_COLOR = "#0f7391"; // replaced by the user's board colour at run time
+const CHIP = { x: 48, y: 30, width: 32, height: 4 * PITCH + 4 };
+const LEFT_X = 20;
+const RIGHT_X = WIDTH - 20;
+const FIRST_Y = CHIP.y + 7;
+
+/** DIP-8 pins, physical order: 1–4 down the left, 8–5 down the right. `pin` = Arduino pin of ATTinyCore. */
+const LEFT = [
+ { dip: 1, label: "PB5", note: "RST · A0", reserved: true },
+ { dip: 2, label: "PB3", note: "A3", pin: 3 },
+ { dip: 3, label: "PB4", note: "A2 · ~", pin: 4 },
+ { dip: 4, label: "GND", power: true },
+];
+const RIGHT = [
+ { dip: 8, label: "VCC", power: true },
+ { dip: 7, label: "PB2", note: "A1", pin: 2 },
+ { dip: 6, label: "PB1", note: "~ · RX · L", pin: 1 },
+ { dip: 5, label: "PB0", note: "~ · TX", pin: 0 },
+];
+
+const fmt = (value) => Number(value.toFixed(2));
+const place = (row, x) => row.map((entry, index) => ({ ...entry, x, y: fmt(FIRST_Y + index * PITCH) }));
+const pins = [...place(LEFT, LEFT_X), ...place(RIGHT, RIGHT_X)];
+const LED = { l: { x: 100, y: 12 }, on: { x: 22, y: 12 } };
+const RESET = { x: 64, y: 82 };
+
+const header = (inner, extra = "") => `
+
+`;
+
+const text = (x, y, size, content, { anchor = "middle", opacity } = {}) => {
+ const fade = opacity ? ` fill-opacity="${opacity}"` : "";
+ return `${content}`;
+};
+
+function boardSvg() {
+ const legs = pins
+ .map(({ x, y }) => {
+ const towardsChip = x < WIDTH / 2 ? CHIP.x : CHIP.x + CHIP.width;
+ return ` `;
+ })
+ .join("\n");
+ const pads = pins
+ .map(({ x, y, label, note, pin, power, reserved }) => {
+ const left = x < WIDTH / 2;
+ const labelX = left ? x - 4 : x + 4;
+ const anchor = left ? "end" : "start";
+ const fill = pin === undefined ? "#8a8a8a" : "#d1c690";
+ return [
+ ` `,
+ ` ${text(labelX, y - 0.4, 3, label, { anchor, ...(power || reserved ? { opacity: 0.7 } : {}) })}`,
+ note ? ` ${text(labelX, y + 3.2, 2.2, note, { anchor, opacity: 0.8 })}` : "",
+ ].filter(Boolean).join("\n");
+ })
+ .join("\n");
+ return header(`
+
+${legs}
+
+
+
+ ${text(CHIP.x + CHIP.width / 2, CHIP.y + CHIP.height / 2 + 1, 3.4, "ATtiny85")}
+ ${text(CHIP.x + CHIP.width / 2, CHIP.y + CHIP.height / 2 + 5, 2.2, "8 MHz", { opacity: 0.8 })}
+ ${text(LED.on.x + 5, LED.on.y + 1.9, 2.2, "ON", { anchor: "start" })}
+ ${text(LED.l.x - 1.5, LED.l.y + 1.9, 2.2, "L", { anchor: "end" })}
+
+
+ ${text(RESET.x + 7, RESET.y + 1, 2.4, "RESET", { anchor: "start", opacity: 0.8 })}
+ ${text(12, 90, 2.8, "ATTINY85", { anchor: "start" })}
+ ${text(WIDTH - 12, 90, 2.4, "UnoSim", { anchor: "end", opacity: 0.8 })}
+${pads}
+`);
+}
+
+function overlaySvg() {
+ const style = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+ const interactive = pins
+ .filter(({ pin }) => pin !== undefined)
+ .map(({ x, y, pin }) => `
+
+
+
+ `)
+ .join("\n");
+ const led = (id, { x, y }) => ` `;
+ return header(`${[
+ ' ',
+ ` `,
+ " ",
+ interactive,
+ led("led-l", LED.l),
+ led("led-on", LED.on),
+ ' ',
+ ` `,
+ " ",
+ ].join("\n")}\n`, `${style}`);
+}
+
+const outDir = join(import.meta.dirname, "..", "client", "public");
+writeFileSync(join(outDir, "ATtiny85.svg"), boardSvg());
+writeFileSync(join(outDir, "ATtiny85-overlay.svg"), overlaySvg());
+console.log(`wrote ATtiny85.svg and ATtiny85-overlay.svg (viewBox ${-MARGIN} ${-MARGIN} ${fmt(WIDTH + 2 * MARGIN)} ${fmt(HEIGHT + 2 * MARGIN)})`);
diff --git a/scripts/runtime-fixtures.ts b/scripts/runtime-fixtures.ts
index a43b8c84e..96b1ad8c7 100644
--- a/scripts/runtime-fixtures.ts
+++ b/scripts/runtime-fixtures.ts
@@ -1,7 +1,7 @@
// Builds the HEX fixtures of the runtime reference corpus (plan 7.4, P1.14):
// npm run fixtures:runtime rebuild tests/fixtures/runtime-corpus/hex//.hex
// npm run fixtures:runtime -- --check compare a rebuild with the checked-in files
-// The toolchain (arduino-cli, arduino:avr core) is the pinned one of the CI toolchain gate.
+// The toolchain (arduino-cli, arduino:avr core, ATTinyCore) is the pinned one of the CI toolchain gate.
import { spawnSync } from "node:child_process";
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
@@ -15,6 +15,7 @@ export const CORPUS_FQBN: Readonly> = {
uno: "arduino:avr:uno",
nano: "arduino:avr:nano:cpu=atmega328",
mega2560: "arduino:avr:mega:cpu=atmega2560",
+ attiny85: "ATTinyCore:avr:attinyx5:chip=85,clock=8internal",
};
export interface CorpusCaseRef {
diff --git a/shared/board-pins.ts b/shared/board-pins.ts
index 9c35a1e87..9b50a4c36 100644
--- a/shared/board-pins.ts
+++ b/shared/board-pins.ts
@@ -4,16 +4,18 @@ import type { BoardId } from "./boards";
/**
* The pins of a board as the UI, the sketch parsers and the input checks see
* them (plan 4.3, P4.1): derived from the pin map of the emulation package, the
- * single definition of the hardware. Boards without a hardware profile yet
- * (Mega 2560 until P5.2, ATtiny85 until P6.4) have none.
+ * single definition of the hardware. A board without a hardware profile has
+ * none.
*/
export interface BoardPin {
/** Arduino pin number of the core (Uno: A0 = 14). */
readonly pin: number;
/** `D13`, `A0`: how the board names it. */
readonly label: string;
- /** The name a sketch writes: `13`, `A0`. */
+ /** The name a sketch writes: `13`, `A0`; for a digital pin with an analog second name (ATtiny85 `2`/`A1`) its number. */
readonly name: string;
+ /** The analog second name of a digital pin (ATtiny85: `A1` for pin 2). */
+ readonly analogName?: string;
/** Usable as a digital pin: it has a port bit (A6/A7 of the Nano have none). */
readonly digital: boolean;
/** Has an ADC channel. */
@@ -30,7 +32,8 @@ function toBoardPin(pin: PinHardware): BoardPin {
return {
pin: pin.pin,
label: pin.label,
- name: analog ? pin.label : String(pin.pin),
+ name: analog && !pin.analogName ? pin.label : String(pin.pin),
+ ...(pin.analogName ? { analogName: pin.analogName } : {}),
digital: pin.port !== undefined,
analog,
...(pin.adcChannel === undefined ? {} : { adcChannel: pin.adcChannel }),
@@ -85,7 +88,8 @@ export function resolveBoardPin(board: BoardId, name: string): number | undefine
const pin = Number(name);
return pins.some((candidate) => candidate.pin === pin) ? pin : undefined;
}
- return pins.find((candidate) => candidate.analog && candidate.label === name.toUpperCase())?.pin;
+ const analog = name.toUpperCase();
+ return pins.find((candidate) => candidate.analog && (candidate.analogName ?? candidate.label) === analog)?.pin;
}
/** Number of hardware serial ports (`Serial`, `Serial1`, …); a board without hardware profile counts as the Uno with one. */
diff --git a/shared/boards.ts b/shared/boards.ts
index d01245387..05d93a53a 100644
--- a/shared/boards.ts
+++ b/shared/boards.ts
@@ -42,7 +42,7 @@ export const BOARD_CATALOG: readonly BoardCatalogEntry[] = [
fqbn: "ATTinyCore:avr:attinyx5:chip=85,clock=8internal",
core: { id: "ATTinyCore:avr", version: "1.5.2" },
runtimes: ["browser"],
- status: "planned",
+ status: "preview",
},
];
@@ -93,14 +93,17 @@ export function boardTutorFacts(id: BoardId): { id: BoardId; displayName: string
}
/**
- * Pin names the sketch parsers resolve for a board (`A0` → 14, `LED_BUILTIN`),
- * taken from the pin map of the emulation package. Boards without hardware
- * profile yet (Mega 2560 and ATtiny85 until P5.1 and P6.2) have none.
+ * Pin names of a board and their pins (`A0` → 14, ATtiny85 `A1` → 2,
+ * `LED_BUILTIN`), taken from the pin map of the emulation package; a board
+ * without hardware profile has none.
*/
export function boardPinAliases(id: BoardId): Readonly> | undefined {
if (!hasBoardHardware(id)) return undefined;
const hardware = boardHardware(id);
const aliases: Record = { LED_BUILTIN: hardware.ledBuiltin };
- for (const pin of hardware.pins) if (/^A\d+$/.test(pin.label)) aliases[pin.label] = pin.pin;
+ for (const pin of hardware.pins) {
+ const name = pin.analogName ?? pin.label;
+ if (/^A\d+$/.test(name)) aliases[name] = pin.pin;
+ }
return aliases;
}
diff --git a/shared/code-parser.ts b/shared/code-parser.ts
index c436428e6..26d6ad12c 100644
--- a/shared/code-parser.ts
+++ b/shared/code-parser.ts
@@ -49,7 +49,7 @@ export class CodeParser {
code: string,
analysis: StaticIOAnalysis = this.analyzeIO(code, { board: this.board }),
): ParserMessage[] {
- const parser = new HardwareCompatibilityParser(analysis, this.board);
+ const parser = new HardwareCompatibilityParser(analysis, this.board, code);
return parser.parse();
}
diff --git a/shared/io-registry-parser.ts b/shared/io-registry-parser.ts
index 61dd30cfd..bb4b244b6 100644
--- a/shared/io-registry-parser.ts
+++ b/shared/io-registry-parser.ts
@@ -197,7 +197,7 @@ function resolveToken(
if (/^\d+$/.test(token)) return Number.parseInt(token, 10);
const analogMatch = /^A(\d+)$/.exec(token);
if (analogMatch) {
- return activeContext.analogPin(Number.parseInt(analogMatch[1], 10));
+ return activeContext.analogValue(Number.parseInt(analogMatch[1], 10));
}
return syms.get(token);
}
@@ -240,12 +240,12 @@ function resolvePin(
const idx = Number.parseInt(arrM[2], 10);
if (arr && idx < arr.length) {
const id = arr[idx];
- return activeContext.has(id) ? id : undefined;
+ return activeContext.isPinValue(id) ? id : undefined;
}
return undefined;
}
const n = resolveToken(t, syms);
- return n !== undefined && activeContext.has(n) ? n : undefined;
+ return n !== undefined && activeContext.isPinValue(n) ? n : undefined;
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -511,8 +511,8 @@ function processStaticPin(
entries: StaticIOCall[],
loopBody?: "braced" | "braceless",
): void {
- const resolvedPinId =
- op === "analogRead" ? (activeContext.analogPin(pinId) ?? pinId) : pinId;
+ const resolvedPinId = activeContext.callPin(pinId, op);
+ if (resolvedPinId === undefined) return;
const loopContext = loopBody === undefined ? {} : { loopBody };
if (op === "pinMode") {
const mode = MODE_MAP[secondArg];
diff --git a/shared/parser-patterns.ts b/shared/parser-patterns.ts
index 5a9d339ca..b1b500a6d 100644
--- a/shared/parser-patterns.ts
+++ b/shared/parser-patterns.ts
@@ -56,6 +56,12 @@ export const COMMENT_PATTERNS = {
* Strip comments while preserving character positions (newlines kept intact).
* This allows line numbers in the stripped code to match the original source.
*/
+/** The baud rate of an active `Serial.begin()` in the sketch, if it is a literal. */
+export function sketchSerialBaud(code: string): number | undefined {
+ const match = SERIAL_PATTERNS.BEGIN_EXTRACT.exec(stripComments(code));
+ return match ? Number(match[1]) : undefined;
+}
+
export function stripComments(code: string): string {
// Multi-line comments → spaces (preserve newlines for correct line counting)
let result = code.replaceAll(COMMENT_PATTERNS.MULTI_LINE, (m) =>
diff --git a/shared/parsers/hardware-compatibility-parser.ts b/shared/parsers/hardware-compatibility-parser.ts
index 738b3087c..e72f8324f 100644
--- a/shared/parsers/hardware-compatibility-parser.ts
+++ b/shared/parsers/hardware-compatibility-parser.ts
@@ -8,6 +8,7 @@ import {
} from "@shared/io-registry-parser";
import type { BoardId } from "../boards";
import { type PinContext, pinContext } from "./pin-context";
+import { stripComments } from "../parser-patterns";
const DIGITAL_OPERATIONS = new Set([
"digitalRead",
@@ -79,7 +80,12 @@ function createVariableMissingPinModeMessage(
export class HardwareCompatibilityParser {
private readonly pins: PinContext;
- constructor(private readonly analysis: StaticIOAnalysis, board?: BoardId) {
+ constructor(
+ private readonly analysis: StaticIOAnalysis,
+ board?: BoardId,
+ /** The sketch, to tell whether it uses `Serial` (rules for a software serial port). */
+ private readonly code = "",
+ ) {
this.pins = pinContext(board);
}
@@ -87,6 +93,8 @@ export class HardwareCompatibilityParser {
return [
...this.checkAnalogWritePWM(),
...this.checkAnalogOnlyPins(),
+ ...this.checkResetPin(),
+ ...this.checkSoftwareSerialPins(),
...this.checkPinModeConflicts(),
...this.checkMissingPinModes(),
...this.checkOutputPinsReadAsInput(),
@@ -126,6 +134,47 @@ export class HardwareCompatibilityParser {
);
}
+ /** A pin reserved for RESET (ATtiny85 PB5) has no digital function; analogRead() of it works. */
+ private checkResetPin(): ParserMessage[] {
+ return this.analysis.pins
+ .filter(({ pinId }) => this.pins.role(pinId) === "reset")
+ .flatMap(({ pinId, calls }) =>
+ calls
+ .filter(({ op }) => op !== "analogRead")
+ .map((call) => ({
+ id: randomUUID(),
+ type: "warning" as const,
+ category: "hardware" as const,
+ severity: 2 as const,
+ message: `${call.op}(${call.sourceExpression}, ...) used on pin ${pinId}, which is the RESET pin of ${this.pins.displayName} and cannot be used as digital I/O.`,
+ suggestion: "// Use one of the pins 0–4",
+ ...sourceLocation(call),
+ })),
+ );
+ }
+
+ /**
+ * On a board whose `Serial` is a software UART on ordinary pins (ATtiny85: TX
+ * on 0, RX on 1), using those pins for anything else disturbs `Serial`.
+ */
+ private checkSoftwareSerialPins(): ParserMessage[] {
+ const serial = this.pins.softwareSerialPins();
+ if (!serial || !/\bSerial\s*\.\s*\w+/.test(stripComments(this.code))) return [];
+ return this.analysis.pins
+ .filter(({ pinId }) => pinId === serial.tx || pinId === serial.rx)
+ .flatMap(({ pinId, calls }) =>
+ calls.map((call) => ({
+ id: randomUUID(),
+ type: "warning" as const,
+ category: "hardware" as const,
+ severity: 2 as const,
+ message: `${call.op}(${call.sourceExpression}, ...) used on pin ${pinId}, which is Serial ${pinId === serial.tx ? "TX" : "RX"} on ${this.pins.displayName}; Serial ${pinId === serial.tx ? "output" : "input"} will not work while the sketch uses the pin.`,
+ suggestion: `// Use another pin, Serial needs pins ${serial.tx} and ${serial.rx}`,
+ ...sourceLocation(call),
+ })),
+ );
+ }
+
private checkPinModeConflicts(): ParserMessage[] {
const messages: ParserMessage[] = [];
for (const { pinId, calls } of this.analysis.pins) {
diff --git a/shared/parsers/pin-context.ts b/shared/parsers/pin-context.ts
index 1c9d631e2..6497c8d7d 100644
--- a/shared/parsers/pin-context.ts
+++ b/shared/parsers/pin-context.ts
@@ -7,7 +7,7 @@ import { type BoardId, DEFAULT_BOARD_ID, getBoard } from "../boards";
* pin numbers exist, what `A0`…`An` and `LED_BUILTIN` mean, how a pin is
* written, which pins can do PWM, and which pins have no digital function.
* Built from the pin model of the board (`board-pins`); a board without a
- * hardware profile yet is parsed like the Uno.
+ * hardware profile is parsed like the Uno.
*/
export interface PinContext {
readonly board: BoardId;
@@ -17,6 +17,20 @@ export interface PinContext {
has(pin: number): boolean;
/** Pin number of analog input `A`; `undefined` if the board has no such input. */
analogPin(channel: number): number | undefined;
+ /**
+ * The value of `A` in the core: the pin number (Uno: A0 = 14), or
+ * `0x80 | channel` where the analog names are second names of digital pins
+ * (ATtiny85, ATTinyCore).
+ */
+ analogValue(channel: number): number | undefined;
+ /** True if `value` names a pin of the board, directly or as an analog value. */
+ isPinValue(value: number): boolean;
+ /**
+ * The pin a call means by `value`, as the core reads it: a value with bit 7
+ * set is an analog channel, `analogRead(n)` takes a channel where the board
+ * has one; `undefined` for a value without a pin.
+ */
+ callPin(value: number, op: string): number | undefined;
/** `13`, `A0`: how a sketch writes the pin. */
label(pin: number): string;
/** Names the parsers resolve without a definition: `LED_BUILTIN`, `A0`… */
@@ -26,6 +40,10 @@ export interface PinContext {
analogOnly(pin: number): boolean;
/** Number of hardware serial ports (`Serial`, `Serial1`, …). */
serialPortCount(): number;
+ /** `reset` for a pin the board reserves for RESET (ATtiny85 PB5), `serial` for the pins of `Serial`. */
+ role(pin: number): "reset" | "serial" | undefined;
+ /** TX and RX of `Serial` when the core implements it in software on ordinary pins (ATtiny85). */
+ softwareSerialPins(): { readonly tx: number; readonly rx: number } | undefined;
}
const cache = new Map();
@@ -35,20 +53,37 @@ function createContext(board: BoardId): PinContext {
const known = new Set(pins.map((pin) => pin.pin));
const analog = boardAnalogPins(board);
const channelPin = new Map(analog.map((pin) => [pin.adcChannel as number, pin.pin]));
+ const encodedAnalog = analog.some((pin) => pin.analogName !== undefined);
+ const decode = (value: number) => (encodedAnalog && value & 0x80 ? channelPin.get(value & 0x7f) : undefined);
+ const analogValue = (channel: number): number | undefined => {
+ if (!channelPin.has(channel)) return undefined;
+ return encodedAnalog ? 0x80 | channel : channelPin.get(channel);
+ };
return {
board,
displayName: getBoard(board).displayName,
maxPin: Math.max(...known),
has: (pin) => known.has(pin),
analogPin: (channel) => channelPin.get(channel),
+ analogValue,
+ isPinValue: (value) => known.has(value) || decode(value) !== undefined,
+ callPin: (value, op) => {
+ const pin = decode(value) ?? (op === "analogRead" ? (channelPin.get(value) ?? value) : value);
+ return known.has(pin) ? pin : undefined;
+ },
label: (pin) => findBoardPin(board, pin)?.name ?? String(pin),
builtinConstants: () => ({
LED_BUILTIN: boardHardware(board).ledBuiltin,
- ...Object.fromEntries(analog.map((pin) => [pin.label, pin.pin])),
+ ...Object.fromEntries(analog.map((pin) => [pin.analogName ?? pin.label, analogValue(pin.adcChannel as number) as number])),
}),
pwmPins: () => pins.filter((pin) => pin.pwm).map((pin) => pin.pin),
analogOnly: (pin) => findBoardPin(board, pin)?.digital === false,
serialPortCount: () => boardHardware(board).serialPorts.length,
+ role: (pin) => findBoardPin(board, pin)?.role,
+ softwareSerialPins: () => {
+ const port = boardHardware(board).serialPorts.find((candidate) => candidate.kind === "software");
+ return port ? { tx: port.txPin, rx: port.rxPin } : undefined;
+ },
};
}
diff --git a/ssot/ssot_function_definition_Boards.md b/ssot/ssot_function_definition_Boards.md
index b320efe1b..20abd66ea 100644
--- a/ssot/ssot_function_definition_Boards.md
+++ b/ssot/ssot_function_definition_Boards.md
@@ -36,7 +36,7 @@ Der Host-Mock gilt nur für den Uno.
- `preview`: wählbar mit Hinweis, sobald die Phase des Boards abgeschlossen ist.
- `available`: nach Review und Unterrichtseinsatz; ein Board wird erst `available`, wenn seine Proben Fälle der Klasse `hardware` im Korpus haben (TESTING_STANDARDS).
-Der Uno steht seit M1 mit `available` im Katalog (seine Proben haben Fälle der Klasse `hardware` im Korpus), der Nano seit P4.8 und der Mega 2560 seit P5.4 mit `preview` (Hardware und Proben im Paket, Korpus-Auszug, E2E), der ATtiny85 mit `planned`. Das Statuswort entscheidet über die Freischaltung: Der Standard-Board (`uno`) ist immer angeboten, weil Anfragen ohne Board ihn meinen.
+Der Uno steht seit M1 mit `available` im Katalog (seine Proben haben Fälle der Klasse `hardware` im Korpus), der Nano seit P4.8, der Mega 2560 seit P5.4 und der ATtiny85 seit P6.5 mit `preview` (Hardware und Proben im Paket, Korpus-Proben bzw. -Auszug, E2E). Das Statuswort entscheidet über die Freischaltung: Der Standard-Board (`uno`) ist immer angeboten, weil Anfragen ohne Board ihn meinen.
## 3. Angebotene Boards und Laufzeiten
@@ -61,7 +61,7 @@ Der Betrieb stellt sie mit vier Variablen ein (`server/runtime-settings.ts`, gep
## 5. Pin-Aliasse
-`boardPinAliases(id)` liefert die Pin-Namen, die die Sketch-Parser auflösen (`A0` → 14, `LED_BUILTIN`), aus der Pinbelegung des Pakets. Der Mega 2560 hat `A0`–`A15` auf den Pins 54–69 und `LED_BUILTIN` 13 (seit P5.2). Boards ohne Hardwareprofil im Paket (ATtiny85 bis P6.2) haben keine Aliasse.
+`boardPinAliases(id)` liefert die Pin-Namen, die die Sketch-Parser auflösen (`A0` → 14, `LED_BUILTIN`), aus der Pinbelegung des Pakets. Der Mega 2560 hat `A0`–`A15` auf den Pins 54–69 und `LED_BUILTIN` 13 (seit P5.2), der ATtiny85 `A0`–`A3` auf den Pins 5, 2, 4, 3 und `LED_BUILTIN` 1 (seit P6.4). Eine ID ohne Hardwareprofil hat keine Aliasse.
### 5.1 Pin-Modell (`shared/board-pins.ts`, P4.1)
@@ -69,14 +69,16 @@ Die Pinlisten, Labels und Fähigkeiten eines Boards stammen aus der Pinbelegung
| Funktion | Ergebnis |
|---|---|
-| `boardPins(id)` | alle Pins in Nummernfolge (`pin`, `label`, `name`, `digital`, `analog`, `adcChannel`, `pwm`, `role`); `undefined` ohne Hardwareprofil |
-| `findBoardPin`, `isBoardPin(id, pin)` | Pinprüfung je Board (Uno 0–19, Nano 0–21, Mega 0–69) |
+| `boardPins(id)` | alle Pins in Nummernfolge (`pin`, `label`, `name`, `analogName`, `digital`, `analog`, `adcChannel`, `pwm`, `role`); `undefined` ohne Hardwareprofil |
+| `findBoardPin`, `isBoardPin(id, pin)` | Pinprüfung je Board (Uno 0–19, Nano 0–21, Mega 0–69, ATtiny85 0–5) |
| `boardPwmPins`, `boardAnalogPins` | PWM-Pins (`~`) und Analogeingänge in Bauteilfolge |
| `boardPinLabel(id, pin)` | Name wie im Sketch (`14` → `A0`) |
-| `resolveBoardPin(id, name)` | `13`, `A0`, `LED_BUILTIN` → Pinnummer; `A6` gibt es nur am Nano |
+| `resolveBoardPin(id, name)` | `13`, `A0`, `LED_BUILTIN` → Pinnummer; `A6` gibt es nur am Nano; am ATtiny85 `A1` → 2 |
| `maxBoardPin(id)` | höchste Pinnummer des Boards |
-Am Nano sind A6 und A7 (Pins 20 und 21) reine Analogeingänge ohne digitale Funktion (`digital: false`). Die WebSocket-Grenze `maxPin: 19` gilt weiter für den Host-Mock; die Eingabe an den Worker prüft den Pin gegen das Board (`isBoardPin`, siehe SimulationRuntime).
+Am Nano sind A6 und A7 (Pins 20 und 21) reine Analogeingänge ohne digitale Funktion (`digital: false`).
+
+Am ATtiny85 (ATTinyCore 1.5.2) sind die Pins 0–5 die Bits PB0–PB5 (`label` `PB0`…). Die Analognamen sind zweite Namen von Digitalpins (`analogName`: `A0` = 5, `A1` = 2, `A2` = 4, `A3` = 3); `name` bleibt die Nummer. Im Core ist `A` der Wert `0x80 | n`, den jede Pinfunktion auf den Pin abbildet, und `analogRead(n)` nimmt den Kanal n; die Sketch-Parser rechnen ebenso (`pinContext(id).analogValue`, `callPin`). PB5 ist RESET (`role: "reset"`), PB0/PB1 tragen `Serial` als Software-UART (`role: "serial"`, Port der Art `software`). Regeln der Parser: Warnung bei digitaler Nutzung von PB5 (`analogRead(A0)` erlaubt) und bei anderer Nutzung von PB0/PB1, wenn der Sketch `Serial` verwendet; `Serial1`–`Serial3` gibt es nicht. Die WebSocket-Grenze `maxPin: 19` gilt weiter für den Host-Mock; die Eingabe an den Worker prüft den Pin gegen das Board (`isBoardPin`, siehe SimulationRuntime).
## 6. Board im Compile-Request
diff --git a/ssot/ssot_function_definition_SimulationRuntime.md b/ssot/ssot_function_definition_SimulationRuntime.md
index c9ad9e9b4..041518c4b 100644
--- a/ssot/ssot_function_definition_SimulationRuntime.md
+++ b/ssot/ssot_function_definition_SimulationRuntime.md
@@ -54,7 +54,7 @@ Der Hauptthread vergibt eine aufsteigende `runId` (`RunGate.runIdFor`). `load`,
| Befehl | Felder | Grenzen |
|---|---|---|
| `load` | `board` (gewählt), `artifactBoard`, `buildId`, `sha256`, `flash` (`ArrayBuffer`), `symbols` | bekannte Boards, `buildId` 1–256 Zeichen, `sha256` 64 Hexziffern, Flash 1 Byte bis 256 KiB |
-| `start` | `timeoutMs` | 1000–300000 (1–300 s, E8) |
+| `start` | `timeoutMs`, optional `serialBaud` | 1000–300000 (1–300 s, E8); `serialBaud` 300–1000000 ganzzahlig: Rate, mit der ein Software-UART (ATtiny85) dekodiert, bis die Leitung die des Sketches zeigt (der Client sendet das Literal aus `Serial.begin()`); Hardware-USARTs ignorieren sie |
| `pause`, `resume`, `stop`, `reset` | – | – |
| `serialIn` | `port`, `text` | Port 0–3, 1 bis 4096 Byte UTF-8 |
| `setDigital` | `pin`, `level` | Pin 0–255, Pegel 0 oder 1 |
diff --git a/tests/client/components/arduino-board-views.test.tsx b/tests/client/components/arduino-board-views.test.tsx
index 4bb090945..83158dbd8 100644
--- a/tests/client/components/arduino-board-views.test.tsx
+++ b/tests/client/components/arduino-board-views.test.tsx
@@ -11,7 +11,7 @@ describe("board view per board (plan P4.4)", () => {
expect(boardView("uno")).toEqual({ boardSvg: "/ArduinoUno.svg", overlaySvg: "/ArduinoUno-overlay.svg", width: 285.2, height: 209 });
expect(boardView("nano")).toEqual({ boardSvg: "/ArduinoNano.svg", overlaySvg: "/ArduinoNano-overlay.svg", width: 172.8, height: 72 });
expect(boardView()).toBe(boardView("uno"));
- expect(boardView("attiny85")).toBe(boardView("uno")); // no graphics yet
+ expect(boardView("attiny85")).toMatchObject({ boardSvg: "/ATtiny85.svg", overlaySvg: "/ATtiny85-overlay.svg", width: 128, height: 96 }); // own graphics since P6.5
expect(boardView("mega2560")).toMatchObject({ boardSvg: "/ArduinoMega.svg", overlaySvg: "/ArduinoMega-overlay.svg", width: 406.4, height: 213.2 });
expect(boardView("mega2560").logo).toEqual({ left: 74, top: 38, width: 20, height: 10 });
expect(boardView("uno").logo).toBeUndefined(); // the Uno keeps the mark where it was
diff --git a/tests/client/lib/active-board.test.ts b/tests/client/lib/active-board.test.ts
index 70a618ac4..599978218 100644
--- a/tests/client/lib/active-board.test.ts
+++ b/tests/client/lib/active-board.test.ts
@@ -3,6 +3,8 @@ import {
activeAnalogPins, activePinLabel, activePins, activePwmPins, analogChannelPin, getActiveBoard, isActivePwmPin, isAnalogPin, setActiveBoard,
} from "../../../client/src/lib/active-board";
+import type { BoardId } from "@shared/boards";
+
afterEach(() => setActiveBoard("uno"));
describe("active board (plan P4.3)", () => {
@@ -29,9 +31,24 @@ describe("active board (plan P4.3)", () => {
});
it("uses the Uno's pins for a board without a hardware profile yet", () => {
- setActiveBoard("attiny85");
+ // Every catalog board has hardware since P6.4; an id without it falls back to the Uno.
+ setActiveBoard("no-hardware" as BoardId);
expect(activePins()).toHaveLength(20);
expect(isAnalogPin(20)).toBe(false);
});
});
+
+describe("active board ATtiny85 (plan P6.5)", () => {
+ it("names analog pins by number and analog name, and knows LED_BUILTIN 1", async () => {
+ const { activeAnalogPinTitle, activeLedBuiltin } = await import("../../../client/src/lib/active-board");
+ setActiveBoard("attiny85");
+ expect(activePins()).toHaveLength(6);
+ expect(activePinLabel(2)).toBe("2");
+ expect(activeAnalogPinTitle(2)).toBe("2 (A1)");
+ expect(activeLedBuiltin()).toBe(1);
+ setActiveBoard("uno");
+ expect(activeAnalogPinTitle(14)).toBe("A0");
+ expect(activeLedBuiltin()).toBe(13);
+ });
+});
diff --git a/tests/client/lib/runtime/browser-runtime.test.ts b/tests/client/lib/runtime/browser-runtime.test.ts
index 3545589bd..4811ecc1e 100644
--- a/tests/client/lib/runtime/browser-runtime.test.ts
+++ b/tests/client/lib/runtime/browser-runtime.test.ts
@@ -535,3 +535,16 @@ describe("BrowserRuntime (plan P3.2)", () => {
expect(seen).toEqual([]);
});
});
+
+describe("BrowserRuntime serial baud for a software UART (plan P6.5)", () => {
+ it("passes the sketch's Serial.begin() value with the start command, and nothing without one", () => {
+ const { runtime, worker, ev } = setup();
+ runtime.start({ kind: "browser", artifact, timeoutSec: 30, serialBaud: 9600 });
+ worker().emit(ev(worker().last("load").runId, { type: "ready", protocolVersion: 1, board: "uno", buildId: artifact.buildId }));
+ expect(worker().last("start")).toMatchObject({ type: "start", timeoutMs: 30_000, serialBaud: 9600 });
+
+ runtime.start({ kind: "browser", artifact, timeoutSec: 30 });
+ worker().emit(ev(worker().last("load").runId, { type: "ready", protocolVersion: 1, board: "uno", buildId: artifact.buildId }));
+ expect(worker().last("start")).not.toHaveProperty("serialBaud");
+ });
+});
diff --git a/tests/fixtures/runtime-corpus/README.md b/tests/fixtures/runtime-corpus/README.md
index 94231cfb2..58d1be4b6 100644
--- a/tests/fixtures/runtime-corpus/README.md
+++ b/tests/fixtures/runtime-corpus/README.md
@@ -14,7 +14,8 @@ hex//.hex precompiled for the unit gate
Toolchain of all fixtures: `arduino-cli` 1.5.1, `arduino:avr@1.8.8`,
`arduino:avr:uno` (Nano: `arduino:avr:nano:cpu=atmega328`, Mega: `arduino:avr:mega:cpu=atmega2560`), as UnoSim compiles
-(with LTO). `npm run fixtures:runtime` rebuilds `hex/`; `npm run
+(with LTO); ATtiny85: ATTinyCore 1.5.2 (`scripts/install-attinycore.sh`),
+`ATTinyCore:avr:attinyx5:chip=85,clock=8internal`. `npm run fixtures:runtime` rebuilds `hex/`; `npm run
fixtures:runtime -- --check` compares a rebuild with the checked-in files.
## `case.json`
@@ -54,6 +55,11 @@ fixtures:runtime -- --check` compares a rebuild with the checked-in files.
| `mega_serial_ports` | derived | `Serial` to `Serial3`: greeting per port, input per port (`events` with a port), echo | `HardwareSerial` of the core; ports 1 to 3 have `expected.mega2560.serial.txt` |
| `mega_interrupts` | derived | INT0–INT5 and one pin-change interrupt per group | datasheet 15, `attachInterrupt` of the core |
| `mega_pins` | derived | pins on ports A, H, J, K, L (extended I/O), PWM on timers 4/5, ADC above channel 7 | `digital_pin_to_port_PGM` of the mega variant, datasheet |
+| `attiny_basics` | derived | ATtiny85: types, overflow, `Serial.print` through the software UART, RAMEND/E2END/FLASHEND, `delay()` | avr-gcc ABI, avr/iotnx5.h, timer 0 of ATTinyCore at 8 MHz |
+| `attiny_adc_read` | derived | ATtiny85: `analogRead` on A1–A3 and by channel, VCC, 1.1 V and 2.56 V reference | datasheet formula `floor(Vin · 1024 / Vref)`, table 17-3 |
+| `attiny_pwm_duty` | derived | ATtiny85: `analogWrite` on 0 (timer 0), 1 and 4 (timer 1), 0/255 as digital, PWM → digital | `wiring_analog.c` and `digital_pin_to_timer_PGM` of ATTinyCore tinyX5 |
+| `attiny_serial_echo` | derived | ATtiny85: input over the analog comparator after `Serial.begin()`, earlier input lost, `parseInt` | `TinySoftwareSerial.cpp`, `Stream::parseInt` |
+| `attiny_interrupts` | derived | ATtiny85: INT0 (rising) and pin change on PB3, sharing GIMSK/GIFR | datasheet 9.2, 9.3 |
| `mega_flash_far` | derived | PROGMEM above 64 KB (`pgm_read_byte_far`), code above 128 KB (22-bit PC, indirect call) | tables of 34000 bytes generated by the assembler, sums computed in Python |
Cases of the class `hardware` are recorded once on a real board by a person
diff --git a/tests/fixtures/runtime-corpus/hex/attiny85/attiny_adc_read.hex b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_adc_read.hex
new file mode 100644
index 000000000..028594bdf
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_adc_read.hex
@@ -0,0 +1,91 @@
+:100000000FC033C032C031C030C057C12EC09FC1F5
+:100010002CC02BC02AC029C028C027C026C051022E
+:1000200011241FBECFE5D2E0DEBFCDBF20E0A2EBA2
+:10003000B0E001C01D92A23FB207E1F710E0A0E6D8
+:10004000B0E0E0E4F5E002C005900D92A23BB107FC
+:10005000D9F710E0C0E1D0E003C02197FE0168D2DB
+:10006000CF30D107D1F7C3D169C2CACF9091DF0099
+:100070009295990F990F907C8F77982B97B997B19C
+:100080008091DF00880F880F8071892B87B9369A9D
+:100090003699FECF84B195B1089508952FB7F8949D
+:1000A000FC01458556853AE06095089420F478B3C4
+:1000B000752378BB04C078B3742B78BB000091D152
+:1000C00090D18FD18ED166953A9581F72FBF81E07F
+:1000D00090E00895CF93DF936DE080EE90E0DEDF57
+:1000E000EC016AE080EE90E0D9DF8C0F9D1FDF917C
+:1000F000CF910895DC011F96ED91FC9120A131A1D3
+:1001000082A193A12817390779F0A2A1B3A1AE0F5C
+:10011000BF1F8C9122A133A12F5F3F4F2F71332737
+:1001200033A322A390E008958FEF9FEF0895DC01A1
+:100130001F96ED91FC9120A131A182A193A12817D6
+:10014000390739F082A193A1E80FF91F808190E06F
+:1001500008958FEF9FEF0895DC011F96ED91FC91BC
+:1001600080A191A122A133A1821B930B8F719927AA
+:100170000895CF92DF92EF92FF920F931F93CF9348
+:10018000DF936C01EB017B01E40EF51E10E000E053
+:10019000CE15DF0561F06991D601ED91FC910190DA
+:1001A000F081E02DC6010995080F191FF1CFC80194
+:1001B000DF91CF911F910F91FF90EF90DF90CF9043
+:1001C0000895CF93C82F52DF8C2FCF914FCFFC01D2
+:1001D00001900020E9F73197AF01481B590BBC0192
+:1001E00080EE90E0C6CF4F925F926F927F92AF9277
+:1001F000BF92CF92DF92EF92FF921F93CF93DF9344
+:10020000CDB7DEB7A1970FB6F894DEBF0FBECDBF56
+:100210006C01990FEE08FF08B12CA12CF7FE0DC060
+:100220006DE280EE90E03ADF5C01F094E094D094CF
+:10023000C094C11CD11CE11CF11C1C2D7D2DC701DB
+:1002400019A27E0121E2E20EF11C2AE0422E512C7D
+:10025000612C712C612FA301920148D1822F6AE099
+:100260003CD1181B1A303CF5105DF70112937F0149
+:10027000122F732FCA01232B242B252B59F7CF01C3
+:10028000A6DF7C0127DF8E0D9F1D8A0D9B1DA19689
+:100290000FB6F894DEBF0FBECDBFDF91CF911F9197
+:1002A000FF90EF90DF90CF90BF90AF907F906F90D6
+:1002B0005F904F900895195CD8CF1F920F920FB6A0
+:1002C0000F9211242F933F938F939F93AF93BF93DC
+:1002D0008091DB009091DC00A091DD00B091DE0008
+:1002E0003091DA0026E0230F2D3768F129E8230F3B
+:1002F0000396A11DB11D2093DA008093DB0090933B
+:10030000DC00A093DD00B093DE008091D6009091D8
+:10031000D700A091D800B091D9000196A11DB11DC0
+:100320008093D6009093D700A093D800B093D900C3
+:10033000BF91AF919F918F913F912F910F900FBEE1
+:100340000F901F9018950296A11DB11DD4CF1F923A
+:100350000F920FB60F9211242F933F934F935F93F9
+:100360007F938F939F93AF93BF93EF93FF934091AE
+:10037000EC0020E038E035D034D033D032D031D06A
+:10038000889476B3742309F0089427953A9509F078
+:10039000F3CF27D026D0E091EF00F091F00080A1BC
+:1003A00091A101968F71992742A153A184179507B6
+:1003B00039F0A0A1B1A1AE0FBF1F2C9391A380A3D0
+:1003C000449AFF91EF91BF91AF919F918F917F914F
+:1003D0005F914F913F912F910F900FBE0F901F9003
+:1003E00018958091F100982F9A95F1F7089583E080
+:1003F0008ABD83BF789489B7826089BF80E48CBD51
+:100400008FEF8DBD87EC80BF86E886B982E480934C
+:10041000F1004398469A4798419A409A87B32091B1
+:10042000EC00922F9095982397BB88B3822B88BBC2
+:1004300097B32091ED00922B97BB88B3822B88BB9A
+:10044000449A439A80E790E0C2DE81E8BADECBDED0
+:1004500084E790E0BCDE82E8B4DEC5DE83E990E0AC
+:10046000B6DE83E8AEDEBFDE88E790E0B0DE81E096
+:10047000A8DEB9DE82E08093DF0083E890E0A7DEAB
+:1004800083E89FDEB0DE87E08093DF0087E990E0BD
+:100490009EDE83E896DEA7DE8CEA90E098DE1ADE28
+:1004A000FFCFE0EEF0E01382128288EE93E0A0E04E
+:1004B000B0E084839583A683B78384E690E091833C
+:1004C000808382EB90E0908B878782E0848781E055
+:1004D00085878EEF8687118A0895002480FD060E99
+:1004E000660F11F08695D1F7802D0895A1E21A2E9E
+:1004F000AA1BBB1BFD010DC0AA1FBB1FEE1FFF1FC8
+:10050000A217B307E407F50720F0A21BB30BE40B17
+:10051000F50B661F771F881F991F1A9469F760955E
+:100520007095809590959B01AC01BD01CF01089518
+:10053000EE0FFF1F0590F491E02D0994F894FFCF82
+:10054000000000004E00B900AC007A0097004D009A
+:1005500041312000413220006368616E6E656C207D
+:10056000312000696E7465726E616C20312E31200D
+:10057000563A2041332000696E7465726E616C20BA
+:10058000322E353620563A2041332000444F4E4516
+:02059000000069
+:00000001FF
diff --git a/tests/fixtures/runtime-corpus/hex/attiny85/attiny_basics.hex b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_basics.hex
new file mode 100644
index 000000000..41896e7b2
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_basics.hex
@@ -0,0 +1,115 @@
+:100000000FC033C032C031C030C067C12EC0AFC1D5
+:100010002CC02BC02AC029C028C027C026C0EB0294
+:1000200011241FBECFE5D2E0DEBFCDBF21E0AEEF91
+:10003000B0E001C01D92AD33B207E1F710E0A0E6D9
+:10004000B0E0E4E7F6E002C005900D92AE3FB107E4
+:10005000D9F710E0C0E1D0E003C02197FE0102D340
+:10006000CF30D107D1F7D3D103C3CACF3FB7F8946C
+:100070008091270190912801A0912901B0912A0136
+:1000800022B708B601FE05C02F3F19F00196A11D49
+:10009000B11D3FBFBA2FA92F982F8827BC01CD01D2
+:1000A000620F711D811D911D43E0660F771F881F30
+:1000B000991F4A95D1F7089508952FB7F894FC0138
+:1000C000458556853AE06095089420F478B3752309
+:1000D00078BB04C078B3742B78BB000092D191D167
+:1000E00090D18FD166953A9581F72FBF81E090E04E
+:1000F0000895CF93DF936DE08BE291E0DEDFEC01BA
+:100100006AE08BE291E0D9DF8C0F9D1FDF91CF91E8
+:100110000895DC011F96ED91FC9120A131A182A1EF
+:1001200093A12817390779F0A2A1B3A1AE0FBF1F81
+:100130008C9122A133A12F5F3F4F2F71332733A31F
+:1001400022A390E008958FEF9FEF0895DC011F96A2
+:10015000ED91FC9120A131A182A193A1281739072B
+:1001600039F082A193A1E80FF91F808190E00895F2
+:100170008FEF9FEF0895DC011F96ED91FC9180A118
+:1001800091A122A133A1821B930B8F71992708950E
+:10019000CF92DF92EF92FF920F931F93CF93DF9353
+:1001A0006C01EB017B01E40EF51E10E000E0CE15C2
+:1001B000DF0561F06991D601ED91FC910190F0812C
+:1001C000E02DC6010995080F191FF1CFC801DF9175
+:1001D000CF911F910F91FF90EF90DF90CF900895F6
+:1001E000FC0101900020E9F73197AF01481B590B42
+:1001F000BC018BE291E0CCCF8F929F92AF92BF92E5
+:10020000EF92FF920F931F93CF93DF93CDB7DEB79B
+:10021000A1970FB6F894DEBF0FBECDBF162F042FE7
+:1002200019A2423008F40AE07E0121E2E20EF11C3C
+:10023000802E912CB12CA12C612FA5019401F0D11D
+:10024000802F622FE4D1181B1A30F4F4105DF701EF
+:1002500012937F01122F732FCA01232B242B252BDE
+:1002600059F7CF01BDDFA1960FB6F894DEBF0FBEE0
+:10027000CDBFDF91CF911F910F91FF90EF90BF9075
+:10028000AF909F908F900895195CE1CFCF92DF924D
+:10029000EF92FF92CF93DF936B017C01D0E0C0E03F
+:1002A000F7FE0DC06DE28BE291E007DFEC01F09408
+:1002B000E094D094C094C11CD11CE11CF11C4AE014
+:1002C000C701B60199DF8C0F9D1FDF91CF91FF9081
+:1002D000EF90DF90CF90089583CF1F920F920FB6CB
+:1002E0000F9211242F933F938F939F93AF93BF93BC
+:1002F0008091230190912401A0912501B0912601C4
+:100300003091220126E0230F2D3768F129E8230FD1
+:100310000396A11DB11D2093220180932301909388
+:100320002401A0932501B09326018091270190918B
+:100330002801A0912901B0912A010196A11DB11DAA
+:100340008093270190932801A0932901B0932A015B
+:10035000BF91AF919F918F913F912F910F900FBEC1
+:100360000F901F9018950296A11DB11DD4CF1F921A
+:100370000F920FB60F9211242F933F934F935F93D9
+:100380007F938F939F93AF93BF93EF93FF9340918E
+:10039000370120E038E035D034D033D032D031D0FE
+:1003A000889476B3742309F0089427953A9509F058
+:1003B000F3CF27D026D0E0913A01F0913B0180A104
+:1003C00091A101968F71992742A153A18417950796
+:1003D00039F0A0A1B1A1AE0FBF1F2C9391A380A3B0
+:1003E000449AFF91EF91BF91AF919F918F917F912F
+:1003F0005F914F913F912F910F900FBE0F901F90E3
+:10040000189580913C01982F9A95F1F7089583E013
+:100410008ABD83BF789489B7826089BF80E48CBD30
+:100420008FEF8DBD87EC80BF86E886B982E480932C
+:100430003C014398469A4798419A409A87B3209145
+:100440003701922F9095982397BB88B3822B88BB56
+:1004500097B320913801922B97BB88B3822B88BB2E
+:10046000449A439A80E790E037DF4AE062E070E028
+:1004700080E090E0C1DE87EF90E02EDF4AE064E0AC
+:1004800070E080E090E0B8DE87EF90E025DF4AE0A2
+:1004900064E070E080E090E0AFDE87EF90E01CDF8A
+:1004A0004AE064E070E080E090E0A6DE22DE8FE8C3
+:1004B00090E012DF60E070E88FEF9FEFE7DE19DE7B
+:1004C00080EA90E009DF63E070E080E090E0DEDE4B
+:1004D00010DE89EA90E000DF4AE062E070E080E050
+:1004E00090E08ADE87EB90E07BDE65E070E080E004
+:1004F00090E0CCDE60E070E0CB01C8DEFADD89EB95
+:1005000090E0EADE82EC90E06BDEF3DD89EC90E0D7
+:10051000E3DE40E16FE572E080E090E06DDEE9DD72
+:1005200085ED90E0D9DE6FEF71E080E090E0AEDE27
+:10053000E0DD8EED90E0D0DE40E16FEF7FE180E026
+:1005400090E05ADED6DD8FB7F8948090230190902A
+:100550002401A0902501B09026018FBF87DDEB011B
+:100560008BE4C82E82E0D82EE12CF12C7FDD6C1BB1
+:100570007D0B683E734048F1C114D104E104F104DD
+:10058000E1F48FB7F894C0902301D0902401E0905B
+:100590002501F09026018FBF8CEE90E09DDEC70113
+:1005A000B601681979098A099B094AE025DEA1DDAF
+:1005B00089EF90E015DE9DDDFFCF81E0C81AD108FC
+:1005C000E108F108C851DC4FD1CFC114D104E104D6
+:1005D000F10461F6D6CFEBE2F1E01382128288EEED
+:1005E00093E0A0E0B0E084839583A683B78384E69C
+:1005F00090E0918380838EEF90E0908B878782E0FC
+:10060000848781E085878EEF8687118A089500248C
+:1006100080FD060E660F11F08695D1F7802D0895A6
+:10062000A1E21A2EAA1BBB1BFD010DC0AA1FBB1FF6
+:10063000EE1FFF1FA217B307E407F50720F0A21B68
+:10064000B30BE40BF50B661F771F881F991F1A94D5
+:1006500069F760957095809590959B01AC01BD01FF
+:10066000CF010895EE0FFF1F0590F491E02D09943E
+:04067000F894FFCF2C
+:10067400000000005D00C800BB008900A6005C000B
+:1006840073697A656F6620696E74206C6F6E67207B
+:10069400666C6F617420646F75626C653A200069E2
+:1006A4006E74203332373637202B2031203D200022
+:1006B40037202F2032203D20007072696E7428325A
+:1006C4002E3529203D20002E00556D6C6175743A3D
+:1006D4002000C3A4C3B6C3BC0052414D454E4420C0
+:1006E4003D203078004532454E44203D2000464CA4
+:1006F400415348454E44203D2030780064656C6188
+:0E0704007928353837293A2000444F4E4500F9
+:00000001FF
diff --git a/tests/fixtures/runtime-corpus/hex/attiny85/attiny_interrupts.hex b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_interrupts.hex
new file mode 100644
index 000000000..e02b75616
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_interrupts.hex
@@ -0,0 +1,98 @@
+:100000001BC09BC117C23DC03CC04DC13AC0C2C15C
+:1000100038C037C036C035C034C033C032C000002D
+:100020000000380000000000370002020202020255
+:10003000010204081020A20211241FBECFE5D2E065
+:10004000DEBFCDBF20E0AEE7B0E001C01D92A13C15
+:10005000B207E1F710E0A0E6B0E0E2EEF5E002C0A2
+:1000600005900D92AE37B107D9F710E0CCE1D0E0A2
+:1000700003C02197FE01ADD2CB31D107D1F7EBD12F
+:10008000AEC2BECF8091A5008F5F8093A50008957A
+:1000900008952FB7F894FC01458556853AE06095A0
+:1000A000089420F478B3752378BB04C078B3742B1C
+:1000B00078BB0000B9D1B8D1B7D1B6D166953A9521
+:1000C00081F72FBF81E090E00895DC011F96ED914C
+:1000D000FC9120A131A182A193A12817390779F0C1
+:1000E000A2A1B3A1AE0FBF1F8C9122A133A12F5F9C
+:1000F0003F4F2F71332733A322A390E008958FEF52
+:100100009FEF0895DC011F96ED91FC9120A131A194
+:1001100082A193A12817390739F082A193A1E80F92
+:10012000F91F808190E008958FEF9FEF0895DC0123
+:100130001F96ED91FC9180A191A122A133A1821B78
+:10014000930B8F7199270895CF92DF92EF92FF92D0
+:100150000F931F93CF93DF936C01EB017B01E40EB0
+:10016000F51E10E000E0CE15DF0561F06991D601C3
+:10017000ED91FC910190F081E02DC6010995080FE9
+:10018000191FF1CFC801DF91CF911F910F91FF90FF
+:10019000EF90DF90CF9008958F929F92AF92BF9291
+:1001A000EF92FF920F931F93CF93DF93CDB7DEB7FC
+:1001B000A1970FB6F894DEBF0FBECDBF162F042F48
+:1001C00019A2423008F40AE07E0121E2E20EF11C9D
+:1001D000802E912CB12CA12C612FA5019401D7D197
+:1001E000802F622FCBD1181B1A303CF5105DF70120
+:1001F00012937F01122F732FCA01232B242B252B3F
+:1002000059F701900020E9F73197AF014E195F09C6
+:10021000B7018FEA90E098DFA1960FB6F894DEBFA1
+:100220000FBECDBFDF91CF911F910F91FF90EF9047
+:10023000BF90AF909F908F900895195CD8CFFC012C
+:1002400001900020E9F73197AF01481B590BBC0121
+:100250008FEA90E079CFCF93DF9390E0FC01E05DEF
+:10026000FF4F2491865D9F4FFC0184918823C1F04C
+:1002700090E0880F991FFC01EC5DFF4FC591D49170
+:10028000FC01E25EFF4FA591B4919FB7F89488817D
+:10029000E22FE0958E2388838C91E823EC939FBF17
+:1002A000DF91CF9108951F920F920FB60F921124F4
+:1002B0002F933F938F939F93AF93BF938091AB0006
+:1002C0009091AC00A091AD00B091AE003091AA0029
+:1002D00026E0230F2D3768F129E8230F0396A11D8F
+:1002E000B11D2093AA008093AB009093AC00A09323
+:1002F000AD00B093AE008091A6009091A700A091B0
+:10030000A800B091A9000196A11DB11D8093A6007F
+:100310009093A700A093A800B093A900BF91AF91BC
+:100320009F918F913F912F910F900FBE0F901F9033
+:1003300018950296A11DB11DD4CF1F920F920FB632
+:100340000F9211242F933F934F935F936F937F935B
+:100350008F939F93AF93BF93EF93FF938091A200EE
+:100360009091A300892B29F0E091A200F091A300C5
+:100370000995FF91EF91BF91AF919F918F917F91DF
+:100380006F915F914F913F912F910F900FBE0F9002
+:100390001F9018951F920F920FB60F9211242F9352
+:1003A0003F934F935F937F938F939F93AF93BF93AD
+:1003B000EF93FF934091BB0020E038E035D034D07C
+:1003C00033D032D031D0889476B3742309F00894B6
+:1003D00027953A9509F0F3CF27D026D0E091BE00BB
+:1003E000F091BF0080A191A101968F71992742A140
+:1003F00053A18417950739F0A0A1B1A1AE0FBF1F7B
+:100400002C9391A380A3449AFF91EF91BF91AF9158
+:100410009F918F917F915F914F913F912F910F907D
+:100420000FBE0F901F9018958091C000982F9A953D
+:10043000F1F708951F920F920FB60F9211248F9328
+:100440008091A4008F5F8093A4008F910F900FBEC6
+:100450000F901F90189583E08ABD83BF789489B769
+:10046000826089BF80E48CBD8FEF8DBD87EC80BF3B
+:1004700086E886B982E48093C0004398469A4798FC
+:10048000419A409A87B32091BB00922F9095982370
+:1004900097BB88B3822B88BB97B32091BC00922B6B
+:1004A00097BB88B3822B88BB449A439A82E0D3DE01
+:1004B00083E0D1DE8FB7F89422E430E03093A300DC
+:1004C0002093A2008FBF85B7836085BF8BB7806400
+:1004D0008BBFAB9A8BB780628BBF2FB7F89480919C
+:1004E000AB009091AC00A091AD00B091AE002FBFD9
+:1004F00081399140A105B10580F380E790E09FDE4E
+:100500006091A50070E090E080E04AE045DE6DE09B
+:100510008FEA90E0BEDD6AE08FEA90E0BADD86E720
+:1005200090E08DDE6091A40070E090E080E04AE011
+:1005300033DE6DE08FEA90E0ACDD6AE08FEA90E0B8
+:10054000A8DDFFCFEFEAF0E01382128288EE93E09D
+:10055000A0E0B0E084839583A683B78384E690E02F
+:10056000918380838EE790E0908B878782E08487F9
+:1005700081E085878EEF8687118A0895002480FDAB
+:10058000060E660F11F08695D1F7802D0895A1E231
+:100590001A2EAA1BBB1BFD010DC0AA1FBB1FEE1FFD
+:1005A000FF1FA217B307E407F50720F0A21BB30B48
+:1005B000E40BF50B661F771F881F991F1A9469F7C4
+:1005C00060957095809590959B01AC01BD01CF0120
+:1005D0000895EE0FFF1F0590F491E02D0994F89413
+:0205E000FFCF4B
+:1005E200000000004900A400970065008200480056
+:0E05F200494E543020005043494E5420000022
+:00000001FF
diff --git a/tests/fixtures/runtime-corpus/hex/attiny85/attiny_pwm_duty.hex b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_pwm_duty.hex
new file mode 100644
index 000000000..21aace0cf
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_pwm_duty.hex
@@ -0,0 +1,48 @@
+:100000001DC02CC02BC02AC029C0D2C027C026C00A
+:1000100025C024C023C022C021C020C01FC00103AE
+:10002000000004000000000038000000000037005D
+:1000300002020202020201020408102011241FBE63
+:10004000CFE5D2E0DEBFCDBF20E0A0E6B0E001C04A
+:100050001D92A936B207E1F7F5D043C1D1CF3FB722
+:10006000F8948091650090916600A0916700B0912E
+:10007000680022B708B601FE05C02F3F19F00196AF
+:10008000A11DB11D3FBFBA2FA92F982F8827BC01F2
+:10009000CD01620F711D811D911D43E0660F771F19
+:1000A000881F991F4A95D1F7089590E0FC01EA5CFA
+:1000B000FF4F2491805D9F4FFC018491882399F02C
+:1000C00090E0880F991FFC01E65DFF4FA591B49168
+:1000D000FC01EC5DFF4F859194918FB7F894EC9102
+:1000E000E22BEC938FBF089590E0FC01E25EFF4F9E
+:1000F0003491FC01EA5CFF4F2491FC01E05DFF4F6D
+:10010000E491EE23B9F0332329F03130A1F48AB51C
+:100110008F778ABDF0E0EE0FFF1FEC5DFF4FA591DA
+:10012000B4918FB7611118C0F894EC9120952E23EB
+:100130002C938FBF0895323019F48AB58F7DE9CFA3
+:10014000333021F480B78F7D80BFE4CF343011F796
+:100150008CB58F7D8CBDDECFF894EC912E2BE8CF43
+:10016000E82FF0E0E25EFF4FE491E13029F48AB538
+:1001700080688ABD69BD0895E23029F48AB580623D
+:100180008ABD68BD0895E33029F480B7806280BFDE
+:100190006EBD0895E43029F48CB580628CBD6BBDD2
+:1001A00008956038710514F460E09ECF61E0FDCFE2
+:1001B0001F920F920FB60F9211242F933F938F939C
+:1001C0009F93AF93BF938091610090916200A09143
+:1001D0006300B09164003091600026E0230F2D375A
+:1001E00068F129E8230F0396A11DB11D209360003B
+:1001F0008093610090936200A0936300B0936400C9
+:100200008091650090916600A0916700B0916800B0
+:100210000196A11DB11D8093650090936600A09387
+:100220006700B0936800BF91AF919F918F913F910C
+:100230002F910F900FBE0F901F9018950296A11D41
+:10024000B11DD4CF83E08ABD83BF789489B7826023
+:1002500089BF80E48CBD8FEF8DBD87EC80BF86E8C1
+:1002600086B980E022DF68E270E080E079DF81E03B
+:100270001CDF64E670E081E073DF84E016DF68EC89
+:1002800070E084E06DDFEBDEEB0182E3C82ED12C61
+:10029000E12CF12CE4DE6C1B7D0B683E7340D8F042
+:1002A000C114D104E104F10471F480E0FEDE60E0E9
+:1002B00080E01ADF81E0F9DE61E081E015DF60E0D7
+:1002C00084E012DFFFCF81E0C81AD108E108F1080D
+:1002D000C851DC4FDFCFC114D104E104F104D1F6E1
+:0602E000E4CFF894FFCF0B
+:00000001FF
diff --git a/tests/fixtures/runtime-corpus/hex/attiny85/attiny_serial_echo.hex b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_serial_echo.hex
new file mode 100644
index 000000000..3ff50faaf
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/hex/attiny85/attiny_serial_echo.hex
@@ -0,0 +1,103 @@
+:100000000FC033C032C031C030C029C12EC071C151
+:100010002CC02BC02AC029C028C027C026C0B402CB
+:1000200011241FBECFE5D2E0DEBFCDBF20E0ACE79C
+:10003000B0E001C01D92AB3BB207E1F710E0A0E6D3
+:10004000B0E0E8E3F6E002C005900D92AC37B107EE
+:10005000D9F710E0C0E1D0E003C02197FE01E4D25F
+:10006000CF30D107D1F795D1E5C2CACF3FB7F894C9
+:100070008091A1009091A200A091A300B091A40052
+:1000800022B708B601FE05C02F3F19F00196A11D49
+:10009000B11D3FBFBA2FA92F982F8827BC01CD01D2
+:1000A000620F711D811D911D43E0660F771F881F30
+:1000B000991F4A95D1F7089508952FB7F894FC0138
+:1000C000458556853AE06095089420F478B3752309
+:1000D00078BB04C078B3742B78BB000054D153D1E3
+:1000E00052D151D166953A9581F72FBF81E090E0CA
+:1000F0000895682F89EA90E0E0CFDC011F96ED912A
+:10010000FC9120A131A182A193A12817390779F090
+:10011000A2A1B3A1AE0FBF1F8C9122A133A12F5F6B
+:100120003F4F2F71332733A322A390E008958FEF21
+:100130009FEF0895DC011F96ED91FC9120A131A164
+:1001400082A193A12817390739F082A193A1E80F62
+:10015000F91F808190E008958FEF9FEF08952FB7EA
+:10016000F8948091A5009091A600A091A700B0916D
+:10017000A8002FBF8093B1009093B200A093B3006A
+:10018000B093B40089EA90E0D5DF97FF26C02FB77F
+:10019000F8948091A5009091A600A091A700B0913D
+:1001A000A8002FBF4091B1005091B2006091B30000
+:1001B0007091B400841B950BA60BB70B4091AD005A
+:1001C0005091AE006091AF007091B0008417950718
+:1001D000A607B707B8F28FEF9FEF0895DC011F96CF
+:1001E000ED91FC9180A191A122A133A1821B930BDF
+:1001F0008F7199270895CF92DF92EF92FF920F931C
+:100200001F93CF93DF936C01EB017B01E40EF51E8E
+:1002100010E000E0CE15DF0561F06991D601ED91A7
+:10022000FC910190F081E02DC6010995080F191F7E
+:10023000F1CFC801DF91CF911F910F91FF90EF9007
+:10024000DF90CF900895FC0101900020E9F73197ED
+:10025000AF01481B590BBC0189EA90E0CCCF1F923B
+:100260000F920FB60F9211242F933F938F939F936A
+:10027000AF93BF938091A5009091A600A091A70095
+:10028000B091A8003091A00026E0230F2D3768F12F
+:1002900029E8230F0396A11DB11D2093A000809390
+:1002A000A5009093A600A093A700B093A80080910A
+:1002B000A1009091A200A091A300B091A40001968A
+:1002C000A11DB11D8093A1009093A200A093A30053
+:1002D000B093A400BF91AF919F918F913F912F91C7
+:1002E0000F900FBE0F901F9018950296A11DB11D83
+:1002F000D4CF1F920F920FB60F9211242F933F93DA
+:100300004F935F937F938F939F93AF93BF93EF939D
+:10031000FF934091B50020E038E035D034D033D0A1
+:1003200032D031D0889476B3742309F0089427959D
+:100330003A9509F0F3CF27D026D0E091B800F0919C
+:10034000B90080A191A101968F71992742A153A173
+:100350008417950739F0A0A1B1A1AE0FBF1F2C9350
+:1003600091A380A3449AFF91EF91BF91AF919F9188
+:100370008F917F915F914F913F912F910F900FBE81
+:100380000F901F9018958091BA00982F9A95F1F7C9
+:100390000895CF93DF93CDB7DEB7A1970FB6F8944A
+:1003A000DEBF0FBECDBF83E08ABD83BF789489B71F
+:1003B000826089BF80E48CBD8FEF8DBD87EC80BFEC
+:1003C00086E886B953DE8B0134E6C32ED12CE12CAE
+:1003D000F12C4CDE601B710B683E734008F4B0C01A
+:1003E000C114D104E104F10409F0A2C082E48093B5
+:1003F000BA004398469A4798419A409A87B3209109
+:10040000B500922F9095982397BB88B3822B88BB19
+:1004100097B32091B600922B97BB88B3822B88BBF1
+:10042000449A439A80E790E00EDF8DE062DE8AE036
+:1004300060DE89EA90E0D2DE892BD9F390DE8C0170
+:1004400097FD8CC08D32910521F0C0970A9708F076
+:100450007EC0C12CD12C7601B12C01301105E1F008
+:100460000D32110509F477C0C801C0970A97A0F4AE
+:100470002AE030E040E050E0C701B6019AD06801C0
+:10048000110FEE08FF08C60ED71EE81EF91EE0E3A6
+:10049000CE1AD108E108F10889EA90E02EDE5FDE8D
+:1004A0008C01C0970A97C8F201301105B1F2BB2048
+:1004B00041F0F094E094D094C094C11CD11CE11C94
+:1004C000F11C86E790E0BFDE0D2C000CEE08FF0863
+:1004D000F7FE0AC08DE20DDEF094E094D094C09453
+:1004E000C11CD11CE11CF11C1C2D7D2DC70119A2C2
+:1004F0007E01F1E2EF0EF11C2AE0822E912CA12C5C
+:10050000B12C612FA50194016DD0822F6AE048D0F3
+:10051000181B1A303CF5105DF70112937F01122F62
+:10052000732FCA01232B242B252B59F7CF017CCF06
+:1005300021E0C21AD108E108F10808511C4F49CF47
+:10054000C114D104E104F10409F043CF4FCF89EA8B
+:1005500090E0D3DD73CFBB24B3949ECFC12CD12CBC
+:100560007601AFCF195CD8CFE9EAF0E013821282AE
+:1005700088EE93E0A0E0B0E084839583A683B78300
+:1005800084E690E0918380838CE790E0908B87876E
+:1005900082E0848781E085878EEF8687118A0895BF
+:1005A000002480FD060E660F11F08695D1F7802D90
+:1005B0000895EE27FF27AA27BB2708C0A20FB31F65
+:1005C000E41FF51F220F331F441F551F9695879573
+:1005D0007795679598F37040A9F7009799F7BD0153
+:1005E000CF010895A1E21A2EAA1BBB1BFD010DC06D
+:1005F000AA1FBB1FEE1FFF1FA217B307E407F507D3
+:1006000020F0A21BB30BE40BF50B661F771F881FAE
+:10061000991F1A9469F760957095809590959B0144
+:10062000AC01BD01CF010895EE0FFF1F0590F491BD
+:08063000E02D0994F894FFCFBE
+:10063800000000005D00FB00EE007D009A005C00F9
+:0C064800726561647900676F7420000027
+:00000001FF
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_adc_read/attiny_adc_read.ino b/tests/fixtures/runtime-corpus/probes/attiny_adc_read/attiny_adc_read.ino
new file mode 100644
index 000000000..1db317185
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_adc_read/attiny_adc_read.ino
@@ -0,0 +1,28 @@
+// ATtiny85 probe (plan P6.5): analogRead on A0-A3 (PB5, PB2, PB4, PB3) and by
+// channel number, against VCC, 1.1 V and 2.56 V. The first conversion after a
+// reference change is discarded as the datasheet recommends.
+int read(uint8_t pin) {
+ analogRead(pin);
+ return analogRead(pin);
+}
+
+void setup() {
+ Serial.begin(9600);
+ Serial.print("A1 ");
+ Serial.println(read(A1));
+ Serial.print("A2 ");
+ Serial.println(read(A2));
+ Serial.print("A3 ");
+ Serial.println(read(A3));
+ Serial.print("channel 1 ");
+ Serial.println(read(1));
+ analogReference(INTERNAL1V1);
+ Serial.print("internal 1.1 V: A3 ");
+ Serial.println(read(A3));
+ analogReference(INTERNAL2V56);
+ Serial.print("internal 2.56 V: A3 ");
+ Serial.println(read(A3));
+ Serial.println("DONE");
+}
+
+void loop() {}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_adc_read/case.json b/tests/fixtures/runtime-corpus/probes/attiny_adc_read/case.json
new file mode 100644
index 000000000..6d39aab1e
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_adc_read/case.json
@@ -0,0 +1,15 @@
+{
+ "board": "attiny85",
+ "simMs": 400,
+ "provenance": "derived",
+ "toolchain": "arduino-cli 1.5.1, ATTinyCore 1.5.2 (avr-gcc of arduino:avr@1.8.8)",
+ "description": "analogRead of the ATtiny85 on A1-A3 and by channel number with the VCC, 1.1 V and 2.56 V reference (REFS2 at bit 4 of ADMUX).",
+ "basis": "ATtiny85 datasheet 17.13 and table 17-3: ADC = floor(Vin * 1024 / Vref), at most 1023; Vref = VCC (5 V) for DEFAULT, 1.1 V for INTERNAL1V1, 2.56 V for INTERNAL2V56 (REFS2:0 = 110); ATTinyCore analogRead takes the channel (pin & 127), A = 0x80 | n; channels 1, 2, 3 are PB2, PB4, PB3; computed by hand: 2.51 V -> 514, 5 V -> 1023, 0.5 V -> 102 (VCC), 465 (1.1 V), 200 (2.56 V)",
+ "events": [
+ [0, "adcVolts", 1, 2.51],
+ [0, "adcVolts", 2, 5],
+ [0, "adcVolts", 3, 0.5]
+ ],
+ "expectState": [],
+ "tolerances": []
+}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_adc_read/expected.attiny85.txt b/tests/fixtures/runtime-corpus/probes/attiny_adc_read/expected.attiny85.txt
new file mode 100644
index 000000000..678ced280
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_adc_read/expected.attiny85.txt
@@ -0,0 +1,7 @@
+A1 514
+A2 1023
+A3 102
+channel 1 514
+internal 1.1 V: A3 465
+internal 2.56 V: A3 200
+DONE
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_basics/attiny_basics.ino b/tests/fixtures/runtime-corpus/probes/attiny_basics/attiny_basics.ino
new file mode 100644
index 000000000..9be166198
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_basics/attiny_basics.ino
@@ -0,0 +1,37 @@
+// ATtiny85 probe (plan P6.5): types, arithmetic, Serial.print, time and the
+// memory layout of ATTinyCore 1.5.2 at 8 MHz; Serial is TinySoftwareSerial on PB0.
+void setup() {
+ Serial.begin(9600);
+ Serial.print("sizeof int long float double: ");
+ Serial.print(sizeof(int));
+ Serial.print(" ");
+ Serial.print(sizeof(long));
+ Serial.print(" ");
+ Serial.print(sizeof(float));
+ Serial.print(" ");
+ Serial.println(sizeof(double));
+ int i = 32767;
+ i++;
+ Serial.print("int 32767 + 1 = ");
+ Serial.println(i);
+ Serial.print("7 / 2 = ");
+ Serial.println(7 / 2);
+ Serial.print("print(2.5) = ");
+ Serial.println(2.5);
+ Serial.print("Umlaut: ");
+ Serial.println("\xC3\xA4\xC3\xB6\xC3\xBC");
+ Serial.print("RAMEND = 0x");
+ Serial.println(RAMEND, HEX);
+ Serial.print("E2END = ");
+ Serial.println(E2END);
+ Serial.print("FLASHEND = 0x");
+ Serial.println(FLASHEND, HEX);
+ unsigned long t0 = millis();
+ delay(587);
+ unsigned long t1 = millis();
+ Serial.print("delay(587): ");
+ Serial.println(t1 - t0);
+ Serial.println("DONE");
+}
+
+void loop() {}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_basics/case.json b/tests/fixtures/runtime-corpus/probes/attiny_basics/case.json
new file mode 100644
index 000000000..9537da052
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_basics/case.json
@@ -0,0 +1,17 @@
+{
+ "board": "attiny85",
+ "simMs": 1500,
+ "provenance": "derived",
+ "toolchain": "arduino-cli 1.5.1, ATTinyCore 1.5.2 (avr-gcc of arduino:avr@1.8.8)",
+ "description": "Data types, int overflow, integer division, Serial.print of a float and UTF-8 text, RAMEND/E2END/FLASHEND and delay() measured by millis() on the ATtiny85 (8 MHz internal).",
+ "basis": "avr-gcc ABI (int 2, long 4, float and double 4 bytes); two's complement wrap-around; Print::printFloat with 2 digits; avr/iotnx5.h: RAMEND 0x25F, E2END 511, FLASHEND 0x1FFF; ATTinyCore wiring.c: timer 0 at prescaler 64 overflows every 2.048 ms, so each millis() reading lags by up to 2.048 ms and an interval of at least 587 ms measures 585 to 589",
+ "events": [],
+ "expectState": [],
+ "tolerances": [
+ {
+ "pattern": "^delay\\(587\\): (\\d+)$",
+ "plusMinus": 2,
+ "reason": "millis() advances only on the timer 0 overflow (2.048 ms at 8 MHz, prescaler 64): each of the two readings lags by up to one overflow"
+ }
+ ]
+}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_basics/expected.attiny85.txt b/tests/fixtures/runtime-corpus/probes/attiny_basics/expected.attiny85.txt
new file mode 100644
index 000000000..a626e8c50
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_basics/expected.attiny85.txt
@@ -0,0 +1,10 @@
+sizeof int long float double: 2 4 4 4
+int 32767 + 1 = -32768
+7 / 2 = 3
+print(2.5) = 2.50
+Umlaut: äöü
+RAMEND = 0x25F
+E2END = 511
+FLASHEND = 0x1FFF
+delay(587): 587
+DONE
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_interrupts/attiny_interrupts.ino b/tests/fixtures/runtime-corpus/probes/attiny_interrupts/attiny_interrupts.ino
new file mode 100644
index 000000000..9e56e0598
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_interrupts/attiny_interrupts.ino
@@ -0,0 +1,27 @@
+// ATtiny85 probe (plan P6.5): INT0 (PB2, rising) and the pin-change interrupt
+// on PB3 count the edges the case drives; the counts are printed at the end.
+volatile uint8_t int0Count = 0;
+volatile uint8_t pcintCount = 0;
+
+void onInt0() { int0Count++; }
+
+ISR(PCINT0_vect) { pcintCount++; }
+
+void setup() {
+ Serial.begin(9600);
+ pinMode(2, INPUT);
+ pinMode(3, INPUT);
+ attachInterrupt(0, onInt0, RISING);
+ PCMSK |= _BV(PCINT3);
+ GIMSK |= _BV(PCIE);
+}
+
+void loop() {
+ if (millis() > 400) {
+ Serial.print("INT0 ");
+ Serial.println(int0Count);
+ Serial.print("PCINT ");
+ Serial.println(pcintCount);
+ while (true) {}
+ }
+}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_interrupts/case.json b/tests/fixtures/runtime-corpus/probes/attiny_interrupts/case.json
new file mode 100644
index 000000000..0d1f3ec2b
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_interrupts/case.json
@@ -0,0 +1,22 @@
+{
+ "board": "attiny85",
+ "simMs": 600,
+ "provenance": "derived",
+ "toolchain": "arduino-cli 1.5.1, ATTinyCore 1.5.2 (avr-gcc of arduino:avr@1.8.8)",
+ "description": "INT0 on PB2 counts only rising edges, the pin-change interrupt on PB3 counts every change; both share GIMSK and GIFR on the ATtiny85.",
+ "basis": "ATtiny85 datasheet 9.2 and 9.3: INT0 with ISC01:0 = 11 fires on rising edges, PCINT fires on every change of an enabled pin (PCMSK, PCIE in GIMSK); the case drives PB2 low-high three times and PB3 high-low twice: 3 rising edges, 4 changes",
+ "events": [
+ [50, "pin", 2, 1],
+ [70, "pin", 2, 0],
+ [90, "pin", 2, 1],
+ [110, "pin", 2, 0],
+ [130, "pin", 2, 1],
+ [150, "pin", 2, 0],
+ [200, "pin", 3, 1],
+ [220, "pin", 3, 0],
+ [240, "pin", 3, 1],
+ [260, "pin", 3, 0]
+ ],
+ "expectState": [],
+ "tolerances": []
+}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_interrupts/expected.attiny85.txt b/tests/fixtures/runtime-corpus/probes/attiny_interrupts/expected.attiny85.txt
new file mode 100644
index 000000000..3a069a954
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_interrupts/expected.attiny85.txt
@@ -0,0 +1,2 @@
+INT0 3
+PCINT 4
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_pwm_duty/attiny_pwm_duty.ino b/tests/fixtures/runtime-corpus/probes/attiny_pwm_duty/attiny_pwm_duty.ino
new file mode 100644
index 000000000..01a11da0f
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_pwm_duty/attiny_pwm_duty.ino
@@ -0,0 +1,13 @@
+// ATtiny85 probe (plan P6.5): analogWrite on the three PWM pins 0 (timer 0),
+// 1 and 4 (timer 1), then 0 and 255 as digital levels and PWM back to digital.
+void setup() {
+ analogWrite(0, 40);
+ analogWrite(1, 100);
+ analogWrite(4, 200);
+ delay(50);
+ analogWrite(0, 0);
+ analogWrite(1, 255);
+ digitalWrite(4, LOW);
+}
+
+void loop() {}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_pwm_duty/case.json b/tests/fixtures/runtime-corpus/probes/attiny_pwm_duty/case.json
new file mode 100644
index 000000000..365a73c4c
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_pwm_duty/case.json
@@ -0,0 +1,18 @@
+{
+ "board": "attiny85",
+ "simMs": 120,
+ "provenance": "derived",
+ "toolchain": "arduino-cli 1.5.1, ATTinyCore 1.5.2 (avr-gcc of arduino:avr@1.8.8)",
+ "description": "analogWrite on PB0 (OC0A), PB1 (OC1A) and PB4 (OC1B) of the ATtiny85, analogWrite 0/255 as digital levels and digitalWrite ending PWM.",
+ "basis": "ATTinyCore 1.5.2 wiring_analog.c with digital_pin_to_timer_PGM of tinyX5 (0: TIMER0A, 1: TIMER1A with TIMER1_PWM, 4: TIMER1B): analogWrite(0) and analogWrite(255) are digitalWrite(LOW/HIGH), other values set OCR0A, OCR1A, OCR1B; timer 0 in 8-bit fast PWM, timer 1 in PWM mode with OCR1C = 255, so the duty cycle equals the value; digitalWrite on a PWM pin turns PWM off (turnOffPWM)",
+ "events": [],
+ "expectState": [
+ [20, "pwm", 0, 40],
+ [20, "pwm", 1, 100],
+ [20, "pwm", 4, 200],
+ [100, "pin", 0, "OUTPUT", 0],
+ [100, "pin", 1, "OUTPUT", 1],
+ [100, "pin", 4, "OUTPUT", 0]
+ ],
+ "tolerances": []
+}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_pwm_duty/expected.attiny85.txt b/tests/fixtures/runtime-corpus/probes/attiny_pwm_duty/expected.attiny85.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_serial_echo/attiny_serial_echo.ino b/tests/fixtures/runtime-corpus/probes/attiny_serial_echo/attiny_serial_echo.ino
new file mode 100644
index 000000000..5ec6347af
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_serial_echo/attiny_serial_echo.ino
@@ -0,0 +1,15 @@
+// ATtiny85 probe (plan P6.5): Serial input through the analog comparator (RX on
+// PB1): parseInt echoes numbers; input sent before Serial.begin() is lost.
+void setup() {
+ delay(100);
+ Serial.begin(9600);
+ Serial.println("ready");
+}
+
+void loop() {
+ if (Serial.available()) {
+ int n = Serial.parseInt();
+ Serial.print("got ");
+ Serial.println(n);
+ }
+}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_serial_echo/case.json b/tests/fixtures/runtime-corpus/probes/attiny_serial_echo/case.json
new file mode 100644
index 000000000..a274f2c30
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_serial_echo/case.json
@@ -0,0 +1,16 @@
+{
+ "board": "attiny85",
+ "simMs": 3200,
+ "provenance": "derived",
+ "toolchain": "arduino-cli 1.5.1, ATTinyCore 1.5.2 (avr-gcc of arduino:avr@1.8.8)",
+ "description": "TinySoftwareSerial receives through the analog comparator after Serial.begin(); input before it is discarded; parseInt skips non-digits and its 1 s timeout ends a number.",
+ "basis": "ATTinyCore 1.5.2 TinySoftwareSerial.cpp: begin() enables the comparator interrupt (ACIE, bandgap against AIN1 = PB1); without it no byte is received; Stream::parseInt of the core: non-digits are discarded until a digit arrives, the 1 s timeout starts anew with every character, so \"42\" ends 1 s after its last digit (got 42 at about 1.3 s) and \"7\" at about 2.5 s; \"9\" arrives before Serial.begin() and is lost",
+ "events": [
+ [20, "serial", "9"],
+ [300, "serial", "42"],
+ [1500, "serial", "7"]
+ ],
+ "expectState": [],
+ "tolerances": [],
+ "expectDiagnostics": ["serial-input-discarded"]
+}
diff --git a/tests/fixtures/runtime-corpus/probes/attiny_serial_echo/expected.attiny85.txt b/tests/fixtures/runtime-corpus/probes/attiny_serial_echo/expected.attiny85.txt
new file mode 100644
index 000000000..8a7ffeeb1
--- /dev/null
+++ b/tests/fixtures/runtime-corpus/probes/attiny_serial_echo/expected.attiny85.txt
@@ -0,0 +1,3 @@
+ready
+got 42
+got 7
diff --git a/tests/integration/avr-emulator-board-pinmap.test.ts b/tests/integration/avr-emulator-board-pinmap.test.ts
index 136831b15..f29c302de 100644
--- a/tests/integration/avr-emulator-board-pinmap.test.ts
+++ b/tests/integration/avr-emulator-board-pinmap.test.ts
@@ -3,7 +3,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, describe, expect, it } from "vitest";
-import { MEGA2560, NANO, UNO, type BoardHardware } from "@unosim/avr-emulator";
+import { ATTINY85_BOARD, MEGA2560, NANO, UNO, type BoardHardware } from "@unosim/avr-emulator";
/**
* Plan P1.3: the board pin maps of the emulation core must match
@@ -20,16 +20,20 @@ const TIMER_NUMBER: Readonly> = {
"3A": 9, "3B": 10, "3C": 11, "4A": 12, "4B": 13, "4C": 14, "5A": 16, "5B": 17, "5C": 18,
};
-/** Runs the core's own preprocessor (paths from `arduino-cli --show-properties`) over pins_arduino.h. */
-function preprocessedTables(fqbn: string): string {
+/** Build properties of `fqbn` from `arduino-cli --show-properties`, and a scratch sketch directory. */
+function coreProperties(fqbn: string): { sketch: string; prop: (key: string) => string } {
const sketch = join(work, `pinmap_${fqbn.replaceAll(/\W/g, "_")}`);
mkdirSync(sketch, { recursive: true });
writeFileSync(join(sketch, `${sketch.split("/").pop()}.ino`), "void setup() {}\nvoid loop() {}\n");
const shown = spawnSync("arduino-cli", ["compile", "--fqbn", fqbn, "--show-properties", sketch], { encoding: "utf8", timeout: 120_000 });
expect(shown.status, shown.stderr).toBe(0);
const props = new Map(shown.stdout.split("\n").map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]));
- const prop = (key: string) => props.get(key) ?? "";
+ return { sketch, prop: (key: string) => props.get(key) ?? "" };
+}
+/** Runs the core's own preprocessor (paths from `arduino-cli --show-properties`) over pins_arduino.h. */
+function preprocessedTables(fqbn: string): string {
+ const { sketch, prop } = coreProperties(fqbn);
const source = join(sketch, "tables.cpp");
writeFileSync(source, [
"#define ARDUINO_MAIN",
@@ -46,6 +50,30 @@ function preprocessedTables(fqbn: string): string {
return result.stdout;
}
+/**
+ * Integer expressions of the core (`analogInputToDigitalPin(2)`, `LED_BUILTIN`)
+ * evaluated by its compiler, which the preprocessor alone does not do: the
+ * constants are compiled to assembly and read from their `.word`.
+ */
+function compiledConstants(fqbn: string, expressions: Readonly>): Record {
+ const { sketch, prop } = coreProperties(fqbn);
+ const source = join(sketch, "constants.cpp");
+ writeFileSync(source, [
+ "#include ",
+ ...Object.entries(expressions).map(([name, expression]) => `extern const int unosim_${name} = ${expression};`),
+ ].join("\n"));
+ const result = spawnSync(`${prop("compiler.path")}${prop("compiler.cpp.cmd")}`, [
+ "-S", "-O1", "-o", "-", `-mmcu=${prop("build.mcu")}`, `-DF_CPU=${prop("build.f_cpu")}`, `-DARDUINO=${prop("runtime.ide.version")}`,
+ `-DARDUINO_${prop("build.board")}`, `-DARDUINO_ARCH_${prop("build.arch")}`,
+ `-I${prop("build.core.path")}`, `-I${prop("build.variant.path")}`, source,
+ ], { encoding: "utf8", timeout: 60_000 });
+ expect(result.status, result.stderr).toBe(0);
+ return Object.fromEntries(Object.keys(expressions).map((name) => {
+ const word = new RegExp(String.raw`unosim_${name}:\s*\.word\s+(-?\d+)`).exec(result.stdout)?.[1];
+ return [name, word === undefined ? Number.NaN : (Number(word) << 16) >> 16];
+ }));
+}
+
function table(source: string, name: string): number[] {
const body = new RegExp(String.raw`${name}\[\]\s*=\s*\{([^}]*)\}`).exec(source)?.[1];
expect(body, name).toBeDefined();
@@ -84,3 +112,34 @@ describe.each([
expect(board.ledBuiltin).toBe(scalar(source, "unosimLedBuiltin"));
});
});
+
+/**
+ * ATTinyCore 1.5.2 (P6.4): same port and timer numbering as arduino:avr; the
+ * analog names are second names (`A1 = 0x80 | 1`), resolved by
+ * `analogInputToDigitalPin`, and NUM_ANALOG_INPUTS counts channels without a pin.
+ */
+describe("pin map of ATTinyCore:avr:attinyx5:chip=85,clock=8internal", () => {
+ const source = preprocessedTables("ATTinyCore:avr:attinyx5:chip=85,clock=8internal");
+ const ports = table(source, "digital_pin_to_port_PGM");
+ const masks = table(source, "digital_pin_to_bit_mask_PGM");
+ const timers = table(source, "digital_pin_to_timer_PGM");
+ const constants = compiledConstants("ATTinyCore:avr:attinyx5:chip=85,clock=8internal", {
+ ledBuiltin: "LED_BUILTIN",
+ numAnalog: "NUM_ANALOG_INPUTS",
+ ...Object.fromEntries(Array.from({ length: 16 }, (_, channel) => [`analog${channel}`, `analogInputToDigitalPin(${channel})`])),
+ });
+
+ it("has the same digital pins with port, bit and timer", () => {
+ expect(ATTINY85_BOARD.pins.map((pin) => pin.pin)).toEqual(ports.map((_, index) => index));
+ for (const pin of ATTINY85_BOARD.pins) {
+ expect([ports[pin.pin], masks[pin.pin]], pin.label).toEqual([PORT_NUMBER[pin.port?.port ?? ""], 1 << (pin.port?.bit ?? 0)]);
+ expect(timers[pin.pin], `${pin.label} timer`).toBe(pin.pwm ? TIMER_NUMBER[`${pin.pwm.timer}${pin.pwm.channel}`] : 0);
+ }
+ });
+
+ it("has the analog channels on the pins of analogInputToDigitalPin and LED_BUILTIN 1", () => {
+ const channels = Array.from({ length: constants.numAnalog }, (_, channel) => [channel, constants[`analog${channel}`]] as const).filter(([, pin]) => pin >= 0);
+ expect(ATTINY85_BOARD.pins.filter((pin) => pin.adcChannel !== undefined).map((pin) => [pin.adcChannel, pin.pin]).sort()).toEqual(channels.map(([channel, pin]) => [channel, pin]).sort());
+ expect(ATTINY85_BOARD.ledBuiltin).toBe(constants.ledBuiltin);
+ });
+});
diff --git a/tests/server/config.test.ts b/tests/server/config.test.ts
index d1ef78538..77cdea96f 100644
--- a/tests/server/config.test.ts
+++ b/tests/server/config.test.ts
@@ -140,6 +140,7 @@ describe("central configuration validation", () => {
{ id: "uno", displayName: "Arduino UNO", status: "available", runtimes: ["browser", "host-mock"] },
{ id: "nano", displayName: "Arduino Nano", status: "preview", runtimes: ["browser"] },
{ id: "mega2560", displayName: "Arduino Mega 2560", status: "preview", runtimes: ["browser"] },
+ { id: "attiny85", displayName: "ATtiny85", status: "preview", runtimes: ["browser"] },
]);
expect(clientConfig.runtime).toEqual({ backends: ["browser", "host-mock"], default: "browser" });
expect(JSON.stringify(clientConfig.boards)).not.toContain("fqbn");
diff --git a/tests/server/routes/compiler-board.routes.test.ts b/tests/server/routes/compiler-board.routes.test.ts
index e275000b5..3a5e190be 100644
--- a/tests/server/routes/compiler-board.routes.test.ts
+++ b/tests/server/routes/compiler-board.routes.test.ts
@@ -75,6 +75,7 @@ describe("compiler.routes - board selection", () => {
});
it("refuses a board the deployment does not offer", async () => {
+ offered = enabledBoards(undefined, "uno", ["host-mock"]); // without the browser backend only the Uno is offered
const result = await postCompile(baseUrl, { code: "x", board: "attiny85" });
expect(result.status).toBe(400);
@@ -83,6 +84,7 @@ describe("compiler.routes - board selection", () => {
});
it("refuses the deprecated fqbn of a board that is not offered", async () => {
+ offered = enabledBoards(undefined, "uno", ["host-mock"]);
const result = await postCompile(baseUrl, { code: "x", fqbn: "ATTinyCore:avr:attinyx5:chip=85,clock=8internal" });
expect(result.status).toBe(400);
diff --git a/tests/shared/attiny85-pins-and-rules.test.ts b/tests/shared/attiny85-pins-and-rules.test.ts
new file mode 100644
index 000000000..881b1b73c
--- /dev/null
+++ b/tests/shared/attiny85-pins-and-rules.test.ts
@@ -0,0 +1,86 @@
+import { describe, expect, it } from "vitest";
+import { boardAnalogPins, boardPinLabel, boardPins, boardPwmPins, isBoardPin, resolveBoardPin } from "@shared/board-pins";
+import { boardPinAliases } from "@shared/boards";
+import { CodeParser } from "@shared/code-parser";
+import { analyzeStaticIO } from "@shared/io-registry-parser";
+import { pinContext } from "@shared/parsers/pin-context";
+
+const pinsOf = (code: string) => analyzeStaticIO(code, { board: "attiny85" }).pins.map((pin) => pin.pinId);
+const warnings = (code: string) =>
+ new CodeParser(undefined, undefined, "attiny85").parseHardwareCompatibility(code).map((message) => message.message);
+
+describe("ATtiny85 pins (plan P6.4)", () => {
+ it("has the pins 0–5; A0–A3 are second names of 5, 2, 4, 3", () => {
+ expect(boardPins("attiny85")?.map((pin) => [pin.pin, pin.name, pin.analogName ?? ""])).toEqual([
+ [0, "0", ""], [1, "1", ""], [2, "2", "A1"], [3, "3", "A3"], [4, "4", "A2"], [5, "5", "A0"],
+ ]);
+ expect(isBoardPin("attiny85", 6)).toBe(false);
+ expect(boardPwmPins("attiny85")).toEqual([0, 1, 4]);
+ expect(boardAnalogPins("attiny85").map((pin) => pin.analogName)).toEqual(["A1", "A3", "A2", "A0"]);
+ expect(boardPinLabel("attiny85", 2)).toBe("2");
+ });
+
+ it("resolves A1, a1 and LED_BUILTIN the way the core does", () => {
+ expect(resolveBoardPin("attiny85", "A1")).toBe(2);
+ expect(resolveBoardPin("attiny85", "a0")).toBe(5);
+ expect(resolveBoardPin("attiny85", "LED_BUILTIN")).toBe(1);
+ expect(resolveBoardPin("attiny85", "A4")).toBeUndefined();
+ expect(boardPinAliases("attiny85")).toMatchObject({ A0: 5, A1: 2, A2: 4, A3: 3, LED_BUILTIN: 1 });
+ });
+
+ it("gives the parsers the analog names and the software Serial on 0/1", () => {
+ const context = pinContext("attiny85");
+ // As in ATTinyCore: A is 0x80 | n, which every pin function maps to its pin.
+ expect(context.builtinConstants()).toEqual({ LED_BUILTIN: 1, A0: 0x80, A1: 0x81, A2: 0x82, A3: 0x83 });
+ expect(context.callPin(0x81, "digitalWrite")).toBe(2);
+ expect(context.callPin(1, "analogRead")).toBe(2);
+ expect(context.callPin(1, "digitalWrite")).toBe(1);
+ expect(context.softwareSerialPins()).toEqual({ tx: 0, rx: 1 });
+ expect(context.role(5)).toBe("reset");
+ expect(context.serialPortCount()).toBe(1);
+ expect(pinContext("uno").softwareSerialPins()).toBeUndefined();
+ });
+});
+
+describe("static analysis and rules for the ATtiny85 (plan P6.4)", () => {
+ it("maps A1 and analogRead(1) to pin 2, as the core does (analogRead takes the channel)", () => {
+ expect(pinsOf("void loop(){ analogRead(A1); }")).toEqual([2]);
+ expect(pinsOf("void loop(){ analogRead(1); }")).toEqual([2]);
+ expect(pinsOf("void loop(){ analogRead(A0); }")).toEqual([5]);
+ expect(pinsOf("void loop(){ digitalWrite(6, HIGH); }")).toEqual([]);
+ });
+
+ it("warns about digital use of the RESET pin, not about analogRead(A0)", () => {
+ expect(warnings("void setup(){ pinMode(5, OUTPUT); } void loop(){ digitalWrite(5, HIGH); }").filter((m) => m.includes("RESET"))).toHaveLength(2);
+ expect(warnings("void setup(){} void loop(){ analogRead(A0); }").some((m) => m.includes("RESET"))).toBe(false);
+ });
+
+ it("warns about other use of the Serial pins only when the sketch uses Serial", () => {
+ const withSerial = warnings("void setup(){ Serial.begin(9600); pinMode(0, OUTPUT); analogWrite(1, 10); } void loop(){ Serial.println(1); }");
+ expect(withSerial.filter((m) => m.includes("Serial TX"))).toHaveLength(1);
+ expect(withSerial.filter((m) => m.includes("Serial RX"))).toHaveLength(1);
+ expect(warnings("void setup(){ pinMode(0, OUTPUT); } void loop(){ digitalWrite(0, HIGH); }").some((m) => m.includes("Serial"))).toBe(false);
+ expect(warnings("// Serial.begin(9600);\nvoid setup(){ pinMode(1, OUTPUT); } void loop(){}").some((m) => m.includes("Serial"))).toBe(false);
+ });
+
+ it("names the PWM pins of the ATtiny85 and refuses Serial1", () => {
+ expect(warnings("void setup(){} void loop(){ analogWrite(3, 10); }")).toContainEqual(expect.stringContaining("PWM pins: 0, 1, 4"));
+ const serial = new CodeParser(undefined, undefined, "attiny85").parseSerialConfiguration("void setup(){ Serial1.begin(9600); } void loop(){ Serial1.println(1); }");
+ expect(serial.map((m) => m.message)).toContainEqual(expect.stringContaining("Serial1 does not exist on ATtiny85"));
+ });
+
+ it("leaves the Uno without the new rules", () => {
+ const uno = new CodeParser(undefined, undefined, "uno").parseHardwareCompatibility("void setup(){ Serial.begin(9600); pinMode(1, OUTPUT); pinMode(5, OUTPUT); } void loop(){}");
+ expect(uno.some((m) => m.message.includes("RESET") || m.message.includes("Serial TX"))).toBe(false);
+ });
+});
+
+describe("sketchSerialBaud (plan P6.5)", () => {
+ it("reads an active Serial.begin() literal, not a commented one", async () => {
+ const { sketchSerialBaud } = await import("@shared/parser-patterns");
+ expect(sketchSerialBaud("void setup(){ Serial.begin(9600); }")).toBe(9600);
+ expect(sketchSerialBaud("// Serial.begin(4800);\nvoid setup(){ Serial.begin( 19200 ); }")).toBe(19200);
+ expect(sketchSerialBaud("// Serial.begin(4800);\nvoid setup(){}")).toBeUndefined();
+ expect(sketchSerialBaud("const long BAUD = 9600; void setup(){ Serial.begin(BAUD); }")).toBeUndefined();
+ });
+});
diff --git a/tests/shared/board-pins.test.ts b/tests/shared/board-pins.test.ts
index 22ccedd85..ca8485499 100644
--- a/tests/shared/board-pins.test.ts
+++ b/tests/shared/board-pins.test.ts
@@ -2,6 +2,9 @@ import { describe, expect, it } from "vitest";
import {
boardAnalogPins, boardPinLabel, boardPins, boardPwmPins, findBoardPin, isBoardPin, maxBoardPin, resolveBoardPin,
} from "@shared/board-pins";
+import type { BoardId } from "@shared/boards";
+
+const NO_HARDWARE = "no-hardware" as BoardId;
describe("board pins (plan P4.1)", () => {
it("lists the 20 pins of the Uno: D0–D13 and A0–A5 as 14–19, PWM on 3 5 6 9 10 11", () => {
@@ -68,11 +71,12 @@ describe("board pins (plan P4.1)", () => {
});
it("has no pins for a board without a hardware profile yet", () => {
- expect(boardPins("attiny85")).toBeUndefined();
- expect(isBoardPin("attiny85", 2)).toBe(false);
- expect(resolveBoardPin("attiny85", "0")).toBeUndefined();
- expect(maxBoardPin("attiny85")).toBeUndefined();
- expect(boardPwmPins("attiny85")).toEqual([]);
- expect(boardPinLabel("attiny85", 3)).toBe("3");
+ // Every catalog board has hardware since P6.4; an id without it has no pins.
+ expect(boardPins(NO_HARDWARE)).toBeUndefined();
+ expect(isBoardPin(NO_HARDWARE, 2)).toBe(false);
+ expect(resolveBoardPin(NO_HARDWARE, "0")).toBeUndefined();
+ expect(maxBoardPin(NO_HARDWARE)).toBeUndefined();
+ expect(boardPwmPins(NO_HARDWARE)).toEqual([]);
+ expect(boardPinLabel(NO_HARDWARE, 3)).toBe("3");
});
});
diff --git a/tests/shared/boards.test.ts b/tests/shared/boards.test.ts
index ec2f30a79..d2c8817cd 100644
--- a/tests/shared/boards.test.ts
+++ b/tests/shared/boards.test.ts
@@ -1,13 +1,14 @@
import { describe, expect, it } from "vitest";
import { BOARD_HARDWARE_IDS, boardHardware, hasBoardHardware } from "@unosim/avr-emulator/boards";
+import type { BoardId } from "../../shared/boards";
import {
BOARD_CATALOG, BOARD_IDS, BOARD_STATUSES, DEFAULT_BOARD_ID, RUNTIME_KINDS, boardIdSchema, boardPinAliases, enabledBoards, findBoardByFqbn, fqbnAllowlist, getBoard,
} from "../../shared/boards";
describe("board catalog (plan 4.3, P2.1)", () => {
- it("lists the four boards of the plan: the Uno is available since M1, the Nano and the Mega are in preview, the ATtiny85 is planned", () => {
+ it("lists the four boards of the plan: the Uno is available since M1, the Nano, the Mega and the ATtiny85 (since P6.5) are in preview", () => {
expect(BOARD_CATALOG.map((board) => board.id)).toEqual(["uno", "nano", "mega2560", "attiny85"]);
- expect(BOARD_CATALOG.map((board) => board.status)).toEqual(["available", "preview", "preview", "planned"]);
+ expect(BOARD_CATALOG.map((board) => board.status)).toEqual(["available", "preview", "preview", "preview"]);
});
it("has exactly the boards of the emulation package", () => {
@@ -58,13 +59,13 @@ describe("board catalog (plan 4.3, P2.1)", () => {
});
describe("enabledBoards", () => {
- it("offers the Uno, the Nano and the Mega (preview) by default, not the planned boards", () => {
- expect(enabledBoards().map((board) => board.id)).toEqual([DEFAULT_BOARD_ID, "nano", "mega2560"]);
+ it("offers the Uno, the Nano, the Mega and the ATtiny85 (preview) by default, not the planned boards", () => {
+ expect(enabledBoards().map((board) => board.id)).toEqual([DEFAULT_BOARD_ID, "nano", "mega2560", "attiny85"]);
});
it("offers only the boards the offered backends can run: the Uno alone without the browser backend", () => {
expect(enabledBoards(undefined, "uno", ["host-mock"]).map((board) => board.id)).toEqual(["uno"]);
- expect(enabledBoards(undefined, "uno", ["browser", "host-mock"]).map((board) => board.id)).toEqual(["uno", "nano", "mega2560"]);
+ expect(enabledBoards(undefined, "uno", ["browser", "host-mock"]).map((board) => board.id)).toEqual(["uno", "nano", "mega2560", "attiny85"]);
});
it("does not filter an explicit list by backend (the startup check rejects an impossible one)", () => {
@@ -100,7 +101,8 @@ describe("board catalog (plan 4.3, P2.1)", () => {
});
it("has none for boards without a hardware profile yet", () => {
- expect(boardPinAliases("attiny85")).toBeUndefined();
+ // Every catalog board has hardware since P6.4; an id without it has no aliases.
+ expect(boardPinAliases("no-hardware" as BoardId)).toBeUndefined();
});
});
});
diff --git a/tests/shared/parsers-per-board.test.ts b/tests/shared/parsers-per-board.test.ts
index b709c27ae..16a9fd3c5 100644
--- a/tests/shared/parsers-per-board.test.ts
+++ b/tests/shared/parsers-per-board.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { CodeParser } from "@shared/code-parser";
import { analyzeStaticIO, parseStaticIORegistry, parseStaticIORegistryProject } from "@shared/io-registry-parser";
import { pinContext } from "@shared/parsers/pin-context";
+import type { BoardId } from "@shared/boards";
const NANO_SKETCH = `void setup() { Serial.begin(115200); pinMode(13, OUTPUT); }
void loop() {
@@ -49,8 +50,9 @@ describe("static I/O analysis per board (plan P4.2)", () => {
});
it("falls back to the Uno for a board without hardware profile yet", () => {
- expect(pinContext("attiny85").board).toBe("uno");
- expect(pinContext("attiny85").maxPin).toBe(19);
+ // Every catalog board has hardware since P6.4; an id without it falls back to the Uno.
+ expect(pinContext("no-hardware" as BoardId).board).toBe("uno");
+ expect(pinContext("no-hardware" as BoardId).maxPin).toBe(19);
});
});