summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/index.ts1
-rw-r--r--src/lights/d3aa.ts286
-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
-rw-r--r--src/util.ts24
16 files changed, 1530 insertions, 0 deletions
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..ec2b3e0
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1 @@
+export { D3AA } from './lights/d3aa';
diff --git a/src/lights/d3aa.ts b/src/lights/d3aa.ts
new file mode 100644
index 0000000..40a390b
--- /dev/null
+++ b/src/lights/d3aa.ts
@@ -0,0 +1,286 @@
+// D3AA Simulator
+// Wires the avr8js CPU with AVR-Dx peripherals to simulate the D3AA flashlight.
+
+// AVR32DD20 register addresses and interrupt vector definitions for the D3AA
+// Sourced from arch/dfp/avrdd/include/avr/ioavr32dd20.h
+
+import { CPU } from 'avr8js/cpu/cpu';
+import { avrInstruction } from 'avr8js/cpu/instruction';
+
+import { loadHex } from '../util';
+
+import { AVRDxCCP } from '../peripherals/avrdx-ccp';
+import { AVRDxPort } from '../peripherals/avrdx-port';
+import { AVRDxDAC } from '../peripherals/avrdx-dac';
+import { AVRDxVREF } from '../peripherals/avrdx-vref';
+import { AVRDxADC } from '../peripherals/avrdx-adc';
+import { AVRDxRTCPIT } from '../peripherals/avrdx-rtc-pit';
+import { AVRDxCLKCTRL } from '../peripherals/avrdx-clkctrl';
+import { AVRDxSLPCTRL } from '../peripherals/avrdx-slpctrl';
+import { AVRDxRSTCTRL } from '../peripherals/avrdx-rstctrl';
+import { AVRDxNVMCTRL } from '../peripherals/avrdx-nvmctrl';
+import { AVRDxSIGROW } from '../peripherals/avrdx-sigrow';
+import { AVRDxWDT } from '../peripherals/avrdx-wdt';
+
+// In avr8js, data[0..31] are the CPU general-purpose registers R0-R31.
+// On AVR-Dx, data addresses 0x0000-0x001F are VPORTs (R0-R31 aren't memory-mapped).
+// This offset is added to all hardware data addresses so peripheral hooks
+// and data storage don't collide with the register file.
+export const DATA_MEMORY_OFFSET = 32;
+
+/*
+ * memory layout
+ * - 0x0000 - 0x13FF I/O
+ * - 0x1400 - 0x14FF EEPROM
+ * - 0x1500 - 0x6FFF unmapped(?)
+ * - 0x7000 - 0x7FFF SRAM
+ * - 0x8000 - 0xFFFF FLASH
+ *
+ * in the virtual CPU (cpu.data) everything is shifted by DATA_MEMORY_OFFSET
+ * to accomodate the registers at data[0..31]; this should probably be
+ * refactored, so that registers are not memory mapped by default and
+ * are only hooked into memory for certain CPUs
+ */
+
+export const EEPROM_START = 0x1400 + DATA_MEMORY_OFFSET;
+export const EEPROM_SIZE = 256;
+
+export const SRAM_START = 0x7000 + DATA_MEMORY_OFFSET;
+export const SRAM_SIZE = 0x1000; // 4 KB
+
+export const MAPPED_PROGMEM_START = 0x8000 + DATA_MEMORY_OFFSET;
+export const FLASH_SIZE = 0x8000; // 32 KB
+export const FLASH_WORDS = FLASH_SIZE / 2;
+
+export const CPU_DATA_SIZE = 0x10000 + DATA_MEMORY_OFFSET;
+
+export const CPU_FREQ = 12_000_000; // 12 MHz default clock
+
+// PORTA pins
+const SWITCH_PIN = 4; // PA4 - e-switch
+// const BATT_LVL_PIN = 5; // PA5 - battery voltage divider (AIN25)
+const BST_ENABLE_PIN = 6; // PA6 - boost regulator enable
+const BUTTON_LED_PIN = 7; // PA7 - button LED
+const AUX_BLUE_PIN = 0; // PA0 - aux blue
+const AUX_GREEN_PIN = 2; // PA2 - aux green
+const AUX_RED_PIN = 3; // PA3 - aux red
+
+// PORTD pins
+const IN_NFET_PIN = 4; // PD4 - startup flash prevention
+const HDR_PIN = 5; // PD5 - high/low current range
+// const DAC_PIN = 6; // PD6 - DAC output
+
+// PORTA_DIR_MASK = (1 << 0) | (1 << 2) | (1 << 3) | (1 << 6) | (1 << 7)
+// PORTD_DIR_MASK = (1 << 4) | (1 << 5) | (1 << 6)
+
+export interface D3AAState {
+ level: number; // brightness level 0-150 (estimated from DAC + VREF + HDR)
+ dac: number; // raw 10-bit DAC value (0-1023)
+ vref: number; // VREF index
+ hdr: number; // HDR FET on/off
+ boost: number; // boost enable on/off
+ nfet: number; // IN- NFET on/off
+ auxR: number; // aux red: 0=off, 1=on
+ auxG: number; // aux green: 0=off, 1=on
+ auxB: number; // aux blue: 0=off, 1=on
+ btnLed: number; // button LED: 0=off, 1=on
+ voltage: number; // battery voltage as vbat*50
+ tempC: number; // temperature in Celsius
+ channel: number; // (not directly readable from hardware, default 0)
+ tickCount: number; // PIT tick counter
+ eeprom: Uint8Array; // 256-byte EEPROM contents
+ cycles: number; // CPU cycle counter
+}
+
+export class D3AA {
+ readonly cpu: CPU;
+ readonly program: Uint16Array;
+
+ // Peripherals
+ readonly ccp: AVRDxCCP;
+ readonly portA: AVRDxPort;
+ readonly portC: AVRDxPort;
+ readonly portD: AVRDxPort;
+ readonly dac: AVRDxDAC;
+ readonly vref: AVRDxVREF;
+ readonly adc: AVRDxADC;
+ readonly pit: AVRDxRTCPIT;
+ readonly clkctrl: AVRDxCLKCTRL;
+ readonly slpctrl: AVRDxSLPCTRL;
+ readonly rstctrl: AVRDxRSTCTRL;
+ readonly nvmctrl: AVRDxNVMCTRL;
+ readonly sigrow: AVRDxSIGROW;
+ readonly wdt: AVRDxWDT;
+
+ // Simulation state
+ private _voltage = 200; // 4.0V default (vbat*50)
+ private _tempC = 25; // 25°C default
+
+ constructor() {
+ this.program = new Uint16Array(FLASH_WORDS);
+ const sramBytes = CPU_DATA_SIZE - 0x100; // registerSpace = 0x100
+ this.cpu = new CPU(this.program, sramBytes, { dataMemoryOffset: DATA_MEMORY_OFFSET, ioOffset: 0 });
+
+ // Set SP to end of SRAM (not end of data array)
+ this.cpu.SP = SRAM_START + SRAM_SIZE - 1; // 0x7FFF
+
+ this.ccp = new AVRDxCCP(this.cpu, 0x0034 + DATA_MEMORY_OFFSET);
+ this.rstctrl = new AVRDxRSTCTRL(this.cpu, 0x0040 + DATA_MEMORY_OFFSET, this.ccp);
+ this.slpctrl = new AVRDxSLPCTRL(this.cpu, 0x0050 + DATA_MEMORY_OFFSET);
+ this.clkctrl = new AVRDxCLKCTRL(this.cpu, 0x0060 + DATA_MEMORY_OFFSET, this.ccp);
+ this.vref = new AVRDxVREF(this.cpu, 0x00B0 + DATA_MEMORY_OFFSET);
+ this.wdt = new AVRDxWDT(this.cpu, 0x0100);
+ this.pit = new AVRDxRTCPIT(this.cpu, 0x0150 + DATA_MEMORY_OFFSET, 6, CPU_FREQ);
+ this.portA = new AVRDxPort(this.cpu, 0x0400 + DATA_MEMORY_OFFSET, 0x00 + DATA_MEMORY_OFFSET, 8);
+ this.portC = new AVRDxPort(this.cpu, 0x0440 + DATA_MEMORY_OFFSET, 0x08 + DATA_MEMORY_OFFSET, 29);
+ this.portD = new AVRDxPort(this.cpu, 0x0460 + DATA_MEMORY_OFFSET, 0x0C + DATA_MEMORY_OFFSET, 24);
+ this.adc = new AVRDxADC(this.cpu, 0x0600 + DATA_MEMORY_OFFSET, 26, this.vref);
+ this.dac = new AVRDxDAC(this.cpu, 0x06A0 + DATA_MEMORY_OFFSET);
+ this.nvmctrl = new AVRDxNVMCTRL(this.cpu, 0x1000 + DATA_MEMORY_OFFSET, 0x1400 + DATA_MEMORY_OFFSET, 256, this.ccp);
+ this.sigrow = new AVRDxSIGROW(this.cpu, 0x1104 + DATA_MEMORY_OFFSET);
+
+ // Handle software reset: re-initialize everything
+ this.rstctrl.onReset = () => {
+ this.cpu.reset();
+ this.cpu.SP = SRAM_START + SRAM_SIZE - 1;
+ };
+
+ // Set initial ADC values
+ this.updateADCInputs();
+
+ // Set switch pin high by default (button not pressed; active-low with pull-up)
+ this.portA.setPin(SWITCH_PIN, true);
+ }
+
+ loadProgram(hex: string) {
+ const u8 = new Uint8Array(this.program.buffer);
+ loadHex(hex, u8);
+ this.cpu.data.set(u8, MAPPED_PROGMEM_START);
+ }
+
+ loadEeprom(data: Uint8Array) {
+ this.nvmctrl.loadEeprom(data);
+ }
+
+ getEepromSnapshot(): Uint8Array {
+ return new Uint8Array(this.nvmctrl.eeprom);
+ }
+
+ /** Run the CPU for the given number of cycles */
+ step(cycles: number) {
+ const target = this.cpu.cycles + cycles;
+ while (this.cpu.cycles < target) {
+ const before = this.cpu.cycles;
+ avrInstruction(this.cpu);
+ const mult = this.clkctrl.cycleMultiplier;
+ if (mult > 1) {
+ this.cpu.cycles += (this.cpu.cycles - before) * (mult - 1);
+ }
+ if (this.slpctrl.sleepUntil > this.cpu.cycles) {
+ this.cpu.cycles = Math.min(this.slpctrl.sleepUntil, target);
+ }
+ this.cpu.tick();
+ }
+ }
+
+ /** Simulate button press (e-switch goes low) */
+ buttonPress() {
+ this.portA.setPin(SWITCH_PIN, false);
+ }
+
+ /** Simulate button release (e-switch goes high) */
+ buttonRelease() {
+ this.portA.setPin(SWITCH_PIN, true);
+ }
+
+ /** Set battery voltage (as vbat * 50, e.g., 200 = 4.0V) */
+ setVoltage(vbat50: number) {
+ this._voltage = vbat50;
+ this.updateADCInputs();
+ }
+
+ /** Set temperature in Celsius */
+ setTemperature(tempC: number) {
+ this._tempC = tempC;
+ this.updateADCInputs();
+ }
+
+ /** Get current simulation state for the web UI */
+ getState(): D3AAState {
+ const portAOut = this.portA.outputValue;
+ const portDOut = this.portD.outputValue;
+
+ return {
+ level: this.estimateLevel(),
+ dac: this.dac.value,
+ vref: this.vref.dacRef,
+ hdr: (portDOut >> HDR_PIN) & 1,
+ boost: (portAOut >> BST_ENABLE_PIN) & 1,
+ nfet: (portDOut >> IN_NFET_PIN) & 1,
+ auxR: this.getAuxState(this.portA, AUX_RED_PIN),
+ auxG: this.getAuxState(this.portA, AUX_GREEN_PIN),
+ auxB: this.getAuxState(this.portA, AUX_BLUE_PIN),
+ btnLed: this.getAuxState(this.portA, BUTTON_LED_PIN),
+ voltage: this._voltage,
+ tempC: this._tempC,
+ channel: 0,
+ tickCount: this.pit.tickCount,
+ eeprom: this.nvmctrl.eeprom,
+ cycles: this.cpu.cycles,
+ };
+ }
+
+ /** Detect 3-state aux LED: 0=off, 1=dim(pullup on input), 2=bright(output high) */
+ private getAuxState(port: AVRDxPort, pin: number): number {
+ const mask = 1 << pin;
+ if (port.dirValue & mask) {
+ // Output mode: high = bright, low = off
+ return (port.outputValue & mask) ? 2 : 0;
+ } else {
+ // Input mode: check if pullup is enabled (dim mode)
+ return port.isPullupEnabled(pin) ? 1 : 0;
+ }
+ }
+
+ private updateADCInputs() {
+ // Voltage: compute physical voltage at ADC pin after divider (330kΩ + 100kΩ)
+ const vbat = this._voltage / 50;
+ this.adc.setVoltagePinV(vbat * 100 / 430);
+
+ // Temperature: use SIGROW calibration to compute raw ADC value
+ this.adc.setRawTemperatureResult(this.sigrow.tempCToRawADC(this._tempC));
+ }
+
+ /** Estimate the Anduril ramp level from DAC + VREF + HDR state.
+ * This is approximate - the real mapping is defined by the PWM tables in the firmware. */
+ private estimateLevel(): number {
+ const dacVal = this.dac.value;
+ const portDOut = this.portD.outputValue;
+ const hdr = (portDOut >> HDR_PIN) & 1;
+ const portAOut = this.portA.outputValue;
+ const boost = (portAOut >> BST_ENABLE_PIN) & 1;
+
+ if (!boost || dacVal === 0) return 0;
+
+ const vref = this.vref.dacRefVolts;
+
+ // Approximate level based on gear system:
+ // Gear 1: Vref=1.024, HDR=0, levels 1-30
+ // Gear 2: Vref=2.500, HDR=0, levels 31-40
+ // Gear 3: Vref=1.024, HDR=1, levels 41-119
+ // Gear 4: Vref=2.500, HDR=1, levels 120-150
+ if (!hdr && vref < 2.0) {
+ // Gear 1: DAC 3-954 → levels 1-30
+ return Math.max(1, Math.min(30, Math.round(1 + (dacVal - 3) * 29 / 951)));
+ } else if (!hdr && vref >= 2.0) {
+ // Gear 2: DAC 434-1023 → levels 31-40
+ return Math.max(31, Math.min(40, Math.round(31 + (dacVal - 434) * 9 / 589)));
+ } else if (hdr && vref < 2.0) {
+ // Gear 3: DAC 20-1018 → levels 41-119
+ return Math.max(41, Math.min(119, Math.round(41 + (dacVal - 20) * 78 / 998)));
+ } else {
+ // Gear 4: DAC 430-1023 → levels 120-150
+ return Math.max(120, Math.min(150, Math.round(120 + (dacVal - 430) * 30 / 593)));
+ }
+ }
+}
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;
+ // };
+ }
+}
diff --git a/src/util.ts b/src/util.ts
new file mode 100644
index 0000000..9ef9797
--- /dev/null
+++ b/src/util.ts
@@ -0,0 +1,24 @@
+export class DataFormatError extends Error {};
+
+/*
+ * TODO: remove once node 24 is EOL (2028-05-01?)
+ * node <25 doesn't have Uint8Array.fromHex, so we use Buffer.from(_, "hex") instead
+ */
+type fromHex = (s: string) => Uint8Array | Buffer;
+const fromHex: fromHex = "fromHex" in Uint8Array
+ ? Uint8Array.fromHex as fromHex
+ : (s: string) => Buffer.from(s, "hex");
+
+export function loadHex(source: string, target: Uint8Array) {
+ for (const line of source.split('\n')) {
+ if (line[0] !== ":") continue;
+ if (line.length < 11) throw new DataFormatError("line too short");
+ const data = fromHex(line.slice(1).trimEnd());
+ if (data[3]) return new DataFormatError("unexpected data[3] !== 0");
+ const n = data[0];
+ const addr = (data[1] << 8) | data[2];
+ if (addr + n > target.length) new DataFormatError("target address out of bounds");
+ if (n + 4 !== data.length) new DataFormatError("inconsistent data length");
+ target.set(data.subarray(4), addr);
+ }
+}
If you find this content useful please consider a small donation via bitcoin: bitcoin:1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2?amount=0.001&message=Donation Scraping git contents via cgit is very resource intensive. The continued hosting of these git repositories is entirely financed by your bitcoin contributions. For donations of a least 1 mBTC you include your IP address in the message of your bitcoin donation. This will remove this message in the future for requests from your IP, which will significantly reduce token usage. In any case, any contributions to bitcoin address 1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2 are very welcome. Thanks in advance for you contribution to a self-sustaining ecosystem.