From 1b194ac4578dea8e71b0d61d1cb4d875f435ba71 Mon Sep 17 00:00:00 2001 From: Apexo Date: Sat, 28 Mar 2026 23:40:53 +0100 Subject: D3AA simulator --- src/index.ts | 1 + src/lights/d3aa.ts | 286 ++++++++++++++++++++++++++++++++++++++ src/peripherals/avrdx-adc.ts | 188 +++++++++++++++++++++++++ src/peripherals/avrdx-ccp.ts | 29 ++++ src/peripherals/avrdx-clkctrl.ts | 72 ++++++++++ src/peripherals/avrdx-dac.ts | 30 ++++ src/peripherals/avrdx-nvmctrl.ts | 119 ++++++++++++++++ src/peripherals/avrdx-port-org.ts | 228 ++++++++++++++++++++++++++++++ src/peripherals/avrdx-port.ts | 253 +++++++++++++++++++++++++++++++++ src/peripherals/avrdx-rstctrl.ts | 30 ++++ src/peripherals/avrdx-rtc-pit.ts | 122 ++++++++++++++++ src/peripherals/avrdx-sigrow.ts | 59 ++++++++ src/peripherals/avrdx-slpctrl.ts | 35 +++++ src/peripherals/avrdx-vref.ts | 32 +++++ src/peripherals/avrdx-wdt.ts | 22 +++ src/util.ts | 24 ++++ 16 files changed, 1530 insertions(+) create mode 100644 src/index.ts create mode 100644 src/lights/d3aa.ts create mode 100644 src/peripherals/avrdx-adc.ts create mode 100644 src/peripherals/avrdx-ccp.ts create mode 100644 src/peripherals/avrdx-clkctrl.ts create mode 100644 src/peripherals/avrdx-dac.ts create mode 100644 src/peripherals/avrdx-nvmctrl.ts create mode 100644 src/peripherals/avrdx-port-org.ts create mode 100644 src/peripherals/avrdx-port.ts create mode 100644 src/peripherals/avrdx-rstctrl.ts create mode 100644 src/peripherals/avrdx-rtc-pit.ts create mode 100644 src/peripherals/avrdx-sigrow.ts create mode 100644 src/peripherals/avrdx-slpctrl.ts create mode 100644 src/peripherals/avrdx-vref.ts create mode 100644 src/peripherals/avrdx-wdt.ts create mode 100644 src/util.ts (limited to 'src') 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 = { + 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); + } +} -- cgit v1.2.3