From 44e1c03369772d23a25f9d3f2d3c99af10c0f371 Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Fri, 23 Oct 2026 19:57:47 +0200 Subject: [PATCH 1/3] feat(emulator): add the ATtiny85 MCU profile (P6.2) Port B with INT0 and PCINT0, timer 0 with its own registers and one-word vectors, the ATtiny timer 1, a four-channel ADC that reads REFS2 at bit 4, EEPROM timed for 8 MHz, watchdog and clock prescaler. INT0 and the pin-change interrupt share GIMSK and GIFR, which avr8js wires for one of them only; the profile wraps both hooks. Registered with the board hardware in P6.4. Co-Authored-By: Claude Opus 5.5 --- docs/proposals/browser-emulation-plan.md | 6 + docs/proposals/browser-emulation-status.md | 1 + packages/avr-emulator/README.md | 27 +++ packages/avr-emulator/src/index.ts | 1 + packages/avr-emulator/src/mcu/attiny85.ts | 202 ++++++++++++++++++ packages/avr-emulator/src/mcu/registry.ts | 2 +- .../tests/attiny85-profile.test.ts | 194 +++++++++++++++++ .../avr-emulator/tests/fixtures/README.md | 10 + .../fixtures/attiny85-tiny_interrupts.hex | 42 ++++ .../tests/fixtures/attiny85-tiny_probe.hex | 61 ++++++ .../tests/fixtures/attiny85-tiny_watchdog.hex | 23 ++ .../tiny_interrupts/tiny_interrupts.ino | 25 +++ .../sketches/tiny_probe/tiny_probe.ino | 29 +++ .../sketches/tiny_watchdog/tiny_watchdog.ino | 15 ++ 14 files changed, 637 insertions(+), 1 deletion(-) create mode 100644 packages/avr-emulator/src/mcu/attiny85.ts create mode 100644 packages/avr-emulator/tests/attiny85-profile.test.ts create mode 100644 packages/avr-emulator/tests/fixtures/attiny85-tiny_interrupts.hex create mode 100644 packages/avr-emulator/tests/fixtures/attiny85-tiny_probe.hex create mode 100644 packages/avr-emulator/tests/fixtures/attiny85-tiny_watchdog.hex create mode 100644 packages/avr-emulator/tests/fixtures/sketches/tiny_interrupts/tiny_interrupts.ino create mode 100644 packages/avr-emulator/tests/fixtures/sketches/tiny_probe/tiny_probe.ino create mode 100644 packages/avr-emulator/tests/fixtures/sketches/tiny_watchdog/tiny_watchdog.ino diff --git a/docs/proposals/browser-emulation-plan.md b/docs/proposals/browser-emulation-plan.md index ee2f38414..fce635922 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 diff --git a/docs/proposals/browser-emulation-status.md b/docs/proposals/browser-emulation-status.md index 22787e009..defad9c6d 100644 --- a/docs/proposals/browser-emulation-status.md +++ b/docs/proposals/browser-emulation-status.md @@ -68,6 +68,7 @@ | 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). Profil noch nicht in `mcuProfile` registriert: das hängt an einer Teständerung (siehe P6.4). Upstream-Meldungen an avr8js: wartet auf Mensch | | 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..cb43c3328 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,18 @@ 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)`). The +profile is exported as `ATTINY85`; `mcuProfile("attiny85")` returns it once +the board hardware exists (P6.4). Tests: `tests/attiny85-profile.test.ts` with +the fixtures `attiny85-tiny_*.hex`. + ## License MIT, like UnoSim. diff --git a/packages/avr-emulator/src/index.ts b/packages/avr-emulator/src/index.ts index c8ed6cab3..2c66cb807 100644 --- a/packages/avr-emulator/src/index.ts +++ b/packages/avr-emulator/src/index.ts @@ -31,6 +31,7 @@ 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/mcu/attiny85.ts b/packages/avr-emulator/src/mcu/attiny85.ts new file mode 100644 index 000000000..4488bf7b2 --- /dev/null +++ b/packages/avr-emulator/src/mcu/attiny85.ts @@ -0,0 +1,202 @@ +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 + // Timer 1 of the ATtiny85 has its own register set (TCCR1, GTCCR, OCR1A–C); its PWM observation follows with the board (P6.4). + timerRegisters: [timerRegisters(timer0)], + createPeripherals(cpu, clockHz, eeprom) { + const clock = new AVRClock(cpu, clockHz, { CLKPR }); + const port = new AVRIOPort(cpu, portB); + shareInterruptRegisters(cpu); + new ATtinyTimer1(cpu, attinyTimer1Config); + return { + ports: new Map([["B", port]]), + timers: [new AVRTimer(cpu, timer0)], + 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..21abb6820 100644 --- a/packages/avr-emulator/src/mcu/registry.ts +++ b/packages/avr-emulator/src/mcu/registry.ts @@ -3,7 +3,7 @@ import { ATMEGA328P } from "./atmega328p"; import type { McuId } from "./mcus"; import type { McuProfile } from "./types"; -/** ATtiny85 (P6.2) follows. */ +/** The ATtiny85 profile (mcu/attiny85.ts, P6.2) is registered with its board hardware (P6.4). */ const PROFILES: Partial> = { atmega328p: ATMEGA328P, atmega2560: ATMEGA2560 }; export function mcuProfile(id: McuId): McuProfile { 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..2c68c18b0 --- /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.length).toBe(0x260); + expect(cpu.SP).toBe(0x25f); + expect(ATTINY85.eepromBytes).toBe(512); + }); + + it("has one port, timer 0, no hardware USART", () => { + const { peripherals } = tiny(); + expect([...peripherals.ports.keys()]).toEqual(["B"]); + expect(peripherals.timers).toHaveLength(1); + 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..d03a1fa67 100644 --- a/packages/avr-emulator/tests/fixtures/README.md +++ b/packages/avr-emulator/tests/fixtures/README.md @@ -50,3 +50,13 @@ 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. 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_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_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_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() {} From 4eb5fa4e3f3549056fe135519bb54cb8cc1a71b8 Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Fri, 23 Oct 2026 20:01:17 +0200 Subject: [PATCH 2/3] refactor(emulator): keep the ATtiny timer 1 among the peripherals Co-Authored-By: Claude Opus 5.5 --- packages/avr-emulator/src/mcu/attiny85.ts | 3 +-- packages/avr-emulator/src/mcu/types.ts | 5 +++-- packages/avr-emulator/tests/attiny85-profile.test.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/avr-emulator/src/mcu/attiny85.ts b/packages/avr-emulator/src/mcu/attiny85.ts index 4488bf7b2..442ae01fe 100644 --- a/packages/avr-emulator/src/mcu/attiny85.ts +++ b/packages/avr-emulator/src/mcu/attiny85.ts @@ -179,10 +179,9 @@ export const ATTINY85: McuProfile = { const clock = new AVRClock(cpu, clockHz, { CLKPR }); const port = new AVRIOPort(cpu, portB); shareInterruptRegisters(cpu); - new ATtinyTimer1(cpu, attinyTimer1Config); return { ports: new Map([["B", port]]), - timers: [new AVRTimer(cpu, timer0)], + timers: [new AVRTimer(cpu, timer0), new ATtinyTimer1(cpu, attinyTimer1Config)], usarts: [], adc: new Attiny85ADC(cpu, adc), eeprom: new AVREEPROM(cpu, new ArrayEepromBackend(eeprom), { 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/attiny85-profile.test.ts b/packages/avr-emulator/tests/attiny85-profile.test.ts index 2c68c18b0..20d2f9bc0 100644 --- a/packages/avr-emulator/tests/attiny85-profile.test.ts +++ b/packages/avr-emulator/tests/attiny85-profile.test.ts @@ -64,15 +64,15 @@ describe("ATtiny85 profile (plan P6.2)", () => { const { cpu } = tiny(); expect(ATTINY85.flashBytes).toBe(8192); expect(ramEnd(ATTINY85.memory)).toBe(0x25f); - expect(cpu.data.length).toBe(0x260); + expect(cpu.data).toHaveLength(0x260); expect(cpu.SP).toBe(0x25f); expect(ATTINY85.eepromBytes).toBe(512); }); - it("has one port, timer 0, no hardware USART", () => { + it("has one port, timers 0 and 1, no hardware USART", () => { const { peripherals } = tiny(); expect([...peripherals.ports.keys()]).toEqual(["B"]); - expect(peripherals.timers).toHaveLength(1); + expect(peripherals.timers).toHaveLength(2); expect(peripherals.usarts).toEqual([]); }); From 5c7284fe2f35a6b9d6125760c98aa4e0335d9dfd Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Fri, 23 Oct 2026 20:10:21 +0200 Subject: [PATCH 3/3] feat(emulator): model the software UART of the ATtiny85 (P6.3) TX is decoded from PB0 with the baud rate found from the edge distances and the bit width measured from them; RX drives PB1 through an analog comparator model. SoftwareUart offers the USART members SerialPorts uses, for the ATtiny85 board. Co-Authored-By: Claude Opus 5.5 --- docs/proposals/browser-emulation-plan.md | 6 + docs/proposals/browser-emulation-status.md | 1 + packages/avr-emulator/README.md | 30 ++ packages/avr-emulator/src/index.ts | 2 + packages/avr-emulator/src/io/software-uart.ts | 297 ++++++++++++++++++ .../avr-emulator/tests/fixtures/README.md | 5 + .../tests/fixtures/attiny85-tiny_echo.hex | 103 ++++++ .../tests/fixtures/attiny85-tiny_serial.hex | 135 ++++++++ .../fixtures/attiny85-tiny_serial_19200.hex | 61 ++++ .../fixtures/attiny85-tiny_serial_ff.hex | 55 ++++ .../fixtures/sketches/tiny_echo/tiny_echo.ino | 15 + .../sketches/tiny_serial/tiny_serial.ino | 37 +++ .../tiny_serial_19200/tiny_serial_19200.ino | 8 + .../tiny_serial_ff/tiny_serial_ff.ino | 8 + .../avr-emulator/tests/software-uart.test.ts | 133 ++++++++ 15 files changed, 896 insertions(+) create mode 100644 packages/avr-emulator/src/io/software-uart.ts create mode 100644 packages/avr-emulator/tests/fixtures/attiny85-tiny_echo.hex create mode 100644 packages/avr-emulator/tests/fixtures/attiny85-tiny_serial.hex create mode 100644 packages/avr-emulator/tests/fixtures/attiny85-tiny_serial_19200.hex create mode 100644 packages/avr-emulator/tests/fixtures/attiny85-tiny_serial_ff.hex create mode 100644 packages/avr-emulator/tests/fixtures/sketches/tiny_echo/tiny_echo.ino create mode 100644 packages/avr-emulator/tests/fixtures/sketches/tiny_serial/tiny_serial.ino create mode 100644 packages/avr-emulator/tests/fixtures/sketches/tiny_serial_19200/tiny_serial_19200.ino create mode 100644 packages/avr-emulator/tests/fixtures/sketches/tiny_serial_ff/tiny_serial_ff.ino create mode 100644 packages/avr-emulator/tests/software-uart.test.ts diff --git a/docs/proposals/browser-emulation-plan.md b/docs/proposals/browser-emulation-plan.md index fce635922..67c78c78c 100644 --- a/docs/proposals/browser-emulation-plan.md +++ b/docs/proposals/browser-emulation-plan.md @@ -1151,6 +1151,12 @@ 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; diff --git a/docs/proposals/browser-emulation-status.md b/docs/proposals/browser-emulation-status.md index defad9c6d..c67443624 100644 --- a/docs/proposals/browser-emulation-status.md +++ b/docs/proposals/browser-emulation-status.md @@ -69,6 +69,7 @@ | 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). Profil noch nicht in `mcuProfile` registriert: das hängt an einer Teständerung (siehe 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 | | 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 cb43c3328..1191a9e14 100644 --- a/packages/avr-emulator/README.md +++ b/packages/avr-emulator/README.md @@ -294,6 +294,36 @@ profile is exported as `ATTINY85`; `mcuProfile("attiny85")` returns it once the board hardware exists (P6.4). Tests: `tests/attiny85-profile.test.ts` with the fixtures `attiny85-tiny_*.hex`. +## 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/index.ts b/packages/avr-emulator/src/index.ts index 2c66cb807..9212240be 100644 --- a/packages/avr-emulator/src/index.ts +++ b/packages/avr-emulator/src/index.ts @@ -22,6 +22,8 @@ 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"; 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/tests/fixtures/README.md b/packages/avr-emulator/tests/fixtures/README.md index d03a1fa67..1fa114dad 100644 --- a/packages/avr-emulator/tests/fixtures/README.md +++ b/packages/avr-emulator/tests/fixtures/README.md @@ -60,3 +60,8 @@ profile tests need no software-UART decoder. Derived expectations: RAMEND (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_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/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_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/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([]); + }); +});