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
6 changes: 6 additions & 0 deletions docs/proposals/browser-emulation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/proposals/browser-emulation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 27 additions & 0 deletions packages/avr-emulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
1 change: 1 addition & 0 deletions packages/avr-emulator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
201 changes: 201 additions & 0 deletions packages/avr-emulator/src/mcu/attiny85.ts
Original file line number Diff line number Diff line change
@@ -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<CPU["updateInterruptEnable"]>[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,
};
},
};
2 changes: 1 addition & 1 deletion packages/avr-emulator/src/mcu/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<McuId, McuProfile>> = { atmega328p: ATMEGA328P, atmega2560: ATMEGA2560 };

export function mcuProfile(id: McuId): McuProfile {
Expand Down
5 changes: 3 additions & 2 deletions packages/avr-emulator/src/mcu/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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";
import type { McuId } from "./mcus";

export interface McuPeripherals {
readonly ports: ReadonlyMap<PortName, AVRIOPort>;
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;
Expand Down
Loading
Loading