diff --git a/docs/proposals/browser-emulation-plan.md b/docs/proposals/browser-emulation-plan.md index bc0973393..fc8a4a580 100644 --- a/docs/proposals/browser-emulation-plan.md +++ b/docs/proposals/browser-emulation-plan.md @@ -1140,8 +1140,8 @@ 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. +OC1x und zählt ohne CTC1 phasenkorrekt statt als Sägezahn (das Profil korrigiert +beides, 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 1479764e8..457fa0859 100644 --- a/docs/proposals/browser-emulation-status.md +++ b/docs/proposals/browser-emulation-status.md @@ -68,11 +68,11 @@ | P7.1 | erledigt bis auf die Messung am Referenz-Host (wartet auf Mensch) | #332 | Compile-Phase in `npm run capacity:calibrate` (vier Schritte, Tabelle im Bericht, `--skip-compile`), Compile-Container tragen im Testprofil das Label des Kapazitätslaufs; CAPACITY_MODEL „Compile capacity (browser emulation)“: Modell, Kenngrößen nach 4.12, Messung auf M2 Pro (30 gleichzeitige neue Compiles p95 6,3 s, ≈ 275 Compiles/min, unverändertes Beispiel 8 ms); Messung auf der Referenz-VM: [human-tests.md, Teil 3](human-tests.md) | | P2.3 | erledigt | #333 | ATTinyCore 1.5.2 gepinnt (Version, HTTPS-Archiv, SHA-256), Installation ohne Board-Manager ins Sketchbook mit `platform.local.txt` gegen die Post-Build-Hooks (Fund: sie schreiben in Core und Sketch; HEX identisch mit und ohne), Prüfskript und Canary-Test; Compile-Sandbox-Image mit ATTinyCore und vorkompiliertem Core je FQBN; CI (Toolchain- und E2E-Job, Cache-Schlüssel, Deployment-Prüfschritt); Toolchain-Gate `toolchain-cores.test.ts` (alle vier Boards); INSTALL_LOCAL, README (Lizenzen LGPL 2.1). Abweichung: kein Board-Manager, weil `drazzy.com` und `azduino.com` abgelaufene Zertifikate liefern | | P6.1 | erledigt | #333 (mit P2.3) | ATTinyCore im Compile-Sandbox-Image; Vertragstest kompiliert für den ATtiny85 in der Sandbox | -| P6.2 | erledigt | #334 | `mcu/attiny85.ts`: Port B mit INT0 (PB2) und PCINT0, Timer 0 mit eigenen Registern und Ein-Wort-Vektoren, `ATtinyTimer1`, ADC mit 4 Kanälen und REFS2 an Bit 4 (avr8js-Fehler), EEPROM 512 B mit Schreibzeit für 8 MHz, Watchdog, Taktteiler, kein USART; RAMEND 0x25F. Tests `attiny85-profile.test.ts` (13) mit drei ATTinyCore-Fixtures (Ergebnisse im EEPROM). Funde: GIMSK/GIFR werden von INT0 und PCINT geteilt, avr8js verdrahtet nur je einen (behoben); `ATtinyTimer1` erzeugt in der PWM-Konfiguration von ATTinyCore kein OC1x-Signal (offen, Register stimmen); Spike-Spanne für `delay(587)` war zu eng (585–589 ms abgeleitet). Registriert mit P6.4. Upstream-Meldungen an avr8js: wartet auf Mensch | +| P6.2 | erledigt | #334, #341 | `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 und zählt ohne CTC1 phasenkorrekt (behoben in #341: Signal aus dem Zählerstand, Sägezahn nach Datenblatt); Spike-Spanne für `delay(587)` war zu eng (585–589 ms abgeleitet). Registriert mit P6.4. Upstream-Meldungen an avr8js: wartet auf Mensch | | P6.3 | erledigt (Modell) | #335 | `io/software-uart.ts`: TX-Decoder an PB0 (nur Ausgang, nicht OC0A) mit Baudraten-Erkennung (größte Standardrate, 90 % der Abstände ganzzahlige Bitvielfache) und gemessener Bitbreite, Rückfall auf die eingestellte Rate bei Ruhe, Startbit-Prüfung in Bitmitte; RX über ein Komparatormodell (ACSR, Bandlücke/AIN0 gegen PB1, Vektor 7), Empfänger aktiv mit `ACIE`; Schnittstelle wie `AVRUSART` für `SerialPorts`. Tests `software-uart.test.ts` (7: 9600/19200, UTF-8, Rückfall, Echo, gesperrte Eingabe, synthetische Flanken). Anbindung an Serial-Schicht und Serial-Monitor-Baudrate mit P6.4, Benutzerhandbuch mit P6.5 | | P6.4 | erledigt | #336 | `ATTINY85_BOARD` (Pins 0–5 = PB0–PB5, A0–A3 als Zweitnamen von 5/2/4/3, PWM 0/1/4, RESET PB5, `LED_BUILTIN` 1, `Serial` als Software-Port 0/1), Profil und Hardware registriert; `SerialPorts` mit USART oder `SoftwareUart` (`setFallbackBaud`); PWM-Tastverhältnis für Timer 1 des ATtiny85 (`pwm.ts`, `pwmChannelRegisters`); Shared: `analogName`, Parser rechnen `A` = `0x80 \| n` und `analogRead(n)` als Kanal wie ATTinyCore; Regeln RESET-Pin und Serial-Pins; Pinbelegung im Toolchain-Gate gegen `variants/tinyX5` (Konstanten über den Compiler ausgewertet). Freigegebene Teständerungen (Nutzer, 2026-10-23): fünf Tests nutzen für „Board ohne Hardware“ eine unbekannte ID statt `attiny85`. Board bleibt `planned` bis P6.5 | | P6.5 | erledigt | #337 | ATtiny85 `preview`: DIP-8-Ansicht (`npm run generate:attiny85-svg`), LED „L“ und Analog-Beschriftungen nach `LED_BUILTIN` bzw. Sketch-Namen des Boards, Analogdialog „2 (A1)“; Startbefehl mit `serialBaud` aus `Serial.begin()` (SSOT SimulationRuntime); 5 Korpus-Proben (`derived`); `e2e/attiny85.spec.ts` (3); Benutzerhandbuch „Board wählen“ mit Halbduplex, RESET-Pin, Serial-Pins; Funde: Baudraten-Erkennung rastete auf 19200 statt 9600 ein (jetzt Rahmenprüfung über alle passenden Raten), Byte 0x00 aus `digitalWrite(0)` ohne `Serial` (TX nur bei aktivem `Serial`). Freigegebene Teständerungen (Nutzer, 2026-10-23): sieben Tests auf Status `preview` des ATtiny85 umgestellt | -| M3 | erledigt (mit ausgewiesenen Ausnahmen) | #337 | ATtiny85 als `preview` freigegeben, Proben und E2E grün, 11 × Echtzeit; **keine Aufnahme an echter ATtiny85-Hardware** (alle Fälle `derived`), Timer-1-PWM-Signal in avr8js offen, Meldungen an avr8js warten auf Mensch; siehe [report-m3-acceptance.md](report-m3-acceptance.md) | +| M3 | erledigt (mit ausgewiesenen Ausnahmen) | #337 | ATtiny85 als `preview` freigegeben, Proben und E2E grün, 11 × Echtzeit; **keine Aufnahme an echter ATtiny85-Hardware** (alle Fälle `derived`), Timer-1-PWM-Signal im Profil korrigiert (#341), Meldungen an avr8js warten auf Mensch; siehe [report-m3-acceptance.md](report-m3-acceptance.md) | | (Rest P4.4) Mobile-Layout je Board | erledigt | #338 | Geprüft mit Screenshots aller vier Boards bei 390 × 844: die schwebende Panel-Navigation überdeckte unten rechts Pins (Uno A0–A5, ATtiny85 PB0–PB2). Die mobile Board-Ansicht hält jetzt unten 20rem für die Navigation frei (`index.css`), das Board steht darüber; Uno, Nano, Mega und ATtiny85 sind frei bedienbar | | Anleitung | bereit | #290 | [human-tests.md](human-tests.md): P0.3 mit `npm run spike:p03` (Messseite für Browser auf Geräten), P1.14-Aufnahmen mit `npm run record:hardware`, P7.1 Compile-Kapazität auf dem Referenz-Host | diff --git a/docs/proposals/report-m3-acceptance.md b/docs/proposals/report-m3-acceptance.md index fd71aa244..11192dbec 100644 --- a/docs/proposals/report-m3-acceptance.md +++ b/docs/proposals/report-m3-acceptance.md @@ -18,9 +18,9 @@ alle ATtiny-Fälle sind `derived`. |---|---|---| | P6.1 Toolchain im Compile-Sandbox-Image | erfüllt | #333: ATTinyCore 1.5.2 mit gepinnter SHA-256 im Image, Core je FQBN vorkompiliert; Vertragstest kompiliert für den ATtiny85 in der Sandbox; Toolchain-Gate `toolchain-cores.test.ts` für alle vier Boards | | P6.2 MCU-Profil | erfüllt | #334: Port B mit INT0/PCINT0, Timer 0 und 1, ADC mit REFS2 an Bit 4, EEPROM, Watchdog; `attiny85-profile.test.ts` (13 Fälle) | -| P6.3 Software-UART | erfüllt | #335 und dieser PR: TX-Dekodierung mit Baudraten-Erkennung, RX über das Komparatormodell; `software-uart.test.ts` (8 Fälle) | +| P6.3 Software-UART | erfüllt | #335 und #337: TX-Dekodierung mit Baudraten-Erkennung, RX über das Komparatormodell; `software-uart.test.ts` (8 Fälle) | | P6.4 Board-Hardware und Regeln | erfüllt | #336: `ATTINY85_BOARD`, Serial-Port der Art `software`, PWM-Beobachtung Timer 1, Parser rechnen `A` wie ATTinyCore, Warnungen RESET-Pin und Serial-Pins; Pinbelegung im Toolchain-Gate gegen `variants/tinyX5` | -| P6.5 Freigabe `preview` | erfüllt | dieser PR: `shared/boards.ts` `attiny85` `preview`, Laufzeit `browser`; Boardansicht DIP-8 (`npm run generate:attiny85-svg`); Benutzerhandbuch „Board wählen“ mit den Grenzen des Boards | +| P6.5 Freigabe `preview` | erfüllt | #337: `shared/boards.ts` `attiny85` `preview`, Laufzeit `browser`; Boardansicht DIP-8 (`npm run generate:attiny85-svg`); Benutzerhandbuch „Board wählen“ mit den Grenzen des Boards | | Proben grün | erfüllt | 5 Korpus-Proben (`attiny_basics`, `attiny_adc_read`, `attiny_pwm_duty`, `attiny_serial_echo`, `attiny_interrupts`), dazu die Paket-Tests von Profil, UART und Board | | E2E grün | erfüllt | `e2e/attiny85.spec.ts`, 3 Szenarien (unten), 3 von 3 in 8,7 s; ganze Suite 54 von 54 | | Bundle | erfüllt | `npm run check:bundle`: initialer Ladepfad 691,0 kB von 710 kB (die Boardansicht lädt seit #326 mit eigenem Chunk); avr8js nur im Worker-Chunk | @@ -66,18 +66,15 @@ ist Arbeit für einen Menschen, eine Anleitung dafür fehlt noch. | avr8js liest REFS2 an Bit 3 | eigener Getter im Profil (#334, aus Spike P0.5) | | avr8js verdrahtet die gemeinsamen Register GIMSK/GIFR nur für INT0 bzw. PCINT | Hooks im Profil umhüllt (#334) | | `analogRead(A1)` wurde im Parser zu Pin 4 (Pin- und Kanalnummer überlagern sich) | Parser rechnen `A` = `0x80 \| n` wie ATTinyCore (#336) | -| LED „L“ und Analog-Beschriftungen der Boardansicht waren fest auf Pin 13 bzw. `A`-Labels | `LED_BUILTIN` und Sketch-Namen des Boards (dieser PR) | -| Baudraten-Erkennung rastete auf ein Vielfaches ein (19200 statt 9600) | Entscheidung erst, wenn jede passende Rate genug Rahmen hat; die Rate ohne Rahmenfehler gewinnt (dieser PR, Probe `attiny_basics`) | -| `digitalWrite(0, LOW)` ohne `Serial` erzeugte ein Byte 0x00 | TX wird nur dekodiert, solange `Serial` aktiv ist (dieser PR, Probe `attiny_pwm_duty`) | +| LED „L“ und Analog-Beschriftungen der Boardansicht waren fest auf Pin 13 bzw. `A`-Labels | `LED_BUILTIN` und Sketch-Namen des Boards (#337) | +| Baudraten-Erkennung rastete auf ein Vielfaches ein (19200 statt 9600) | Entscheidung erst, wenn jede passende Rate genug Rahmen hat; die Rate ohne Rahmenfehler gewinnt (#337, Probe `attiny_basics`) | +| `digitalWrite(0, LOW)` ohne `Serial` erzeugte ein Byte 0x00 | TX wird nur dekodiert, solange `Serial` aktiv ist (#337, Probe `attiny_pwm_duty`) | | Spanne für `delay(587)` aus dem Spike (586–588 ms) zu eng | abgeleitet 585–589 ms (#334) | +| avr8js erzeugt in der PWM-Konfiguration von ATTinyCore kein Signal an OC1A/OC1B (Timer 1) und zählt ohne CTC1 phasenkorrekt | Profil treibt OC1A/OC1B aus dem Zählerstand und zählt als Sägezahn wie das Datenblatt (#341; vier Tests, gleiche Geschwindigkeit) | ## Bekannte Restpunkte (nicht M3-blockierend) - **Keine Aufnahme an echter ATtiny85-Hardware** (alle Fälle `derived`). -- **Timer-1-PWM-Signal:** avr8js 0.21.1 erzeugt in der PWM-Konfiguration von - ATTinyCore kein Signal an OC1A/OC1B. Das Tastverhältnis in UnoSim kommt aus - den Registern und stimmt; ein Sketch, der den Pegel des Pins liest, sieht LOW - (README des Pakets). - **Meldungen an avr8js** (REFS2, GIMSK/GIFR, Timer-1-PWM): nach außen gerichtet, wartet auf einen Menschen. - **Halbduplex** des Software-UART ist Eigenschaft des Boards und im diff --git a/packages/avr-emulator/README.md b/packages/avr-emulator/README.md index 04702053d..c14d1083f 100644 --- a/packages/avr-emulator/README.md +++ b/packages/avr-emulator/README.md @@ -249,12 +249,13 @@ at least 90 % line coverage for `src/`. 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 +- **Timer 1 PWM on the ATtiny85:** `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. + chip the counter runs from 0 to OCR1C either way, OC1x is set at 0 and + cleared at OCR1x (COM1x = 01 or 10; inverted with 11). The profile drives + OC1A/OC1B from the counter and replaces the phase-correct count by that + sawtooth. Not modelled: the inverted outputs on PB0/PB3 of COM1x = 01. ## ATmega2560 profile diff --git a/packages/avr-emulator/src/mcu/attiny85.ts b/packages/avr-emulator/src/mcu/attiny85.ts index 121d0a884..1fbc0abb6 100644 --- a/packages/avr-emulator/src/mcu/attiny85.ts +++ b/packages/avr-emulator/src/mcu/attiny85.ts @@ -85,6 +85,91 @@ function shareInterruptRegisters(cpu: CPU): void { }; } +/** The members of avr8js' `ATtinyTimer1` (private in 0.21.1, pinned) that `correctTimer1Pwm` replaces or reads. */ +interface ATtinyTimer1Internals { + readonly TOP: number; + readonly ocrA: number; + readonly ocrB: number; + readonly comA: number; + readonly comB: number; + readonly tcntUpdated: boolean; + readonly OVF: InterruptConfig; + readonly OCFA: InterruptConfig; + readonly OCFB: InterruptConfig; + phasePwmCount(value: number, delta: number): number; + timerUpdated(value: number, prevValue: number): void; +} + +const PWM1A = 0x40; // TCCR1 +const PWM1B = 0x40; // GTCCR +const OC1A_PIN = 1; // PB1 +const OC1B_PIN = 4; // PB4 +/** `PinOverrideMode.Set` and `.Clear` of avr8js; the package does not export the enum. */ +type PinOverrideMode = Parameters[1]; +const OVERRIDE_SET = 2 as PinOverrideMode; +const OVERRIDE_CLEAR = 3 as PinOverrideMode; + +/** + * In PWM mode timer 1 of the ATtiny85 counts up from 0 to OCR1C and starts at 0 again, + * with or without CTC1 (datasheet, "Timer/Counter1 in PWM Mode"; ATTinyCore sets OCR1C = 255 because "this module + * doesn't have a 8bit PWM mode"). OC1x is high from 0 until the match with OCR1x + * (COM1x = 01 or 10) and inverted with COM1x = 11. avr8js drives no waveform with CTC1 set, + * as ATTinyCore configures `analogWrite`, and counts phase-correct without it. The + * correction drives OC1A (PB1) and OC1B (PB4) from the counter after every count with CTC1 + * (avr8js counts that sawtooth right), and replaces the phase-correct count by the sawtooth + * with its flags. Not modelled: the inverted outputs on PB0 and PB3 of COM1x = 01. + * Members are replaced as plain properties: an accessor on the instance slows the whole + * timer down in V8 (ATtiny85 emulation 11 × → 8 × real time). + */ +function correctTimer1Pwm(cpu: CPU, timer: ATtinyTimer1): void { + const internals = timer as unknown as ATtinyTimer1Internals; + const tccr1 = attinyTimer1Config.TCCR1; + const gtccr = attinyTimer1Config.GTCCR; + const port = () => cpu.gpioByPort[attinyTimer1Config.compPortB]; + + const output = (pin: number, com: number, ocr: number, prev: number, count: number) => { + if (com === 0 || (count !== 0 && (prev < ocr) === (count < ocr))) return; + const high = (count < ocr) === (com !== 3); + port()?.timerOverridePin(pin, high ? OVERRIDE_SET : OVERRIDE_CLEAR); + }; + /** OC1A and OC1B for a step of the counter from `prev` to `count`. */ + const outputs = (prev: number, count: number) => { + if (cpu.data[tccr1] & PWM1A) output(OC1A_PIN, internals.comA, internals.ocrA, prev, count); + if (cpu.data[gtccr] & PWM1B) output(OC1B_PIN, internals.comB, internals.ocrB, prev, count); + }; + + const timerUpdated = internals.timerUpdated.bind(timer); + internals.timerUpdated = (value, prevValue) => { + timerUpdated(value, prevValue); + outputs(prevValue, value); + }; + + /** Flags of the sawtooth for the counter value just reached (avr8js sets them only on the CTC path). */ + const flags = (count: number) => { + if (count === 0) cpu.setInterruptFlag(internals.OVF); + if (count === internals.ocrA) cpu.setInterruptFlag(internals.OCFA); + if (count === internals.ocrB) cpu.setInterruptFlag(internals.OCFB); + }; + internals.phasePwmCount = (value, delta) => { + const top = internals.TOP; + let count = value; + for (let step = 0; step < delta; step++) { + const prev = count; + count = count >= top ? 0 : count + 1; + if (internals.tcntUpdated) continue; + flags(count); + outputs(prev, count); + } + return count & 0xff; + }; +} + +function createTimer1(cpu: CPU): ATtinyTimer1 { + const timer = new ATtinyTimer1(cpu, attinyTimer1Config); + correctTimer1Pwm(cpu, timer); + return timer; +} + /** Timer 0: own registers and vectors; OC0A on PB0, OC0B on PB1, T0 on PB2. */ const timer0: AVRTimerConfig = { ...timer0Config, @@ -190,7 +275,7 @@ export const ATTINY85: McuProfile = { shareInterruptRegisters(cpu); return { ports: new Map([["B", port]]), - timers: [new AVRTimer(cpu, timer0), new ATtinyTimer1(cpu, attinyTimer1Config)], + timers: [new AVRTimer(cpu, timer0), createTimer1(cpu)], usarts: [], adc: new Attiny85ADC(cpu, adc), eeprom: new AVREEPROM(cpu, new ArrayEepromBackend(eeprom), { diff --git a/packages/avr-emulator/tests/attiny85-profile.test.ts b/packages/avr-emulator/tests/attiny85-profile.test.ts index 20d2f9bc0..8e47ba0ba 100644 --- a/packages/avr-emulator/tests/attiny85-profile.test.ts +++ b/packages/avr-emulator/tests/attiny85-profile.test.ts @@ -112,8 +112,10 @@ describe("ATtiny85 profile (plan P6.2)", () => { 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 the timer 1 waveform on PB4: high from 0 to OCR1B, period OCR1C + 1 (avr8js alone: none)", () => { + expect(highShare(probe, 4)).toBeCloseTo(200 / 256, 1); }); it("drives fast PWM on PB0 with timer 0", () => { @@ -169,6 +171,65 @@ describe("ATtiny85 profile (plan P6.2)", () => { }); }); + describe("timer 1 PWM (datasheet: Timer/Counter1 in PWM Mode; avr8js counts phase-correct and drives no pin with CTC1)", () => { + const TCCR1 = 0x50; + const GTCCR = 0x4c; + const OCR1A = 0x4e; + const OCR1B = 0x4b; + const OCR1C = 0x4d; + const TIFR = 0x58; + const DDRB = 0x37; + + /** Level of `pin` after each of the next `cycles` CPU cycles, as 0/1 string, timer 1 at prescaler 1. */ + function levels({ cpu, peripherals }: Tiny, pin: number, cycles: number): string { + const port = peripherals.ports.get("B"); + let out = ""; + for (let i = 0; i < cycles; i++) { + cpu.cycles++; + cpu.tick(); + out += port?.pinState(pin) === PinState.High ? "1" : "0"; + } + return out; + } + + function pwmTiny(setup: (cpu: CPU) => void): Tiny { + const probe = tiny(); + probe.cpu.writeData(DDRB, (1 << 1) | (1 << 4)); + probe.cpu.writeData(OCR1C, 9); // period 10 counts + setup(probe.cpu); + levels(probe, 1, 25); // settle into the first periods + return probe; + } + + it("sets OC1A at 0 and clears it on OCR1A, period OCR1C + 1, with CTC1 as ATTinyCore sets it", () => { + const probe = pwmTiny((cpu) => { + cpu.writeData(OCR1A, 3); + cpu.writeData(TCCR1, 0x80 | 0x40 | 0x20 | 0x01); // CTC1, PWM1A, COM1A = 10, CK/1 + }); + const wave = levels(probe, 1, 40); + expect(wave.match(/1+/g)?.slice(0, 3).map((run) => run.length)).toEqual([3, 3, 3]); + expect(wave.match(/0+/g)?.slice(1, 3).map((run) => run.length)).toEqual([7, 7]); + }); + + it("counts the same sawtooth without CTC1 (not phase-correct)", () => { + const probe = pwmTiny((cpu) => { + cpu.writeData(OCR1A, 3); + cpu.writeData(TCCR1, 0x40 | 0x20 | 0x01); // PWM1A, COM1A = 10, CK/1 + }); + expect(levels(probe, 1, 40).match(/1+/g)?.slice(0, 3).map((run) => run.length)).toEqual([3, 3, 3]); + }); + + it("inverts OC1B with COM1B = 11 and sets TOV1 at the wrap to 0", () => { + const probe = pwmTiny((cpu) => { + cpu.writeData(OCR1B, 3); + cpu.writeData(GTCCR, 0x40 | 0x30); // PWM1B, COM1B = 11 + cpu.writeData(TCCR1, 0x01); // CK/1 + }); + expect(levels(probe, 4, 40).match(/1+/g)?.slice(1, 3).map((run) => run.length)).toEqual([7, 7]); + expect(probe.cpu.data[TIFR] & (1 << 2)).toBeTruthy(); // TOV1 + }); + }); + it("resets on a 15 ms watchdog timeout and reports it in MCUSR", () => { const probe = tiny("tiny_watchdog"); run(probe, 100);