Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/proposals/browser-emulation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/proposals/browser-emulation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<n>` = `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 |

Expand Down
15 changes: 6 additions & 9 deletions docs/proposals/report-m3-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<n>` 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 |
Expand Down Expand Up @@ -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<n>` = `0x80 \| n` wie ATTinyCore (#336) |
| LED „L“ und Analog-Beschriftungen der Boardansicht waren fest auf Pin 13 bzw. `A<k>`-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<k>`-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
Expand Down
9 changes: 5 additions & 4 deletions packages/avr-emulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
87 changes: 86 additions & 1 deletion packages/avr-emulator/src/mcu/attiny85.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AVRIOPort["timerOverridePin"]>[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,
Expand Down Expand Up @@ -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), {
Expand Down
65 changes: 63 additions & 2 deletions packages/avr-emulator/tests/attiny85-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
Expand Down
Loading