summaryrefslogtreecommitdiff
path: root/src/peripherals
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/peripherals/avrdx-adc.ts188
-rw-r--r--src/peripherals/avrdx-ccp.ts29
-rw-r--r--src/peripherals/avrdx-clkctrl.ts72
-rw-r--r--src/peripherals/avrdx-dac.ts30
-rw-r--r--src/peripherals/avrdx-nvmctrl.ts119
-rw-r--r--src/peripherals/avrdx-port-org.ts228
-rw-r--r--src/peripherals/avrdx-port.ts253
-rw-r--r--src/peripherals/avrdx-rstctrl.ts30
-rw-r--r--src/peripherals/avrdx-rtc-pit.ts122
-rw-r--r--src/peripherals/avrdx-sigrow.ts59
-rw-r--r--src/peripherals/avrdx-slpctrl.ts35
-rw-r--r--src/peripherals/avrdx-vref.ts32
-rw-r--r--src/peripherals/avrdx-wdt.ts22
13 files changed, 1219 insertions, 0 deletions
diff --git a/src/peripherals/avrdx-adc.ts b/src/peripherals/avrdx-adc.ts
new file mode 100644
index 0000000..34a6f24
--- /dev/null
+++ b/src/peripherals/avrdx-adc.ts
@@ -0,0 +1,188 @@
+// AVR-Dx ADC0 peripheral
+// 12-bit ADC with accumulation, free-running mode, and multiple input sources.
+
+import { type CPU, type AVRInterruptConfig } from 'avr8js/cpu/cpu';
+import { type AVRDxVREF } from './avrdx-vref';
+
+const CTRLA = 0x0000;
+const CTRLB = 0x0001;
+const CTRLC = 0x0002;
+// const CTRLD = 0x0003;
+// const CTRLE = 0x0004;
+// const SAMPCTRL = 0x0006;
+const MUXPOS = 0x0008;
+// const MUXNEG = 0x0009;
+const COMMAND = 0x000A;
+// const EVCTRL = 0x000B;
+const INTCTRL = 0x000C;
+const INTFLAGS = 0x000D;
+// const DBGCTRL = 0x000E;
+// const TEMP = 0x000F;
+const RESL = 0x0010;
+const RESH = 0x0011;
+
+// CTRLA bits
+const ADC_ENABLE_bm = 0x01;
+const ADC_FREERUN_bm = 0x02;
+// const ADC_RESSEL_12BIT_gc = 0x00;
+// const ADC_RESSEL_10BIT_gc = 0x04;
+// const ADC_LEFTADJ_bm = 0x10;
+// const ADC_CONVMODE_SINGLEENDED_gc = 0x00;
+// const ADC_RUNSTBY_bm = 0x80;
+
+// CTRLB accumulation
+// const ADC_SAMPNUM_NONE_gc = 0x00;
+// const ADC_SAMPNUM_ACC2_gc = 0x01;
+// const ADC_SAMPNUM_ACC4_gc = 0x02;
+// const ADC_SAMPNUM_ACC8_gc = 0x03;
+// const ADC_SAMPNUM_ACC16_gc = 0x04;
+// const ADC_SAMPNUM_ACC32_gc = 0x05;
+// const ADC_SAMPNUM_ACC64_gc = 0x06;
+
+// ADC0.COMMAND
+const ADC_STCONV_bm = 0x01;
+
+// ADC0.INTCTRL / INTFLAGS
+const ADC_RESRDY_bm = 0x01;
+
+// ADC MUXPOS special values
+// const ADC_MUXPOS_AIN25_gc = 0x19; // PA5 battery voltage divider
+// const ADC_MUXPOS_GND_gc = 0x40; // Ground
+const ADC_MUXPOS_TEMPSENSE_gc = 0x42; // internal temperature sensor
+// const ADC_MUXPOS_VDDDIV10_gc = 0x44; // VDD/10
+// const ADC_MUXPOS_VDDIO2DIV10_gc = 0x45; // VDDIO2/10
+
+// ADC prescaler (CTRLC)
+// const ADC_PRESC_DIV2_gc = 0x00;
+// const ADC_PRESC_DIV4_gc = 0x01;
+// const ADC_PRESC_DIV8_gc = 0x02;
+// const ADC_PRESC_DIV16_gc = 0x03;
+// const ADC_PRESC_DIV32_gc = 0x04;
+// const ADC_PRESC_DIV64_gc = 0x05;
+// const ADC_PRESC_DIV128_gc = 0x06;
+// const ADC_PRESC_DIV256_gc = 0x07;
+
+
+export class AVRDxADC {
+ /** Voltage at the ADC pin in volts (after external voltage divider) */
+ private voltagePinV = 0;
+ /** Temperature: pre-computed raw accumulated ADC result (from SIGROW calibration) */
+ private temperatureInput = 0;
+ private conversionCallback: (() => void) | null = null;
+
+ private readonly resrdyIrq: AVRInterruptConfig;
+
+ constructor(private cpu: CPU, private base: number, resrdyIrqNo: number, private vref: AVRDxVREF) {
+ this.resrdyIrq = {
+ address: resrdyIrqNo * 2, // vector 26, word addr 0x34
+ flagRegister: base + INTFLAGS,
+ flagMask: ADC_RESRDY_bm,
+ enableRegister: base + INTCTRL,
+ enableMask: ADC_RESRDY_bm,
+ } as const;
+
+ // COMMAND register - writing STCONV starts a conversion
+ cpu.writeHooks[base + COMMAND] = (value) => {
+ cpu.data[base + COMMAND] = value;
+ if (value & ADC_STCONV_bm) {
+ this.startConversion();
+ }
+ return true;
+ };
+
+ // INTCTRL
+ cpu.writeHooks[base + INTCTRL] = (value) => {
+ cpu.data[base + INTCTRL] = value;
+ if (value & ADC_RESRDY_bm) {
+ cpu.updateInterruptEnable(this.resrdyIrq, value);
+ }
+ return true;
+ };
+
+ // INTFLAGS - write 1 to clear
+ cpu.writeHooks[base + INTFLAGS] = (value) => {
+ cpu.data[base + INTFLAGS] &= ~value;
+ if (value & ADC_RESRDY_bm) {
+ cpu.clearInterrupt(this.resrdyIrq);
+ }
+ return true;
+ };
+
+ // RES registers - read only (but firmware can read them)
+ cpu.writeHooks[base + RESL] = () => true; // ignore writes
+ cpu.writeHooks[base + RESH] = () => true;
+ }
+
+ /** Set the voltage at the ADC pin (volts, after external divider).
+ * The ADC result is computed at conversion time from this voltage,
+ * the current VREF selection, and the accumulation count. */
+ setVoltagePinV(volts: number) {
+ this.voltagePinV = volts;
+ }
+
+ /** Set the raw ADC result for temperature (computed by runner with SIGROW values) */
+ setRawTemperatureResult(raw16: number) {
+ this.temperatureInput = raw16;
+ }
+
+ private startConversion() {
+ const ctrla = this.cpu.data[this.base + CTRLA];
+ if (!(ctrla & ADC_ENABLE_bm)) return;
+
+ // Compute approximate conversion time
+ // Prescaler from CTRLC
+ const prescDiv = [2, 4, 8, 16, 32, 64, 128, 256][this.cpu.data[this.base + CTRLC] & 0x07];
+ // Number of accumulated samples
+ const sampNum = this.cpu.data[this.base + CTRLB] & 0x07;
+ const numSamples = sampNum === 0 ? 1 : (1 << sampNum); // 1, 2, 4, 8, 16, 32, 64
+ // Each conversion ~13 ADC clock cycles (plus init delay for first)
+ const adcCycles = 15 * numSamples;
+ const cpuCycles = adcCycles * prescDiv;
+
+ // Schedule completion
+ if (this.conversionCallback) {
+ this.cpu.clearClockEvent(this.conversionCallback);
+ }
+ // TODO: do ADC CPU cycles depend on clock scaling?
+ this.conversionCallback = this.cpu.addClockEvent(() => this.completeConversion(), cpuCycles);
+ }
+
+ private completeConversion() {
+ this.conversionCallback = null;
+
+ const muxpos = this.cpu.data[this.base + MUXPOS];
+ let result: number;
+
+ if (muxpos === ADC_MUXPOS_TEMPSENSE_gc) {
+ // Temperature: use pre-computed accumulated result from SIGROW calibration
+ result = this.temperatureInput;
+ } else {
+ // External pin (voltage divider on AIN25, etc.):
+ // Compute ADC result from physical pin voltage, current VREF, and accumulation
+ const vref = this.vref.adcRefVolts;
+ const sampNum = this.cpu.data[this.base + CTRLB] & 0x07;
+ const numSamples = sampNum === 0 ? 1 : (1 << sampNum);
+ const single = Math.min(4095, Math.max(0, Math.round(this.voltagePinV / vref * 4096)));
+ result = single * numSamples;
+ }
+
+ // Clamp to 16-bit
+ result = Math.max(0, Math.min(0xFFFF, Math.round(result)));
+
+ // Write result
+ this.cpu.data[this.base + RESL] = result & 0xFF;
+ this.cpu.data[this.base + RESH] = (result >> 8) & 0xFF;
+
+ // Clear STCONV
+ this.cpu.data[this.base + COMMAND] &= ~ADC_STCONV_bm;
+
+ // Set RESRDY flag and fire interrupt
+ this.cpu.setInterruptFlag(this.resrdyIrq);
+
+ // Free-running: start another conversion
+ const ctrla = this.cpu.data[this.base + CTRLA];
+ if (ctrla & ADC_FREERUN_bm) {
+ this.startConversion();
+ }
+ }
+}
diff --git a/src/peripherals/avrdx-ccp.ts b/src/peripherals/avrdx-ccp.ts
new file mode 100644
index 0000000..a4f0370
--- /dev/null
+++ b/src/peripherals/avrdx-ccp.ts
@@ -0,0 +1,29 @@
+// CCP - Configuration Change Protection
+// When 0xD8 (IOREG) or 0x9D (SPM) is written to CCP, a 4-cycle window opens
+// during which protected registers can be written.
+
+import type { CPU } from 'avr8js/cpu/cpu';
+
+const ADDR = 0;
+
+const SPM = 0x9D;
+const IOREG = 0xD8;
+
+export class AVRDxCCP {
+ private unlockedUntil = -Infinity;
+
+ constructor(private cpu: CPU, base: number) {
+ cpu.writeHooks[base + ADDR] = (value) => {
+ if (value === IOREG || value === SPM) {
+ // TODO: adjust for cycle scaling(?)
+ this.unlockedUntil = cpu.cycles + 4;
+ }
+ cpu.data[base + ADDR] = value;
+ return true;
+ };
+ }
+
+ isUnlocked(): boolean {
+ return this.cpu.cycles <= this.unlockedUntil;
+ }
+}
diff --git a/src/peripherals/avrdx-clkctrl.ts b/src/peripherals/avrdx-clkctrl.ts
new file mode 100644
index 0000000..91d081b
--- /dev/null
+++ b/src/peripherals/avrdx-clkctrl.ts
@@ -0,0 +1,72 @@
+// AVR-Dx CLKCTRL peripheral
+// Clock controller with CCP-protected writes.
+
+import { type CPU } from 'avr8js/cpu/cpu';
+import { type AVRDxCCP } from './avrdx-ccp';
+
+const MCLKCTRLA = 0x00;
+const MCLKCTRLB = 0x01;
+const MCLKSTATUS = 0x02;
+const OSCHFCTRLA = 0x08;
+
+const PEN_bm = 0x01;
+const PDIV_gm = 0x1E;
+const SOSC_bm = 0x01; // in MCLKSTATUS
+
+export class AVRDxCLKCTRL {
+ /** How many base clocks per instruction clock (1 = no prescaling, 4 = div4, etc.) */
+ cycleMultiplier = 1;
+
+ // PDIV field -> divisor lookup (AVR-Dx datasheet Table 11-1)
+ private static readonly PDIV_DIVISORS: Record<number, number> = {
+ 0: 2, 1: 4, 2: 8, 3: 16, 4: 32, 5: 64,
+ 8: 6, 9: 10, 10: 12, 11: 24, 12: 48,
+ };
+
+ constructor(private cpu: CPU, private base: number, private ccp: AVRDxCCP) {
+ // MCLKCTRLA - CCP protected
+ cpu.writeHooks[base + MCLKCTRLA] = (value) => {
+ if (this.ccp.isUnlocked()) {
+ cpu.data[base + MCLKCTRLA] = value;
+ }
+ return true;
+ };
+
+ // MCLKCTRLB - CCP protected (prescaler)
+ cpu.writeHooks[base + MCLKCTRLB] = (value) => {
+ if (this.ccp.isUnlocked()) {
+ cpu.data[base + MCLKCTRLB] = value;
+ this.updatePrescaler();
+ // Clear SOSC (System Oscillator Changing) flag immediately
+ // (firmware busy-waits on this)
+ cpu.data[base + MCLKSTATUS] &= ~SOSC_bm;
+ }
+ return true;
+ };
+
+ // MCLKSTATUS - read-only
+ cpu.readHooks[base + MCLKSTATUS] = () => {
+ // Always report stable (SOSC = 0)
+ return cpu.data[base + MCLKSTATUS] & ~SOSC_bm;
+ };
+ cpu.writeHooks[base + MCLKSTATUS] = () => true; // ignore writes
+
+ // OSCHFCTRLA - CCP protected
+ cpu.writeHooks[base + OSCHFCTRLA] = (value) => {
+ if (this.ccp.isUnlocked()) {
+ cpu.data[base + OSCHFCTRLA] = value;
+ }
+ return true;
+ };
+ }
+
+ private updatePrescaler() {
+ const val = this.cpu.data[this.base + MCLKCTRLB];
+ if (!(val & PEN_bm)) {
+ this.cycleMultiplier = 1;
+ } else {
+ const pdiv = (val & PDIV_gm) >> 1;
+ this.cycleMultiplier = AVRDxCLKCTRL.PDIV_DIVISORS[pdiv] ?? 2;
+ }
+ }
+}
diff --git a/src/peripherals/avrdx-dac.ts b/src/peripherals/avrdx-dac.ts
new file mode 100644
index 0000000..d4843ca
--- /dev/null
+++ b/src/peripherals/avrdx-dac.ts
@@ -0,0 +1,30 @@
+// AVR-Dx DAC0 peripheral
+// 10-bit DAC with output enable, used for LED brightness control on the D3AA
+
+import type { CPU } from 'avr8js/cpu/cpu';
+
+const CTRLA = 0x00;
+// const DATA = 0x02; // 16-bit (DATAL + DATAH)
+const DATAL = 0x02;
+const DATAH = 0x03;
+
+const ENABLE_bm = 0x01;
+const OUTEN_bm = 0x40;
+
+export class AVRDxDAC {
+ constructor(private cpu: CPU, private base: number) {
+ }
+
+ /** Whether DAC is enabled and output is enabled */
+ get enabled(): boolean {
+ const ctrla = this.cpu.data[this.base + CTRLA];
+ return !!(ctrla & ENABLE_bm) && !!(ctrla & OUTEN_bm);
+ }
+
+ /** Get the current 10-bit DAC value (0-1023).
+ * DAC0.DATA is left-aligned: the 10-bit value sits in bits [15:6]. */
+ get value(): number {
+ const raw16 = this.cpu.data[this.base + DATAL] | (this.cpu.data[this.base + DATAH] << 8);
+ return raw16 >> 6;
+ }
+}
diff --git a/src/peripherals/avrdx-nvmctrl.ts b/src/peripherals/avrdx-nvmctrl.ts
new file mode 100644
index 0000000..2321023
--- /dev/null
+++ b/src/peripherals/avrdx-nvmctrl.ts
@@ -0,0 +1,119 @@
+// AVR-Dx NVMCTRL + Mapped EEPROM
+// EEPROM is memory-mapped at 0x1400-0x14FF and accessed via NVMCTRL commands.
+
+import type { CPU } from 'avr8js/cpu/cpu';
+import type { AVRDxCCP } from './avrdx-ccp';
+
+const CTRLA = 0x0000;
+// const CTRLB = 0x0001;
+// const STATUS = 0x0002;
+// const INTCTRL = 0x0003;
+// const INTFLAGS = 0x0004;
+// const DATAL = 0x0006;
+// const DATAH = 0x0007;
+// const ADDR0 = 0x0008;
+// const ADDR1 = 0x0009;
+// const ADDR2 = 0x000A;
+// const ADDR3 = 0x000B;
+
+// CMD values
+const CMD_NONE_gc = 0x00;
+const CMD_NOOP_gc = 0x01;
+// const CMD_FLWR_gc = 0x02;
+// const CMD_FLPER_gc = 0x08;
+// const CMD_FLMPER2_gc = 0x09;
+// const CMD_FLMPER4_gc = 0x0A;
+// const CMD_FLMPER8_gc = 0x0B;
+// const CMD_FLMPER16_gc = 0x0C;
+// const CMD_FLMPER32_gc = 0x0D;
+const CMD_EEWR_gc = 0x12;
+const CMD_EEERWR_gc = 0x13;
+const CMD_EEBER_gc = 0x18;
+// const CMD_EEMBER2_gc = 0x19;
+// const CMD_EEMBER4_gc = 0x1A;
+// const CMD_EEMBER8_gc = 0x1B;
+// const CMD_EEMBER16_gc = 0x1C;
+// const CMD_EEMBER32_gc = 0x1D;
+
+export class AVRDxNVMCTRL {
+ readonly eeprom: Uint8Array;
+ /** Page buffer for EEPROM writes (tracks which bytes have been written) */
+ private pageBuffer: Uint8Array;
+ private pageBufferDirty: Uint8Array;
+
+ constructor(cpu: CPU, base: number, start: number, private size: number, private ccp: AVRDxCCP, init: undefined | Uint8Array = undefined) {
+ this.eeprom = new Uint8Array(size);
+ this.pageBuffer = new Uint8Array(size);
+ this.pageBufferDirty = new Uint8Array(size);
+
+ this.eeprom.fill(0xFF);
+ if (init) this.loadEeprom(init);
+
+ // CTRLA - CCP protected, executes NVM commands
+ cpu.writeHooks[base + CTRLA] = (value: number) => {
+ if (this.ccp.isUnlocked()) {
+ cpu.data[base + CTRLA] = value;
+ this.executeCommand(value);
+ }
+ return true;
+ };
+
+ // Mapped EEPROM read hooks (0x1400-0x14FF)
+ for (let i = 0; i < size; i++) {
+ cpu.readHooks[start + i] = () => this.eeprom[i];
+
+ // Writes to mapped EEPROM go to the page buffer
+ cpu.writeHooks[start + i] = (value: number) => {
+ this.pageBuffer[i] = value;
+ this.pageBufferDirty[i] = 1;
+
+ // Check the current active command
+ const cmd = cpu.data[base + CTRLA];
+
+ if (cmd === CMD_EEERWR_gc) {
+ // Erase+write: replace byte directly
+ this.eeprom[i] = value;
+ this.pageBufferDirty[i] = 0;
+ } else if (cmd === CMD_EEWR_gc) {
+ // Write-only: AND with existing data (can only clear bits)
+ this.eeprom[i] &= value;
+ this.pageBufferDirty[i] = 0;
+ }
+ return true;
+ };
+ }
+ }
+
+ loadEeprom(data: Uint8Array) {
+ this.eeprom.set(data.subarray(0, this.size));
+ }
+
+ private executeCommand(cmd: number) {
+ switch (cmd) {
+ case CMD_NONE_gc:
+ case CMD_NOOP_gc:
+ this.pageBufferDirty.fill(0);
+ break;
+ case CMD_EEWR_gc:
+ // Write-only mode: subsequent mapped EEPROM writes AND with existing data.
+ // Actual writes happen in the mapped EEPROM write hooks.
+ // Don't clear command — it stays active until NONE/NOOP.
+ break;
+ case CMD_EEERWR_gc:
+ // Erase+write mode: subsequent mapped EEPROM writes replace data directly.
+ // Actual writes happen in the mapped EEPROM write hooks.
+ // Don't clear command — it stays active until NONE/NOOP.
+ break;
+ case CMD_EEBER_gc:
+ // Erase EEPROM page (the page containing the address in ADDR)
+ // For simplicity, erase the whole EEPROM
+ // TODO: figure out page size
+ this.eeprom.fill(0xFF);
+ this.pageBufferDirty.fill(0);
+ break;
+ default:
+ // TODO: implement other commands (if needed)
+ break;
+ }
+ }
+}
diff --git a/src/peripherals/avrdx-port-org.ts b/src/peripherals/avrdx-port-org.ts
new file mode 100644
index 0000000..538384e
--- /dev/null
+++ b/src/peripherals/avrdx-port-org.ts
@@ -0,0 +1,228 @@
+// AVR-Dx VPORT + PORT GPIO peripheral
+// Implements the dual VPORT (fast, 4 regs) + PORT (full, ~24 regs) model.
+
+import { CPU, AVRInterruptConfig } from 'avr8js/dist/esm/cpu/cpu';
+import {
+ VPORT_DIR, VPORT_OUT, VPORT_IN, VPORT_INTFLAGS,
+ PORT_DIR, PORT_DIRSET, PORT_DIRCLR, PORT_DIRTGL,
+ PORT_OUT, PORT_OUTSET, PORT_OUTCLR, PORT_OUTTGL,
+ PORT_IN, PORT_INTFLAGS, PORT_PIN0CTRL,
+ PORT_ISC_gm, PORT_ISC_INTDISABLE_gc, PORT_ISC_BOTHEDGES_gc,
+ PORT_ISC_RISING_gc, PORT_ISC_FALLING_gc, PORT_ISC_LEVEL_gc,
+} from '../d3aa-config';
+
+export interface AVRDxPortConfig {
+ vportBase: number;
+ portBase: number;
+ interrupt: AVRInterruptConfig;
+}
+
+export type PortListener = (dir: number, out: number) => void;
+
+export class AVRDxPort {
+ private pinState = 0x00; // external input state
+ private listeners: PortListener[] = [];
+
+ constructor(
+ private cpu: CPU,
+ private config: AVRDxPortConfig,
+ ) {
+ const { vportBase: vb, portBase: pb } = config;
+
+ cpu.writeHooks[vb + VPORT_DIR] = (value) => {
+ cpu.data[vb + VPORT_DIR] = value;
+ cpu.data[pb + PORT_DIR] = value;
+ return true;
+ };
+
+ cpu.writeHooks[vb + VPORT_OUT] = (value) => {
+ this.setOutput(value);
+ return true;
+ };
+
+ cpu.readHooks[vb + VPORT_IN] = () => {
+ return this.computeInputValue();
+ };
+
+ // VPORT.INTFLAGS - write 1 to clear
+ cpu.writeHooks[vb + VPORT_INTFLAGS] = (value) => {
+ cpu.data[vb + VPORT_INTFLAGS] &= ~value;
+ cpu.data[pb + PORT_INTFLAGS] &= ~value;
+ // If all flags cleared, clear the interrupt
+ if (cpu.data[vb + VPORT_INTFLAGS] === 0) {
+ cpu.clearInterrupt(config.interrupt);
+ }
+ return true;
+ };
+
+ cpu.writeHooks[pb + PORT_DIR] = (value) => {
+ cpu.data[pb + PORT_DIR] = value;
+ cpu.data[vb + VPORT_DIR] = value;
+ return true;
+ };
+
+ // PORT.DIRSET - write 1 to set bits in DIR
+ cpu.writeHooks[pb + PORT_DIRSET] = (value) => {
+ const newDir = cpu.data[pb + PORT_DIR] | value;
+ cpu.data[pb + PORT_DIR] = newDir;
+ cpu.data[vb + VPORT_DIR] = newDir;
+ return true;
+ };
+
+ // PORT.DIRCLR - write 1 to clear bits in DIR
+ cpu.writeHooks[pb + PORT_DIRCLR] = (value) => {
+ const newDir = cpu.data[pb + PORT_DIR] & ~value;
+ cpu.data[pb + PORT_DIR] = newDir;
+ cpu.data[vb + VPORT_DIR] = newDir;
+ return true;
+ };
+
+ // PORT.DIRTGL - write 1 to toggle bits in DIR
+ cpu.writeHooks[pb + PORT_DIRTGL] = (value) => {
+ const newDir = cpu.data[pb + PORT_DIR] ^ value;
+ cpu.data[pb + PORT_DIR] = newDir;
+ cpu.data[vb + VPORT_DIR] = newDir;
+ return true;
+ };
+
+ // PORT.OUT
+ cpu.writeHooks[pb + PORT_OUT] = (value) => {
+ this.setOutput(value);
+ return true;
+ };
+
+ // PORT.OUTSET
+ cpu.writeHooks[pb + PORT_OUTSET] = (value) => {
+ this.setOutput(cpu.data[pb + PORT_OUT] | value);
+ return true;
+ };
+
+ // PORT.OUTCLR
+ cpu.writeHooks[pb + PORT_OUTCLR] = (value) => {
+ this.setOutput(cpu.data[pb + PORT_OUT] & ~value);
+ return true;
+ };
+
+ // PORT.OUTTGL
+ cpu.writeHooks[pb + PORT_OUTTGL] = (value) => {
+ this.setOutput(cpu.data[pb + PORT_OUT] ^ value);
+ return true;
+ };
+
+ // PORT.IN - read returns pin state
+ cpu.readHooks[pb + PORT_IN] = () => {
+ return this.computeInputValue();
+ };
+
+ // PORT.INTFLAGS - write 1 to clear (same as VPORT)
+ cpu.writeHooks[pb + PORT_INTFLAGS] = (value) => {
+ cpu.data[pb + PORT_INTFLAGS] &= ~value;
+ cpu.data[vb + VPORT_INTFLAGS] &= ~value;
+ if (cpu.data[vb + VPORT_INTFLAGS] === 0) {
+ cpu.clearInterrupt(config.interrupt);
+ }
+ return true;
+ };
+
+ // PINnCTRL registers (0x10-0x17)
+ for (let pin = 0; pin < 8; pin++) {
+ cpu.writeHooks[pb + PORT_PIN0CTRL + pin] = (value) => {
+ cpu.data[pb + PORT_PIN0CTRL + pin] = value;
+ return true;
+ };
+ }
+ }
+
+ /** Set an external pin input value */
+ setPin(pin: number, high: boolean) {
+ const oldInput = this.computeInputValue();
+ if (high) {
+ this.pinState |= (1 << pin);
+ } else {
+ this.pinState &= ~(1 << pin);
+ }
+ const newInput = this.computeInputValue();
+ this.checkInterrupts(oldInput, newInput);
+ }
+
+ /** Register a listener for output changes */
+ addListener(listener: PortListener) {
+ this.listeners.push(listener);
+ }
+
+ /** Get the current output register value */
+ get outputValue(): number {
+ return this.cpu.data[this.config.portBase + PORT_OUT];
+ }
+
+ /** Get the current direction register value */
+ get dirValue(): number {
+ return this.cpu.data[this.config.portBase + PORT_DIR];
+ }
+
+ private setOutput(value: number) {
+ const { vportBase: vb, portBase: pb } = this.config;
+ const oldOut = this.cpu.data[pb + PORT_OUT];
+ this.cpu.data[pb + PORT_OUT] = value;
+ this.cpu.data[vb + VPORT_OUT] = value;
+ if (oldOut !== value) {
+ const dir = this.cpu.data[pb + PORT_DIR];
+ for (const listener of this.listeners) {
+ listener(dir, value);
+ }
+ }
+ }
+
+ /** Compute the IN register value: output pins reflect OUT, input pins reflect external state */
+ private computeInputValue(): number {
+ const { portBase: pb } = this.config;
+ const dir = this.cpu.data[pb + PORT_DIR];
+ const out = this.cpu.data[pb + PORT_OUT];
+ // Output pins read back OUT value; input pins read external pin state
+ return (dir & out) | (~dir & this.pinState);
+ }
+
+ private checkInterrupts(oldIn: number, newIn: number) {
+ const { portBase: pb, vportBase: vb } = this.config;
+ const changed = oldIn ^ newIn;
+ if (!changed) return;
+
+ let intFlags = 0;
+ for (let pin = 0; pin < 8; pin++) {
+ if (!(changed & (1 << pin))) continue;
+ const isc = this.cpu.data[pb + PORT_PIN0CTRL + pin] & PORT_ISC_gm;
+ const wasHigh = !!(oldIn & (1 << pin));
+ const isHigh = !!(newIn & (1 << pin));
+
+ let fire = false;
+ switch (isc) {
+ case PORT_ISC_INTDISABLE_gc:
+ break;
+ case PORT_ISC_BOTHEDGES_gc:
+ fire = true;
+ break;
+ case PORT_ISC_RISING_gc:
+ fire = !wasHigh && isHigh;
+ break;
+ case PORT_ISC_FALLING_gc:
+ fire = wasHigh && !isHigh;
+ break;
+ case PORT_ISC_LEVEL_gc:
+ fire = !isHigh; // low level
+ break;
+ }
+ if (fire) {
+ intFlags |= (1 << pin);
+ }
+ }
+
+ if (intFlags) {
+ this.cpu.data[vb + VPORT_INTFLAGS] |= intFlags;
+ this.cpu.data[pb + PORT_INTFLAGS] |= intFlags;
+ // Directly queue the interrupt (bypassing the enable check, since
+ // AVR-Dx port interrupts are enabled per-pin via PINnCTRL ISC bits,
+ // not via a centralized enable register)
+ this.cpu.queueInterrupt(this.config.interrupt);
+ }
+ }
+}
diff --git a/src/peripherals/avrdx-port.ts b/src/peripherals/avrdx-port.ts
new file mode 100644
index 0000000..74ab8ae
--- /dev/null
+++ b/src/peripherals/avrdx-port.ts
@@ -0,0 +1,253 @@
+// AVR-Dx VPORT + PORT GPIO peripheral
+// Implements the dual VPORT (fast, 4 regs) + PORT (full, ~24 regs) model.
+
+import type { CPU, AVRInterruptConfig } from 'avr8js/cpu/cpu';
+
+// VPORT register offsets
+const VPORT_DIR = 0x00;
+const VPORT_OUT = 0x01;
+const VPORT_IN = 0x02;
+const VPORT_INTFLAGS = 0x03;
+
+// PORT register offsets from base
+const DIR = 0x00;
+const DIRSET = 0x01;
+const DIRCLR = 0x02;
+const DIRTGL = 0x03;
+const OUT = 0x04;
+const OUTSET = 0x05;
+const OUTCLR = 0x06;
+const OUTTGL = 0x07;
+const IN = 0x08;
+const INTFLAGS = 0x09;
+// const PORTCTRL = 0x0A;
+const PIN0CTRL = 0x10;
+// PIN1CTRL = 0x11, ..., PIN7CTRL = 0x17
+
+// PINnCTRL bits
+const PULLUPEN_bm = 0x08;
+const ISC_gm = 0x07;
+const ISC_INTDISABLE_gc = 0x00;
+const ISC_BOTHEDGES_gc = 0x01;
+const ISC_RISING_gc = 0x02;
+const ISC_FALLING_gc = 0x03;
+// const ISC_INPUT_DISABLE_gc = 0x04;
+const ISC_LEVEL_gc = 0x05;
+
+export type PortListener = (dir: number, out: number) => void;
+
+export class AVRDxPort {
+ private pinState = 0x00; // external input state
+ private listeners: PortListener[];
+ private irq: AVRInterruptConfig;
+
+ constructor(
+ private cpu: CPU,
+ private base: number,
+ private vbase: number,
+ irqNo: number,
+ ) {
+ this.listeners = [];
+
+ this.irq = {
+ address: irqNo * 2,
+ flagRegister: vbase + VPORT_INTFLAGS,
+ flagMask: 0xFF,
+ enableRegister: base + PIN0CTRL, // dummy; we manage enable ourselves
+ enableMask: 0,
+ constant: true, // don't auto-clear; firmware clears flags manually
+ };
+
+ cpu.writeHooks[vbase + VPORT_DIR] = (value) => {
+ cpu.data[vbase + VPORT_DIR] = value;
+ cpu.data[base + DIR] = value;
+ return true;
+ };
+
+ cpu.writeHooks[vbase + VPORT_OUT] = (value) => {
+ this.setOutput(value);
+ return true;
+ };
+
+ // VPORT.IN - read returns pin state
+ cpu.readHooks[vbase + VPORT_IN] = () => {
+ return this.computeInputValue();
+ };
+
+ // VPORT.INTFLAGS - write 1 to clear
+ cpu.writeHooks[vbase + VPORT_INTFLAGS] = (value) => {
+ cpu.data[vbase + VPORT_INTFLAGS] &= ~value;
+ cpu.data[base + INTFLAGS] &= ~value;
+ // If all flags cleared, clear the interrupt
+ if (cpu.data[vbase + VPORT_INTFLAGS] === 0) {
+ cpu.clearInterrupt(this.irq);
+ }
+ return true;
+ };
+
+ // PORT.DIR
+ cpu.writeHooks[base + DIR] = (value) => {
+ cpu.data[base + DIR] = value;
+ cpu.data[vbase + VPORT_DIR] = value;
+ return true;
+ };
+
+ // PORT.DIRSET - write 1 to set bits in DIR
+ cpu.writeHooks[base + DIRSET] = (value) => {
+ const newDir = cpu.data[base + DIR] | value;
+ cpu.data[base + DIR] = newDir;
+ cpu.data[vbase + VPORT_DIR] = newDir;
+ return true;
+ };
+
+ // PORT.DIRCLR - write 1 to clear bits in DIR
+ cpu.writeHooks[base + DIRCLR] = (value) => {
+ const newDir = cpu.data[base + DIR] & ~value;
+ cpu.data[base + DIR] = newDir;
+ cpu.data[vbase + VPORT_DIR] = newDir;
+ return true;
+ };
+
+ // PORT.DIRTGL - write 1 to toggle bits in DIR
+ cpu.writeHooks[base + DIRTGL] = (value) => {
+ const newDir = cpu.data[base + DIR] ^ value;
+ cpu.data[base + DIR] = newDir;
+ cpu.data[vbase + VPORT_DIR] = newDir;
+ return true;
+ };
+
+ cpu.writeHooks[base + OUT] = (value) => {
+ this.setOutput(value);
+ return true;
+ };
+
+ cpu.writeHooks[base + OUTSET] = (value) => {
+ this.setOutput(cpu.data[base + OUT] | value);
+ return true;
+ };
+
+ cpu.writeHooks[base + OUTCLR] = (value) => {
+ this.setOutput(cpu.data[base + OUT] & ~value);
+ return true;
+ };
+
+ cpu.writeHooks[base + OUTTGL] = (value) => {
+ this.setOutput(cpu.data[base + OUT] ^ value);
+ return true;
+ };
+
+ cpu.readHooks[base + IN] = () => {
+ return this.computeInputValue();
+ };
+
+ // PORT.INTFLAGS - write 1 to clear (same as VPORT)
+ cpu.writeHooks[base + INTFLAGS] = (value) => {
+ cpu.data[base + INTFLAGS] &= ~value;
+ cpu.data[vbase + VPORT_INTFLAGS] &= ~value;
+ if (cpu.data[vbase + VPORT_INTFLAGS] === 0) {
+ cpu.clearInterrupt(this.irq);
+ }
+ return true;
+ };
+
+ // PINnCTRL registers (0x10-0x17)
+ for (let pin = 0; pin < 8; pin++) {
+ cpu.writeHooks[base + PIN0CTRL + pin] = (value) => {
+ cpu.data[base + PIN0CTRL + pin] = value;
+ return true;
+ };
+ }
+ }
+
+ /** Set an external pin input value */
+ setPin(pin: number, high: boolean) {
+ const oldInput = this.computeInputValue();
+ if (high) {
+ this.pinState |= (1 << pin);
+ } else {
+ this.pinState &= ~(1 << pin);
+ }
+ const newInput = this.computeInputValue();
+ this.checkInterrupts(oldInput, newInput);
+ }
+
+ /** Register a listener for output changes */
+ addListener(listener: PortListener) {
+ this.listeners.push(listener);
+ }
+
+ /** Get the current output register value */
+ get outputValue(): number {
+ return this.cpu.data[this.base + OUT];
+ }
+
+ /** Get the current direction register value */
+ get dirValue(): number {
+ return this.cpu.data[this.base + DIR];
+ }
+
+ isPullupEnabled(pin: number): boolean {
+ if (pin < 0 || pin > 7) return false;
+ return !!(this.cpu.data[this.base + PIN0CTRL + pin] & PULLUPEN_bm);
+ }
+
+ private setOutput(value: number) {
+ const oldOut = this.cpu.data[this.base + OUT];
+ this.cpu.data[this.base + OUT] = value;
+ this.cpu.data[this.vbase + VPORT_OUT] = value;
+ if (oldOut !== value) {
+ const dir = this.cpu.data[this.base + DIR];
+ for (const listener of this.listeners) {
+ listener(dir, value);
+ }
+ }
+ }
+
+ /** Compute the IN register value: output pins reflect OUT, input pins reflect external state */
+ private computeInputValue(): number {
+ const dir = this.cpu.data[this.base + DIR];
+ const out = this.cpu.data[this.base + OUT];
+ // Output pins read back OUT value; input pins read external pin state
+ return (dir & out) | (~dir & this.pinState);
+ }
+
+ private checkInterrupts(oldIn: number, newIn: number) {
+ const changed = oldIn ^ newIn;
+ if (!changed) return;
+
+ let intFlags = 0;
+ for (let pin = 0; pin < 8; pin++) {
+ if (!(changed & (1 << pin))) continue;
+ const isc = this.cpu.data[this.base + PIN0CTRL + pin] & ISC_gm;
+ const wasHigh = !!(oldIn & (1 << pin));
+ const isHigh = !!(newIn & (1 << pin));
+
+ let fire = false;
+ switch (isc) {
+ case ISC_INTDISABLE_gc:
+ break;
+ case ISC_BOTHEDGES_gc:
+ fire = true;
+ break;
+ case ISC_RISING_gc:
+ fire = !wasHigh && isHigh;
+ break;
+ case ISC_FALLING_gc:
+ fire = wasHigh && !isHigh;
+ break;
+ case ISC_LEVEL_gc:
+ fire = !isHigh; // low level
+ break;
+ }
+ if (fire) {
+ intFlags |= (1 << pin);
+ }
+ }
+
+ if (intFlags) {
+ this.cpu.data[this.vbase + VPORT_INTFLAGS] |= intFlags;
+ this.cpu.data[this.base + INTFLAGS] |= intFlags;
+ this.cpu.setInterruptFlag(this.irq);
+ }
+ }
+}
diff --git a/src/peripherals/avrdx-rstctrl.ts b/src/peripherals/avrdx-rstctrl.ts
new file mode 100644
index 0000000..370b9c3
--- /dev/null
+++ b/src/peripherals/avrdx-rstctrl.ts
@@ -0,0 +1,30 @@
+// AVR-Dx RSTCTRL - Reset Controller
+// Handles software reset and reset flags.
+
+import { type CPU } from 'avr8js/cpu/cpu';
+import { type AVRDxCCP } from './avrdx-ccp';
+
+export const RSTFR = 0;
+export const SWRR = 1;
+export const SWRST_bm = 0x01;
+
+export class AVRDxRSTCTRL {
+ /** Set this callback to handle software resets */
+ onReset: (() => void) | null = null;
+
+ constructor(cpu: CPU, base: number, ccp: AVRDxCCP) {
+ // RSTFR - reset flags, write 1 to clear
+ cpu.writeHooks[base + RSTFR] = (value) => {
+ cpu.data[base + RSTFR] &= ~value;
+ return true;
+ };
+
+ // SWRR - software reset register (CCP protected)
+ cpu.writeHooks[base + SWRR] = (value) => {
+ if (ccp.isUnlocked() && (value & SWRST_bm)) {
+ if (this.onReset) this.onReset();
+ }
+ return true;
+ };
+ }
+}
diff --git a/src/peripherals/avrdx-rtc-pit.ts b/src/peripherals/avrdx-rtc-pit.ts
new file mode 100644
index 0000000..101f265
--- /dev/null
+++ b/src/peripherals/avrdx-rtc-pit.ts
@@ -0,0 +1,122 @@
+// AVR-Dx RTC Periodic Interrupt Timer (PIT)
+// Generates periodic interrupts from the 32768 Hz internal ULP oscillator.
+
+import type { CPU, AVRInterruptConfig } from 'avr8js/cpu/cpu';
+
+const FREQ_HZ = 32768;
+
+const CTRLA = 0x0;
+const STATUS = 0x1;
+const INTCTRL = 0x2;
+const INTFLAGS = 0x3;
+
+const PI_bm = 0x01;
+const PITEN_bm = 0x01;
+const PERIOD_gm = 0x78; // bits [6:3]
+
+// PIT period to number of 32768Hz clock cycles
+// PERIOD field is bits [6:3] of CTRLA
+export const PERIOD_CYCLES = [
+ 0, // 0x00: OFF
+ 4, // 0x01: CYC4
+ 8, // 0x02: CYC8
+ 16, // 0x03: CYC16
+ 32, // 0x04: CYC32
+ 64, // 0x05: CYC64
+ 128, // 0x06: CYC128
+ 256, // 0x07: CYC256
+ 512, // 0x08: CYC512
+ 1024, // 0x09: CYC1024
+ 2048, // 0x0A: CYC2048
+ 4096, // 0x0B: CYC4096
+ 8192, // 0x0C: CYC8192
+ 16384, // 0x0D: CYC16384
+ 32768, // 0x0E: CYC32768
+ 0,
+] as const;
+
+export class AVRDxRTCPIT {
+ private pitCallback: (() => void) | null = null;
+ private irq: AVRInterruptConfig;
+ tickCount = 0;
+
+ constructor(private cpu: CPU, private base: number, irqNo: number, private cpuFreqHz: number) {
+ this.irq = {
+ address: irqNo * 2,
+ flagRegister: base + INTFLAGS,
+ flagMask: PI_bm,
+ enableRegister: base + INTCTRL,
+ enableMask: PI_bm,
+ }
+
+ // CTRLA - period select + enable
+ cpu.writeHooks[base + CTRLA] = (value) => {
+ cpu.data[base + CTRLA] = value;
+ this.reconfigure();
+ return true;
+ };
+
+ // STATUS - read-only busy flag (always report not busy for simplicity)
+ // TODO: when should this report busy? do we need this?
+ cpu.readHooks[base + STATUS] = () => 0;
+ cpu.writeHooks[base + STATUS] = () => true; // ignore writes
+
+ // INTCTRL - enable interrupt
+ cpu.writeHooks[base + INTCTRL] = (value) => {
+ cpu.data[base + INTCTRL] = value;
+ if (value & PI_bm) {
+ cpu.updateInterruptEnable(this.irq, value);
+ }
+ return true;
+ };
+
+ // INTFLAGS - write 1 to clear
+ cpu.writeHooks[base + INTFLAGS] = (value) => {
+ if (value & PI_bm) {
+ cpu.data[base + INTFLAGS] &= ~PI_bm;
+ cpu.clearInterrupt(this.irq);
+ }
+ return true;
+ };
+ }
+
+ private get cycles() {
+ const ctrla = this.cpu.data[this.base + CTRLA];
+ if (!(ctrla & PITEN_bm)) return;
+ return PERIOD_CYCLES[(ctrla & PERIOD_gm) >> 3] || undefined;
+ }
+
+ // effective tick frequency (Hz), null if disabled
+ get frequency() {
+ const c = this.cycles;
+ return c ? FREQ_HZ / this.cycles : null;
+ }
+
+ private reconfigure() {
+ if (this.pitCallback) {
+ this.cpu.clearClockEvent(this.pitCallback);
+ this.pitCallback = null;
+ }
+
+ const cycles = this.cycles;
+ if (!cycles) return;
+
+ this.scheduleTick(Math.round(cycles * this.cpuFreqHz / FREQ_HZ));
+ }
+
+ private scheduleTick(cycles: number) {
+ this.pitCallback = this.cpu.addClockEvent(() => this.onTick(cycles), cycles);
+ }
+
+ private onTick(cycles: number) {
+ this.tickCount++;
+ this.pitCallback = null;
+
+ this.cpu.setInterruptFlag(this.irq);
+
+ // Re-schedule if still enabled
+ if (this.cpu.data[this.base + CTRLA] & PITEN_bm) {
+ this.scheduleTick(cycles);
+ }
+ }
+}
diff --git a/src/peripherals/avrdx-sigrow.ts b/src/peripherals/avrdx-sigrow.ts
new file mode 100644
index 0000000..ddd266a
--- /dev/null
+++ b/src/peripherals/avrdx-sigrow.ts
@@ -0,0 +1,59 @@
+// AVR-Dx SIGROW - Signature Row
+// Read-only factory calibration data. Pre-loaded with values that produce
+// correct temperature readings with the D3AA firmware's conversion formula.
+
+import { CPU } from 'avr8js/cpu/cpu';
+
+// Typical AVR32DD20 calibration values.
+// The firmware formula (from arch/avr32dd20.c mcu_temp_raw2cooked):
+// temp = (sigrow_offset << 4) - measurement
+// temp *= sigrow_slope
+// temp += 65536 / 8
+// temp >>= 10
+// result is Kelvin << 6
+//
+// IMPORTANT: On AVR, the (sigrow_offset << 4) shift is done in 16-bit
+// arithmetic (int is 16-bit on AVR), so offset MUST be ≤ 0x0FFF (12-bit)
+// or the shift overflows. The datasheet specifies TEMPSENSE1 as a 12-bit value.
+//
+// With slope=0x036F (879), offset=0x0800 (2048):
+// offset << 4 = 32768 (fits in 16 bits)
+// At 25°C: ADC_acc16 ≈ 10548, result = 19081 → 23°C (±2°C rounding)
+// At 80°C: ADC_acc16 ≈ 6447, result = 22601 → 78°C
+// Range -40°C..125°C: ADC values 3092..15394, all in valid 16-bit range
+const DEFAULT_TEMPSENSE0 = 0x036F; // slope
+const DEFAULT_TEMPSENSE1 = 0x0800; // offset (12-bit)
+
+const TEMPSENSE0 = 0;
+const TEMPSENSE1 = 2;
+
+export class AVRDxSIGROW {
+ constructor(cpu: CPU, base: number, readonly slope = DEFAULT_TEMPSENSE0, readonly offset = DEFAULT_TEMPSENSE1) {
+ // TEMPSENSE0 (16-bit at 0x1104-0x1105)
+ cpu.readHooks[base + TEMPSENSE0] = () => slope & 0xFF;
+ cpu.readHooks[base + TEMPSENSE0 + 1] = () => (slope >> 8) & 0xFF;
+
+ // TEMPSENSE1 (16-bit at 0x1106-0x1107)
+ cpu.readHooks[base + TEMPSENSE1] = () => offset & 0xFF;
+ cpu.readHooks[base + TEMPSENSE1 + 1] = () => (offset >> 8) & 0xFF;
+
+ // Write hooks to prevent accidental writes
+ cpu.writeHooks[base + TEMPSENSE0] = () => true;
+ cpu.writeHooks[base + TEMPSENSE0 + 1] = () => true;
+ cpu.writeHooks[base + TEMPSENSE1] = () => true;
+ cpu.writeHooks[base + TEMPSENSE1 + 1] = () => true;
+ }
+
+ /** Compute the raw ADC result (16-bit accumulated) for a given temperature in Celsius */
+ tempCToRawADC(tempC: number): number {
+ const tempK = tempC + 273.15;
+ const kelvin6 = Math.round(tempK * 64);
+ // Reverse the firmware formula:
+ // kelvin6 = ((offset << 4) - measurement) * slope + 8192) >> 10
+ // kelvin6 << 10 = (offset << 4 - measurement) * slope + 8192
+ // (kelvin6 << 10) - 8192 = (offset << 4 - measurement) * slope
+ // measurement = (offset << 4) - ((kelvin6 << 10) - 8192) / slope
+ const measurement = (this.offset << 4) - ((kelvin6 << 10) - 8192) / this.slope;
+ return Math.max(0, Math.min(0xFFFF, Math.round(measurement)));
+ }
+}
diff --git a/src/peripherals/avrdx-slpctrl.ts b/src/peripherals/avrdx-slpctrl.ts
new file mode 100644
index 0000000..93cae58
--- /dev/null
+++ b/src/peripherals/avrdx-slpctrl.ts
@@ -0,0 +1,35 @@
+// AVR-Dx SLPCTRL - Sleep Controller
+// Handles the SLEEP instruction by fast-forwarding to the next clock event.
+
+import type { CPU } from 'avr8js/cpu/cpu';
+
+const CTRLA = 0;
+const SEN_bm = 0x01;
+// const SMODE_gm = 0x06;
+
+export class AVRDxSLPCTRL {
+ sleepUntil: number = 0;
+
+ constructor(private cpu: CPU, private base: number) {
+ // CTRLA - sleep mode + sleep enable
+ cpu.writeHooks[base + CTRLA] = (value) => {
+ cpu.data[base + CTRLA] = value;
+ return true;
+ };
+
+ // Hook the SLEEP instruction
+ cpu.onSleep = () => {
+ this.handleSleep();
+ };
+ }
+
+ private handleSleep() {
+ const ctrla = this.cpu.data[this.base + CTRLA];
+ if (!(ctrla & SEN_bm)) return; // sleep not enabled
+
+ const nextEvent = (this.cpu as any).nextClockEvent;
+ if (nextEvent) {
+ this.sleepUntil = Math.min(this.sleepUntil, nextEvent.cycles);
+ }
+ }
+}
diff --git a/src/peripherals/avrdx-vref.ts b/src/peripherals/avrdx-vref.ts
new file mode 100644
index 0000000..0097096
--- /dev/null
+++ b/src/peripherals/avrdx-vref.ts
@@ -0,0 +1,32 @@
+// AVR-Dx VREF peripheral
+// Voltage reference selection for DAC0 and ADC0
+
+import { CPU } from 'avr8js/cpu/cpu';
+
+const ADC0REF = 0;
+const DAC0REF = 2;
+
+const VREF_VOLTAGES = [1.024, 2.048, 2.500, 4.096] as const;
+
+export class AVRDxVREF {
+ constructor(private cpu: CPU, private base: number) {
+ }
+
+ /** Get DAC Vref selection (raw register value) */
+ get dacRef(): number {
+ return this.cpu.data[this.base + DAC0REF] & 0x07;
+ }
+
+ get dacRefVolts(): number {
+ return VREF_VOLTAGES[this.dacRef] ?? 0;
+ }
+
+ /** Get ADC Vref selection (raw register value) */
+ get adcRef(): number {
+ return this.cpu.data[this.base + ADC0REF] & 0x07;
+ }
+
+ get adcRefVolts(): number {
+ return VREF_VOLTAGES[this.adcRef] ?? 0;
+ }
+}
diff --git a/src/peripherals/avrdx-wdt.ts b/src/peripherals/avrdx-wdt.ts
new file mode 100644
index 0000000..08cd308
--- /dev/null
+++ b/src/peripherals/avrdx-wdt.ts
@@ -0,0 +1,22 @@
+// AVR-Dx SLPCTRL - Sleep Controller
+// Handles the SLEEP instruction by fast-forwarding to the next clock event.
+
+import { type CPU } from 'avr8js/cpu/cpu';
+
+// const CTRLA = 0;
+// const STATUS = 1;
+
+export class AVRDxWDT {
+ constructor(cpu: CPU, base: number) {
+ // firmware writes 0 to disable WDT. We need a write hook so it doesn't crash.
+ // cpu.writeHooks[base + CTRLA] = (value) => {
+ // this.cpu.data[base + CTRLA] = value;
+ // return true;
+ // };
+
+ // cpu.writeHooks[base + STATUS] = (value) => {
+ // this.cpu.data[base + STATUS] = value;
+ // return true;
+ // };
+ }
+}