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..442ae01fe --- /dev/null +++ b/packages/avr-emulator/src/mcu/attiny85.ts @@ -0,0 +1,201 @@ +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); + 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..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/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 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..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() {}