diff --git a/docs/proposals/browser-emulation-plan.md b/docs/proposals/browser-emulation-plan.md index ee2f38414..9ea4cf46a 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,10 +1151,23 @@ 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`. diff --git a/docs/proposals/browser-emulation-status.md b/docs/proposals/browser-emulation-status.md index 22787e009..4e05000ac 100644 --- a/docs/proposals/browser-emulation-status.md +++ b/docs/proposals/browser-emulation-status.md @@ -68,6 +68,9 @@ | 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 | +| P6.2 | erledigt | (dieser PR) | `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) | (dieser PR) | `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 | (dieser PR) | `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 | | 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 | Status: offen · in Arbeit · erledigt · wartet auf Mensch · blockiert 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..50c5d2612 --- /dev/null +++ b/packages/avr-emulator/src/io/software-uart.ts @@ -0,0 +1,297 @@ +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 it is an output and not driven by + * timer 0 PWM. The baud rate is not in any register: it is the largest + * standard bit time of which the edge distances are whole multiples, the bit + * width is then refined from them (the core's delay loop runs a few percent + * slow). Until enough edges fix it, the configured baud rate (the Serial + * Monitor'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; +/** 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 (!(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]); + for (const baud of STANDARD_BAUDS) { + const bit = this.clockHz / baud; + const inFrame = intervals.filter((distance) => distance < bit * 9.5); + if (inFrame.length < MIN_FIT_INTERVALS) continue; + const fitting = inFrame.filter((distance) => { + const bits = Math.round(distance / bit); + return bits >= 1 && Math.abs(distance - bits * bit) < bit * 0.2; + }); + if (fitting.length < inFrame.length * MIN_FIT_SHARE) continue; + const measured = fitting.reduce((sum, distance) => sum + distance / Math.round(distance / bit), 0) / fitting.length; + this.lock(baud, measured); + return; + } + } + + 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/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..544ec47a6 --- /dev/null +++ b/packages/avr-emulator/tests/attiny85-board.test.ts @@ -0,0 +1,87 @@ +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"); + +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 + e.advance(1500); + const lines = text(e).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); + }); + + it("shows the analogWrite duty cycles of timer 1 (PB4) and timer 0 (PB0)", () => { + const e = emulator("tiny_probe"); + e.advance(1000); + 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", () => { + const e = emulator("tiny_probe"); + e.setAnalog(2, 512); + e.advance(1000); + 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/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..f67bd5734 --- /dev/null +++ b/packages/avr-emulator/tests/software-uart.test.ts @@ -0,0 +1,133 @@ +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(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("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/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..0ec942c49 100644 --- a/shared/boards.ts +++ b/shared/boards.ts @@ -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/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..ab03fd5cc 100644 --- a/ssot/ssot_function_definition_Boards.md +++ b/ssot/ssot_function_definition_Boards.md @@ -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/tests/client/lib/active-board.test.ts b/tests/client/lib/active-board.test.ts index 70a618ac4..8dd0ccd4b 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,7 +31,8 @@ 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); 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/shared/attiny85-pins-and-rules.test.ts b/tests/shared/attiny85-pins-and-rules.test.ts new file mode 100644 index 000000000..def1fb88c --- /dev/null +++ b/tests/shared/attiny85-pins-and-rules.test.ts @@ -0,0 +1,76 @@ +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); + }); +}); 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..117ef2647 100644 --- a/tests/shared/boards.test.ts +++ b/tests/shared/boards.test.ts @@ -1,5 +1,6 @@ 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"; @@ -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); }); });