1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
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)));
}
}
}
|