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