summaryrefslogtreecommitdiff
path: root/src/lights
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/lights/d3aa.ts286
1 files changed, 286 insertions, 0 deletions
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)));
+ }
+ }
+}
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.