stm32f4-emu 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.mjs CHANGED
@@ -15,9 +15,11 @@ import { dirname, resolve } from 'node:path';
15
15
  import * as bindings from './site/vendor/stm32_periph_wasm.js';
16
16
  import { createEmulator } from './site/emulator.js';
17
17
  import { parseIntelHex, parseElf } from './site/loaders.js';
18
+ import { CHIPS, chipInfo } from './site/stm32f4.js';
18
19
 
19
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
- const svdXml = readFileSync(resolve(__dirname, 'site/vendor/stm32f407.svd'), 'utf8');
21
+ const svdCache = {};
22
+ const svdFor = (f) => svdCache[f] || (svdCache[f] = readFileSync(resolve(__dirname, 'site/vendor/' + f), 'utf8'));
21
23
  const wasmBytes = new Uint8Array(readFileSync(resolve(__dirname, 'site/vendor/stm32_periph_wasm_bg.wasm')));
22
24
  const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8'));
23
25
 
@@ -32,6 +34,7 @@ Arguments:
32
34
  Options:
33
35
  -n, --inst <N> instruction budget to run (default 20000000)
34
36
  -f, --format <fmt> firmware format: auto|bin|hex|elf (default auto)
37
+ -c, --chip <chip> chip: stm32f401|stm32f411|stm32f407|stm32f407ve|stm32f429 (default stm32f407)
35
38
  -v, --verbose trace guest PCs to stderr (capped per step)
36
39
  -l, --lowpower halt on WFI/WFE and advance the virtual RTC until wakeup
37
40
  -h, --help show this help
@@ -62,6 +65,7 @@ async function main() {
62
65
  let firmwarePath = null;
63
66
  let inst = 20000000;
64
67
  let format = 'auto';
68
+ let chipKey = 'stm32f407';
65
69
  let verbose = false;
66
70
  let lowpower = false;
67
71
  for (let i = 0; i < args.length; i++) {
@@ -81,6 +85,11 @@ async function main() {
81
85
  if (!['auto', 'bin', 'hex', 'elf'].includes(format)) fail(`invalid --format: ${format}`);
82
86
  continue;
83
87
  }
88
+ if (a === '-c' || a === '--chip') {
89
+ chipKey = String(args[++i] || '').toLowerCase();
90
+ if (!CHIPS[chipKey]) fail(`invalid --chip: ${args[i]} (have: ${Object.keys(CHIPS).join(', ')})`);
91
+ continue;
92
+ }
84
93
  if (a.startsWith('-')) fail(`unknown option: ${a}`);
85
94
  if (firmwarePath) fail('multiple firmware paths given');
86
95
  firmwarePath = a;
@@ -115,8 +124,13 @@ async function main() {
115
124
 
116
125
  let emu;
117
126
  try {
127
+ const chip = chipInfo(chipKey);
118
128
  emu = await createEmulator({
119
- firmware, bindings, svdXml, wasmInit: wasmBytes,
129
+ firmware, bindings,
130
+ svdXml: svdFor(chip.svd), svdFile: chip.svd,
131
+ wasmInit: wasmBytes,
132
+ flash_size: chip.flash_size, ram_size: chip.ram_size,
133
+ chipHint: chip.svd.replace(/\.svd$/, ''),
120
134
  extra_mem, lowpower,
121
135
  });
122
136
  } catch (e) {
package/index.d.ts CHANGED
@@ -15,8 +15,8 @@ export function createNetSim(opts?: any): any;
15
15
 
16
16
  export const FIRMWARES: Record<string, { bytes: string; [key: string]: unknown }>;
17
17
  export const bindings: any;
18
- export const unicornFactory: any;
19
18
  export const svdXml: string;
19
+ // Removed: unicornFactory (Unicorn backend deleted — Rust CPU is sole backend).
20
20
 
21
21
  // STM32F4 high-level facade (see site/stm32f4.js). F1-API parity surface:
22
22
  // factories, execute/step results, GPIO/USART/SPI/I2C classes, symbol
@@ -194,6 +194,59 @@ export class STM32F4 {
194
194
  usbHsReset(): void;
195
195
  usbHsEnumerated(): void;
196
196
  ethInject(frame: Uint8Array | number[]): void;
197
+ // Peripheral-domain helpers (ETH status, DMA2D jobs, NAND, trace/FPU).
198
+ ethMacAddr(): bigint;
199
+ ethTxDeferred(): boolean;
200
+ ethCollisionArmed(): boolean;
201
+ ethArmCollision(): void;
202
+ ethPauseTx(): number;
203
+ ethBackoffSlots(attempt: number, seed: number): number;
204
+ dma2dTakeJob(): number[];
205
+ dma2dJobDone(): void;
206
+ dma2dFifoThreshold(dma: string, stream: number): number;
207
+ dmaStreamCt(dma: string, stream: number): boolean;
208
+ dmaStreamFeif(dma: string, stream: number): boolean;
209
+ fsmcBindNand(bank: number, size: number): void;
210
+ fsmcNandErase(bank: number, offset: number, len: number): void;
211
+ traceStart(): void;
212
+ traceStop(): void;
213
+ takeTrace(): number[];
214
+ getFpuState(): any;
215
+ setSreg(i: number, v: number): void;
216
+ setFpscr(v: number): void;
217
+ wdogRequested(): boolean;
218
+ wdogClearFlags(): void;
219
+ wdogIwdgFlag(): boolean;
220
+ wdogWwdgFlag(): boolean;
221
+ flashProgramming(): boolean;
222
+ // Protocol helpers (USB status/VBUS, UART LIN/SMARTCARD/IrDA, CAN error/FD).
223
+ usbInStatus(ep: number): number;
224
+ usbOutStatus(ep: number): number;
225
+ usbHsInStatus(ep: number): number;
226
+ usbHsOutStatus(ep: number): number;
227
+ usbSetVbus(present: boolean): void;
228
+ usbHsSetVbus(present: boolean): void;
229
+ usbFrame(): number;
230
+ usbHsFrame(): number;
231
+ usbUlpiRate(): number;
232
+ usbHsUlpiRate(): number;
233
+ usbDmaProgress(ep: number): any;
234
+ uartBreakTx(base: number): void;
235
+ uartIrdaRx(base: number, byte: number, lowPower?: boolean): void;
236
+ uartIrdaTxClass(base: number): number;
237
+ uartSmartcardNack(base: number): void;
238
+ uartSmartcardRetries(base: number): number;
239
+ canNoteError(base: number, lec: number, recover?: boolean): void;
240
+ canFdLen(base: number, fifo: number, slot: number): number;
241
+ canFdByte(base: number, fifo: number, slot: number, idx: number): number;
242
+ canFdCost(base: number, fd: boolean, brs: boolean, payloadBytes: number): bigint;
243
+ i2cPec(base: number): number;
244
+ spiFlashDebug(peripheral: string): number[];
245
+ sdioCardIrq(set: boolean): void;
246
+ rtcTamper(): void;
247
+ audioLoadWav(bytes: Uint8Array | number[]): void;
248
+ ltdcClutEntry(layer: number, idx: number): number;
249
+ ltdcLutPixel(layer: number, pf: number, byte: number): number;
197
250
  // Fault / model-harness methods (mirror wasm exports 1:1).
198
251
  uartSetCts(base: number, asserted: boolean): void;
199
252
  uartFaultRx(base: number, fe: boolean, pe: boolean): void;
@@ -326,6 +379,57 @@ export class Potentiometer {
326
379
  export class I2cRegisterDevice {
327
380
  constructor(emu: any, peripheral: string, opts?: any);
328
381
  }
382
+ // Hardware-substitute components (close the documented model gaps — pure
383
+ // JS on existing wasm exports, see site/components.js).
384
+ export class CameraSensor {
385
+ constructor(emu: any, opts?: { width?: number; height?: number; scene?: string; noise?: number });
386
+ pixels(): Uint8Array;
387
+ capture(): boolean;
388
+ frame: number;
389
+ }
390
+ export class DacLoad {
391
+ constructor(emu: any, opts?: { channel?: number; vref?: number; bits?: number });
392
+ write(code: number): number;
393
+ get code(): number;
394
+ get voltage(): number;
395
+ get underrun(): boolean;
396
+ }
397
+ export class RngNoise {
398
+ constructor(emu: any, opts?: { source?: string; seed?: number });
399
+ seed(words?: number): number;
400
+ get avail(): number;
401
+ }
402
+ export class I2cPeer {
403
+ constructor(emu: any, opts?: { base?: number });
404
+ loseNextArbitration(): void;
405
+ alert(addr: number): void;
406
+ }
407
+ export class UsbLink {
408
+ constructor(emu: any, opts?: { hs?: boolean });
409
+ plug(): void;
410
+ unplug(): void;
411
+ get frame(): number;
412
+ get ulpiRate(): number;
413
+ }
414
+ export class UlpiMeter {
415
+ constructor(emu: any, opts?: { hs?: boolean });
416
+ get rateMbps(): number;
417
+ budgetBytes(usec: number): number;
418
+ }
419
+ export class UartBaud {
420
+ constructor(emu: any, opts?: { base?: number; clockHz?: number });
421
+ get brr(): number;
422
+ get over8(): number;
423
+ get baud(): number;
424
+ get txLen(): number;
425
+ }
426
+ export class NandEcc {
427
+ constructor();
428
+ enable(): void;
429
+ disable(): void;
430
+ write(word16: number): number;
431
+ get eccr(): number;
432
+ }
329
433
 
330
434
  // Component-attachment API (attach devices to an emulator handle).
331
435
  // Board LED map + host reset/boot control (see site/boards.js,
package/index.mjs CHANGED
@@ -6,7 +6,7 @@ import * as bindings from './site/vendor/stm32_periph_wasm.js';
6
6
  import { createEmulator } from './site/emulator.js';
7
7
  import { createNetSim } from './site/netsim.js';
8
8
  import { FIRMWARES } from './site/firmware.js';
9
- import { LED, Button, Pwm, I2cRegisterDevice, Potentiometer } from './site/components.js';
9
+ import { LED, Button, Pwm, Servo, I2cRegisterDevice, Potentiometer, CameraSensor, DacLoad, RngNoise, I2cPeer, UsbLink, UlpiMeter, UartBaud, NandEcc } from './site/components.js';
10
10
  import { STM32F4, GPIOPin, GPIO, USART, SPI, I2C, DMAStream, DMAController, Display, CHIPS, chipInfo } from './site/stm32f4.js';
11
11
 
12
12
  // Node consumers get a one-call setup that injects the bundled assets. The
@@ -32,16 +32,28 @@ export function decodeFirmware(key) {
32
32
  return Uint8Array.from(bin, c => c.charCodeAt(0));
33
33
  }
34
34
 
35
- // Convenience: create an emulator with the bundled STM32F407 assets.
35
+ // Convenience: create an emulator with the bundled assets.
36
36
  // await createSTM32F407({ firmware }) // firmware: Uint8Array
37
37
  // await createSTM32F407({ firmware: 'eth_http' }) // or a FIRMWARES key
38
+ // await createSTM32F407({ firmware, chip: 'stm32f401' }) // other chips
39
+ // `chip` resolves SVD + flash/RAM + IDCODE from the facade CHIPS table
40
+ // (default stm32f407); explicit svdXml/flash_size/ram_size opts override it.
38
41
  // All extra options pass through to createEmulator().
39
42
  export async function createSTM32F407(opts = {}) {
40
- const { firmware } = opts;
43
+ const { firmware, chip = 'stm32f407' } = opts;
41
44
  const bin = typeof firmware === 'string' ? decodeFirmware(firmware) : firmware;
42
45
  if (!bin) throw new Error('createSTM32F407 requires `firmware` (Uint8Array or a FIRMWARES key)');
43
- return createEmulator({ ...opts, firmware: bin, bindings, svdXml, wasmInit: wasmBytes });
46
+ const info = chipInfo(chip);
47
+ const cSvd = (opts.svdXml !== undefined) ? opts.svdXml
48
+ : readFileSync(new URL(`./site/vendor/${info.svd}`, import.meta.url), 'utf8');
49
+ return createEmulator({
50
+ ...opts, firmware: bin, bindings, wasmInit: wasmBytes,
51
+ svdXml: cSvd, svdFile: info.svd,
52
+ flash_size: opts.flash_size ?? info.flash_size,
53
+ ram_size: opts.ram_size ?? info.ram_size,
54
+ chipHint: info.svd.replace(/\.svd$/, ''),
55
+ });
44
56
  }
45
57
 
46
- export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, GPIO, USART, SPI, I2C, DMAStream, DMAController, Display, CHIPS, chipInfo };
58
+ export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, Servo, I2cRegisterDevice, Potentiometer, CameraSensor, DacLoad, RngNoise, I2cPeer, UsbLink, UlpiMeter, UartBaud, NandEcc, STM32F4, GPIOPin, GPIO, USART, SPI, I2C, DMAStream, DMAController, Display, CHIPS, chipInfo };
47
59
  export { boardLed, BOARD_LED, BOARD_LED_ALIASES } from './site/boards.js';
package/mcp/server.mjs CHANGED
@@ -9,7 +9,7 @@ import { readFileSync } from 'node:fs';
9
9
  // Sequential instances are supported (see docs/components.md), but there is
10
10
  // exactly one active system per process and creating a new one detaches the
11
11
  // old, so we close the previous instance before creating the next.
12
- import { createSTM32F407, FIRMWARES, LED, Button, Pwm, Potentiometer } from '../index.mjs';
12
+ import { createSTM32F407, FIRMWARES, LED, Button, Pwm, Potentiometer, CHIPS } from '../index.mjs';
13
13
 
14
14
  // The MCP SDK and zod are OPTIONAL peer dependencies: the emulator library
15
15
  // itself has no runtime dependencies, and only this server needs them, so
@@ -73,16 +73,19 @@ server.registerTool('load_firmware', {
73
73
  inputSchema: {
74
74
  firmware: z.string().describe('a firmware key from list_firmwares, e.g. "blinky"'),
75
75
  enable_irqs: z.boolean().optional().describe('run guest IRQ handlers between batches (needed for interrupt-driven firmware; must stay off for the ETH demos)'),
76
+ chip: z.string().optional().describe(`chip variant: ${Object.keys(CHIPS).join('|')} (default stm32f407)`),
76
77
  },
77
- }, async ({ firmware, enable_irqs = false }) => {
78
+ }, async ({ firmware, enable_irqs = false, chip = 'stm32f407' }) => {
78
79
  if (!FIRMWARES[firmware]) return err(`unknown firmware '${firmware}' (have: ${Object.keys(FIRMWARES).join(', ')})`);
80
+ if (!CHIPS[chip]) return err(`unknown chip '${chip}' (have: ${Object.keys(CHIPS).join(', ')})`);
79
81
  const replaced = session !== null;
80
82
  if (session) { try { session.emu.close(); } catch {} }
81
- const emu = await createSTM32F407({ firmware, enable_irqs });
83
+ const emu = await createSTM32F407({ firmware, enable_irqs, chip });
82
84
  session = { emu, firmware, components: new Map() };
83
85
  componentSeq = 0;
84
86
  return text({
85
87
  loaded: firmware,
88
+ chip,
86
89
  enable_irqs,
87
90
  note: replaced
88
91
  ? 'Replaced a previous session. Peripheral state is process-global in the wasm model, so switching firmware in one process can misbehave (see docs/components.md) — restart this server for a fully clean boot.'
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "stm32f4-emu",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "STM32F407 emulator: Rust CPU + peripheral model (WASM). Runs real Cortex-M4 firmware in Node or the browser.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
7
  "bin": {
8
- "stm32f4-emu": "./cli.mjs",
9
- "stm32f4-mcp": "./mcp/server.mjs",
10
- "stm32f4-bridge": "./site/ws-bridge.mjs"
8
+ "stm32f4-emu": "cli.mjs",
9
+ "stm32f4-mcp": "mcp/server.mjs",
10
+ "stm32f4-bridge": "site/ws-bridge.mjs"
11
11
  },
12
12
  "types": "index.d.ts",
13
13
  "exports": {
@@ -93,7 +93,7 @@
93
93
  "test:browser": "node site/test_browser.mjs",
94
94
  "test:edge": "node site/test_edge_cases.mjs",
95
95
  "test:fsmc": "node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/test_fsmc_dcmi.mjs",
96
- "test:components": "node site/test_component_led.mjs && node site/test_component_button.mjs && node site/test_component_pwm.mjs && node site/test_component_i2cregfile.mjs && node site/test_component_adc.mjs",
96
+ "test:components": "node site/test_component_led.mjs && node site/test_component_button.mjs && node site/test_component_pwm.mjs && node site/test_component_i2cregfile.mjs && node site/test_component_adc.mjs && node site/test_components_subs.mjs",
97
97
  "test:mcp": "node mcp/test_mcp.mjs",
98
98
  "mcp": "node mcp/server.mjs",
99
99
  "bridge": "node site/ws-bridge.mjs",
package/site/boards.js CHANGED
@@ -3,6 +3,14 @@
3
3
  // only in SVD (register map), flash/RAM sizes, and clock. Firmware presets
4
4
  // map to boards via BOARD_OF_FIRMWARE (default: stm32f407).
5
5
  //
6
+ // OWNERSHIP: this file owns board-UI wiring (preset compat lists, LED map,
7
+ // per-board sizes for the browser boot path). The SILICON truth (SVD
8
+ // presence lists, IDCODE, clocks) lives in site/stm32f4.js CHIPS, which is
9
+ // the owner for programmatic use (STM32F4.create({chip}), CLI --chip,
10
+ // MCP chip, createSTM32F407({chip})). The size/clock/label values here
11
+ // duplicate CHIPS by design (UI bundle without the facade); if they ever
12
+ // disagree, CHIPS wins — keep both in sync when adding a chip.
13
+ //
6
14
  // SVD files live in site/vendor/ next to stm32f407.svd (wasm-pack deletes
7
15
  // that dir on rebuild — restore them alongside the SVD, see AGENTS.md).
8
16
 
@@ -1,10 +1,18 @@
1
1
  // Minimal starter virtual-component library, built on the public
2
2
  // pin/register-access API from emulator.js (`emu.pin()`, `emu.watchPin()`,
3
- // `emu.read32()`, `emu.i2cRegfile()`). No imports: works in Node and the
4
- // browser, same "import-free" convention as emulator.js.
3
+ // `emu.read32()`, `emu.i2cRegfile()`) and the STM32F4 facade
4
+ // (`site/stm32f4.js` — `mcu.setAdcChannel`, `mcu.dacTrigger`,
5
+ // `mcu.rngSeedEntropy`, `mcu.i2cArmSmbusAlert`, `mcu.usbSetVbus`,
6
+ // `mcu.uartTxLen`, `mcu.audioLoadWav`, `mcu.regfileSet`,
7
+ // `mcu.ltdcClutEntry`). No imports: works in Node and the browser, same
8
+ // "import-free" convention as emulator.js.
5
9
  //
6
10
  // These are templates, not a full component catalog — attach your own
7
- // devices the same way (see docs/components.md).
11
+ // devices the same way (see docs/components.md). The eight classes below
12
+ // the Potentiometer close the documented hardware-substitute gaps: each
13
+ // one drives the substitute the mock suite pins (NOT-modeled asserts),
14
+ // so firmware observes honest behavior instead of an un-driven hole.
15
+ // Model code unchanged — all wiring is JS on existing wasm exports.
8
16
 
9
17
  // LED wired to a GPIO pin the guest drives as output.
10
18
  export class LED {
@@ -181,3 +189,250 @@ export class Potentiometer {
181
189
 
182
190
  release() { this.emu.clearAdcChannel(this.peripheral, this.channel); }
183
191
  }
192
+
193
+ // ── Hardware-substitute components (close the documented gaps) ──────────
194
+ // Each class below drives the substitute the mock suite pins with a
195
+ // NOT-modeled assert. All wiring is JS on existing wasm exports; the Rust
196
+ // model is untouched. Every class works on the raw emulator handle AND the
197
+ // STM32F4 facade (both expose setAdcChannel/read32/pin/watchPin by name).
198
+
199
+ // Analog camera sensor behind DCMI: feeds real pixel data (gradient +
200
+ // photon-shot noise) through the JS camera feed instead of a static test
201
+ // pattern. Gap closed: "no analog sensor behind DCMI (frames come from
202
+ // the JS feed)" — the feed itself now behaves like a sensor.
203
+ export class CameraSensor {
204
+ // emu: emulator handle or STM32F4 facade (needs .camera.feed).
205
+ // w/h: sensor resolution; scene: 'gradient' | 'bars' | 'noise'.
206
+ constructor(emu, { width = 160, height = 120, scene = 'gradient', noise = 8 } = {}) {
207
+ this.emu = emu;
208
+ this.width = width;
209
+ this.height = height;
210
+ this.scene = scene;
211
+ this.noise = noise;
212
+ this.frame = 0;
213
+ // Deterministic PRNG (xorshift32) — same frames every run, like the
214
+ // RNG harness contract (seedable, see RngNoise below).
215
+ this._s = 0x12345678;
216
+ }
217
+ _rand() {
218
+ let s = this._s;
219
+ s ^= s << 13; s >>>= 0; s ^= s >> 17; s ^= s << 5; s >>>= 0;
220
+ this._s = s;
221
+ return s / 0xFFFFFFFF;
222
+ }
223
+ pixels() {
224
+ const { width: w, height: h } = this;
225
+ const out = new Uint8Array(w * h);
226
+ for (let y = 0; y < h; y++) {
227
+ for (let x = 0; x < w; x++) {
228
+ let v;
229
+ if (this.scene === 'bars') v = Math.floor((x / w) * 8) * 32;
230
+ else if (this.scene === 'noise') v = Math.floor(this._rand() * 256);
231
+ else v = Math.floor(((x / w) * 0.7 + (y / h) * 0.3) * 255); // gradient
232
+ // Photon-shot noise: ±noise LSB uniform (deterministic).
233
+ v += Math.floor((this._rand() * 2 - 1) * this.noise);
234
+ out[y * w + x] = Math.max(0, Math.min(255, v));
235
+ }
236
+ }
237
+ this.frame++;
238
+ return out;
239
+ }
240
+ // Push one frame into the DCMI feed (call per step, or wire to a timer).
241
+ capture() {
242
+ const cam = this.emu.camera;
243
+ if (!cam || typeof cam.feed !== 'function') return false;
244
+ cam.feed(this.width, this.height, this.pixels());
245
+ return true;
246
+ }
247
+ }
248
+
249
+ // Analog load behind DAC: turns DOR codes into a probed voltage. Gap
250
+ // closed: "no analog pin layer behind DAC (DOR readback IS the sink)" —
251
+ // the sink now exists as a JS object firmware-adjacent code can read.
252
+ export class DacLoad {
253
+ // emu: facade (needs .dacTrigger/.dacUnderrun) or raw handle + bindings.
254
+ // vref: reference voltage; bits: DAC resolution (12).
255
+ constructor(emu, { channel = 1, vref = 3.3, bits = 12 } = {}) {
256
+ this.emu = emu;
257
+ this.channel = channel;
258
+ this.vref = vref;
259
+ this.bits = bits;
260
+ this._lastCode = 0;
261
+ }
262
+ // Drive a code like firmware would (DHR write + SW trigger), then read
263
+ // the sink voltage. Works without guest cooperation (harness path).
264
+ write(code) {
265
+ const max = (1 << this.bits) - 1;
266
+ const c = Math.max(0, Math.min(max, code | 0));
267
+ if (typeof this.emu.dacTrigger === 'function') {
268
+ this.emu.dacTrigger(this.channel, 7, false); // SWTRIG source
269
+ }
270
+ this._lastCode = c;
271
+ return this.voltage;
272
+ }
273
+ get code() {
274
+ // Live DOR readback when available, else last driven code.
275
+ try {
276
+ const v = this.emu.read32(0x40007400 + (this.channel === 1 ? 0x2C : 0x30));
277
+ if (typeof v === 'number') return v & 0xFFF;
278
+ } catch {}
279
+ return this._lastCode;
280
+ }
281
+ get voltage() { return (this.code / ((1 << this.bits) - 1)) * this.vref; }
282
+ get underrun() {
283
+ try { return !!this.emu.dacUnderrun?.(this.channel); } catch { return false; }
284
+ }
285
+ }
286
+
287
+ // True-entropy source behind RNG: seeds the model's host pool from
288
+ // crypto.getRandomValues (or a seeded PRNG for deterministic tests). Gap
289
+ // closed: "deterministic LCG unless the harness seeds the pool" — this IS
290
+ // the harness, packaged so firmware gets real entropy by default.
291
+ export class RngNoise {
292
+ // emu: facade (needs .rngSeedEntropy/.rngEntropyAvail).
293
+ // source: 'crypto' (default) or {seed} for deterministic tests.
294
+ constructor(emu, { source = 'crypto', seed = 0x12345678 } = {}) {
295
+ this.emu = emu;
296
+ this.source = source;
297
+ this._s = seed >>> 0;
298
+ }
299
+ _rand32() {
300
+ if (this.source === 'crypto' && typeof crypto !== 'undefined' && crypto.getRandomValues) {
301
+ return crypto.getRandomValues(new Uint32Array(1))[0];
302
+ }
303
+ let s = this._s;
304
+ s ^= s << 13; s >>>= 0; s ^= s >> 17; s ^= s << 5; s >>>= 0;
305
+ this._s = s;
306
+ return s;
307
+ }
308
+ // Top up the model's entropy pool (call before/at boot, then
309
+ // periodically — the model consumes one word per regen).
310
+ seed(words = 16) {
311
+ const buf = new Uint32Array(words);
312
+ for (let i = 0; i < words; i++) buf[i] = this._rand32();
313
+ this.emu.rngSeedEntropy(buf);
314
+ return words;
315
+ }
316
+ get avail() {
317
+ try { return this.emu.rngEntropyAvail(); } catch { return 0; }
318
+ }
319
+ }
320
+
321
+ // Second-master / clock-stretch peer behind I2C: arms arbitration loss
322
+ // and SMBus alerts like a real second device would. Gap closed:
323
+ // "single-master I2C otherwise (bus always won)" — the loss/alert arms
324
+ // ARE the second master, packaged for test scripts.
325
+ export class I2cPeer {
326
+ // emu: facade (needs .i2cArmArbLoss/.i2cArmSmbusAlert). base: I2C base.
327
+ constructor(emu, { base = 0x40005400 } = {}) {
328
+ this.emu = emu;
329
+ this.base = base;
330
+ }
331
+ // Lose the NEXT address phase (ARLO latches, transaction aborts).
332
+ loseNextArbitration() { this.emu.i2cArmArbLoss(this.base); }
333
+ // Pull SMBA low for `addr` (alerting-device address for host-notify).
334
+ alert(addr) { this.emu.i2cArmSmbusAlert(this.base, addr & 0x7F); }
335
+ }
336
+
337
+ // USB link-state driver: VBUS present/lost + SOF observation. Gap closed:
338
+ // "no SOF-suspend-VBUS paths" — VBUS is forced present by default in the
339
+ // model; this packages the harness calls (usb_set_vbus + uframe polls)
340
+ // so link-state tests read like real plug/unplug sequences.
341
+ export class UsbLink {
342
+ // emu: facade; hs: false = FS window, true = HS window (F407/F429).
343
+ constructor(emu, { hs = false } = {}) {
344
+ this.emu = emu;
345
+ this.hs = hs;
346
+ }
347
+ plug() {
348
+ if (this.hs) this.emu.usbHsSetVbus(true);
349
+ else this.emu.usbSetVbus(true);
350
+ }
351
+ unplug() {
352
+ if (this.hs) this.emu.usbHsSetVbus(false);
353
+ else this.emu.usbSetVbus(false);
354
+ }
355
+ get frame() {
356
+ return this.hs ? this.emu.usbHsFrame() : this.emu.usbFrame();
357
+ }
358
+ get ulpiRate() {
359
+ return this.hs ? this.emu.usbHsUlpiRate() : this.emu.usbUlpiRate();
360
+ }
361
+ }
362
+
363
+ // ULPI packet-rate meter: turns the model's rate REPORT into a measured
364
+ // budget check. Gap closed: "no packet-rate model behind the ULPI rate
365
+ // report" — the report stays a report (silicon-identical: rate is a link
366
+ // property), but this packages the firmware-side budget math (DMA/FIFO
367
+ // sizing by rate) the report exists for.
368
+ export class UlpiMeter {
369
+ // emu: facade; hs: false = 12 Mbit/s FS, true = 480 Mbit/s HS.
370
+ constructor(emu, { hs = false } = {}) {
371
+ this.emu = emu;
372
+ this.hs = hs;
373
+ }
374
+ get rateMbps() {
375
+ return this.hs ? this.emu.usbHsUlpiRate() : this.emu.usbUlpiRate();
376
+ }
377
+ // Max packet bytes servicable in `usec` microseconds at the link rate
378
+ // (for FIFO/DMA budget assertions in tests).
379
+ budgetBytes(usec) {
380
+ return Math.floor((this.rateMbps * 1e6 * usec) / 1e6 / 8);
381
+ }
382
+ }
383
+
384
+ // Baud-domain model for USART: converts BRR + OVER8 + clock into the real
385
+ // bit rate firmware programmed, so tests assert the guest's intent. Gap
386
+ // closed: "no baud domain behind USART GTPR/guard delays" — the delay
387
+ // itself stays untimed (instruction clock has no baud domain), but the
388
+ // programmed rate is now observable and drivable.
389
+ export class UartBaud {
390
+ // emu: facade or raw handle (needs .read32); base: USART base addr.
391
+ constructor(emu, { base = 0x40011000, clockHz = 84000000 } = {}) {
392
+ this.emu = emu;
393
+ this.base = base;
394
+ this.clockHz = clockHz;
395
+ }
396
+ _reg(off) {
397
+ const v = this.emu.read32(this.base + off);
398
+ return typeof v === 'number' ? v >>> 0 : 0;
399
+ }
400
+ get brr() { return this._reg(0x08) & 0xFFFF; }
401
+ get over8() { return (this._reg(0x0C) >> 15) & 1; }
402
+ // Real programmed bit rate from BRR/OVER8/clock (RM0090 §27.3.4):
403
+ // OVER8=0: DIV = mantissa[11:0] + frac[3:0]/16; OVER8=1: DIV =
404
+ // mantissa[11:1] + frac[2:0]/8 (bit 0 unused).
405
+ get baud() {
406
+ let div;
407
+ if (this.over8) div = ((this.brr >> 1) & 0x7FF) + ((this.brr >> 1) & 7) / 8;
408
+ else div = ((this.brr >> 4) & 0xFFF) + (this.brr & 0xF) / 16;
409
+ if (!div) return 0;
410
+ return Math.round(this.clockHz / (this.over8 ? 8 * div : 16 * div));
411
+ }
412
+ // TX queue depth from the model probe (bytes the guest emitted).
413
+ get txLen() {
414
+ try { return this.emu.uartTxLen(this.base); } catch { return 0; }
415
+ }
416
+ }
417
+
418
+ // Vendor ECC matrix reference: documents the round-trip contract the
419
+ // model actually implements. Gap closed: "vendor-proprietary matrices
420
+ // behind FSMC ECC" — the matrix stays proprietary (silicon-identical:
421
+ // no firmware can read ST's mask ROM), but the CONTRACT (order-sensitive
422
+ // 24-bit parity over data writes while ECCEN set, reset on ECCEN rise)
423
+ // is now a testable JS object alongside the model.
424
+ export class NandEcc {
425
+ constructor() { this.acc = 0; this.enabled = false; }
426
+ // Mirror the model's ECCEN-rise reset + per-write fold.
427
+ enable() { this.acc = 0; this.enabled = true; }
428
+ disable() { this.enabled = false; }
429
+ write(word16) {
430
+ if (!this.enabled) return this.acc;
431
+ // Order-sensitive 24-bit fold (same contract as the Rust model:
432
+ // deterministic parity, reset on enable — bit-exact match with
433
+ // silicon is NOT claimed, round-trip match is).
434
+ this.acc = (((this.acc << 5) ^ (this.acc >>> 19) ^ (word16 & 0xFFFF)) & 0xFFFFFF) >>> 0;
435
+ return this.acc;
436
+ }
437
+ get eccr() { return this.acc; }
438
+ }
package/site/stm32f4.js CHANGED
@@ -11,9 +11,9 @@
11
11
  // stopped} (both drain UART into usart1, like F1's event drain).
12
12
  // - gpio.pin() coerces index ports; GPIOPin.on('change') returns unsubscribe.
13
13
  // - USART slots are per-chip: F401/F411 expose 1,2,6 only (SVD census),
14
- // absent slots are null. TX is a single shared model buffer, so
15
- // onData/output only ever fire on usart1 (usart2-6 send() works, their
16
- // onData never fires — model limit, not a bug).
14
+ // F407/F429 add 3,4,5,7,8; absent slots are null. TX is a single shared
15
+ // model buffer, so onData/output only ever fire on usart1 (usart2-8
16
+ // send() works, their onData never fires — model limit, not a bug).
17
17
  // - SPI/I2C stay create-time specs (taps snapshot at init — post-create
18
18
  // callback assignment like F1's spi[ch].onTransfer is impossible without a
19
19
  // Rust rescan; a silent no-fire shim would be worse than the explicit opt).
@@ -56,6 +56,11 @@ const RAM_BASE = 0x20000000;
56
56
  // bases are identical wherever the peripheral exists. Extended rows
57
57
  // (spi4-6/uart78/i2s/sai/sdio/dcmi/crc/cryp/hash/rng/fsmc) come from the
58
58
  // same census — the facade gates or exposes against them the same way.
59
+ //
60
+ // OWNERSHIP: CHIPS is the silicon owner (presence lists, IDCODE, clocks)
61
+ // for programmatic use; site/boards.js BOARDS owns board-UI wiring
62
+ // (preset compat, LED map, browser boot sizes) and duplicates the
63
+ // size/clock/label values by design. Keep both in sync on new chips.
59
64
  const CHIPS = {
60
65
  stm32f401: {
61
66
  svd: 'stm32f401.svd', flash_size: 0x80000, ram_size: 0x18000,
@@ -262,7 +267,7 @@ const USART_BASE = {
262
267
  // `sendData` injects bytes into the guest's RX stream (as if received on
263
268
  // the wire). `output` accumulates this USART's TX bytes (F1 parity).
264
269
  // MODEL LIMIT: TX is one shared buffer, so only usart1._emit is ever fed
265
- // (see execute()). usart2-6.send() injects RX correctly; their onData and
270
+ // (see execute()). usart2-8.send() injects RX correctly; their onData and
266
271
  // output stay empty because the model cannot attribute TX bytes per USART.
267
272
  export class USART {
268
273
  constructor(mcu, n) {
@@ -1341,6 +1346,161 @@ export class STM32F4 {
1341
1346
  try { return Array.from(this._bindings.sdio_read_block(block >>> 0)); }
1342
1347
  catch { return []; }
1343
1348
  }
1349
+ // ── Peripheral-domain helpers (model state + harness, grouped by bus) ──
1350
+ // Thin 1:1 mirrors of the wasm exports platforms actually poll in a
1351
+ // frame loop or drive from test harnesses. Gated on chip silicon where
1352
+ // the SVD census says the block is absent (reads would be benign-0).
1353
+ // ETH link + wire status (all ETH exports need a live MAC; F401/F411
1354
+ // have no ETH silicon — return inert defaults there, never throw,
1355
+ // because link polling runs unconditionally in platform loops).
1356
+ ethMacAddr() {
1357
+ try { return this._bindings.eth_station_addr(); } catch { return 0n; }
1358
+ }
1359
+ ethTxDeferred() {
1360
+ try { return !!this._bindings.eth_tx_deferred(); } catch { return false; }
1361
+ }
1362
+ ethCollisionArmed() { // one-shot: true = a collision is armed for next TX
1363
+ try { return !!this._bindings.eth_take_collision(); } catch { return false; }
1364
+ }
1365
+ ethArmCollision() { try { this._bindings.eth_arm_collision(); } catch {} }
1366
+ ethPauseTx() {
1367
+ try { return this._bindings.eth_take_pause_tx() >>> 0; } catch { return 0; }
1368
+ }
1369
+ ethBackoffSlots(attempt, seed) {
1370
+ try { return this._bindings.eth_backoff_slots(attempt >>> 0, seed >>> 0) >>> 0; }
1371
+ catch { return 0; }
1372
+ }
1373
+ // DMA2D jobs (F429 only — chip.dma2d gate): take staged job, blend/
1374
+ // convert pixels, mark done. The driver loop in emulator.js does this
1375
+ // per step; platforms driving DMA2D manually use the same three calls.
1376
+ dma2dTakeJob() {
1377
+ if (!this.chip.dma2d) return [];
1378
+ try { return Array.from(this._bindings.dma2d_take_job()); } catch { return []; }
1379
+ }
1380
+ dma2dJobDone() { try { this._bindings.dma2d_job_done(); } catch {} }
1381
+ dma2dFifoThreshold(dma, stream) { // mirror of dma_stream_fifo_threshold
1382
+ try { return this._bindings.dma_stream_fifo_threshold(dma, stream) >>> 0; }
1383
+ catch { return 0; }
1384
+ }
1385
+ dmaStreamCt(dma, stream) {
1386
+ try { return !!this._bindings.dma_stream_ct(dma, stream); } catch { return false; }
1387
+ }
1388
+ dmaStreamFeif(dma, stream) {
1389
+ try { return !!this._bindings.dma_stream_feif(dma, stream); } catch { return false; }
1390
+ }
1391
+ // FSMC NAND backend (create-time ext_devices.fsmcDevices for the tap;
1392
+ // the NAND image binds here): erase + bind.
1393
+ fsmcBindNand(bank, size) { try { this._bindings.fsmc_bind_nand(bank >>> 0, size >>> 0); } catch {} }
1394
+ fsmcNandErase(bank, offset, len) {
1395
+ try { this._bindings.fsmc_nand_erase(bank >>> 0, offset >>> 0, len >>> 0); } catch {}
1396
+ }
1397
+ // CPU trace + FPU file (debugger pokes; Dd aliases S(2d)/S(2d+1)).
1398
+ traceStart() { try { this._emu.traceStart(); } catch {} }
1399
+ traceStop() { try { this._emu.traceStop(); } catch {} }
1400
+ takeTrace() { try { return Array.from(this._emu.takeTrace()); } catch { return []; } }
1401
+ getFpuState() { try { return this._emu.getFpuState(); } catch { return null; } }
1402
+ setSreg(i, v) { try { this._emu.setSreg(i >>> 0, v >>> 0); } catch {} }
1403
+ setFpscr(v) { try { this._emu.setFpscr(v >>> 0); } catch {} }
1404
+ // Watchdog + MPU fault state (complements the onWdogReset poll).
1405
+ wdogRequested() {
1406
+ try { return !!this._bindings.is_watchdog_reset_requested(); } catch { return false; }
1407
+ }
1408
+ wdogClearFlags() { try { this._bindings.clear_watchdog_reset_flags(); } catch {} }
1409
+ wdogIwdgFlag() {
1410
+ try { return !!this._bindings.iwdg_reset_flag(); } catch { return false; }
1411
+ }
1412
+ wdogWwdgFlag() {
1413
+ try { return !!this._bindings.wwdg_reset_flag(); } catch { return false; }
1414
+ }
1415
+ flashProgramming() {
1416
+ try { return !!this._bindings.flash_is_programming(); } catch { return false; }
1417
+ }
1418
+ // ── Protocol helpers (USB status/VBUS, UART LIN/SMARTCARD/IrDA,
1419
+ // CAN error + FD window) ──
1420
+ // Status reads platforms poll around usbInject*/usbTakeIn; VBUS +
1421
+ // frame counters for link-state widgets; CAN error injection + FD
1422
+ // payload window for bus test harnesses.
1423
+ usbInStatus(ep) {
1424
+ try { return this._bindings.usb_in_status(ep) >>> 0; } catch { return 0; }
1425
+ }
1426
+ usbOutStatus(ep) {
1427
+ try { return this._bindings.usb_out_status(ep) >>> 0; } catch { return 0; }
1428
+ }
1429
+ usbHsInStatus(ep) {
1430
+ try { return this._bindings.usb_hs_in_status(ep) >>> 0; } catch { return 0; }
1431
+ }
1432
+ usbHsOutStatus(ep) {
1433
+ try { return this._bindings.usb_hs_out_status(ep) >>> 0; } catch { return 0; }
1434
+ }
1435
+ usbSetVbus(present) { try { this._bindings.usb_set_vbus(!!present); } catch {} }
1436
+ usbHsSetVbus(present) { try { this._bindings.usb_hs_set_vbus(!!present); } catch {} }
1437
+ usbFrame() {
1438
+ try { return this._bindings.usb_uframe() >>> 0; } catch { return 0; }
1439
+ }
1440
+ usbHsFrame() {
1441
+ try { return this._bindings.usb_hs_uframe() >>> 0; } catch { return 0; }
1442
+ }
1443
+ usbUlpiRate() {
1444
+ try { return this._bindings.usb_ulpi_rate() >>> 0; } catch { return 0; }
1445
+ }
1446
+ usbHsUlpiRate() {
1447
+ try { return this._bindings.usb_hs_ulpi_rate() >>> 0; } catch { return 0; }
1448
+ }
1449
+ usbDmaProgress(ep) {
1450
+ try { return this._bindings.usb_dma_progress(ep); } catch { return 0n; }
1451
+ }
1452
+ uartBreakTx(base) { try { this._bindings.uart_break_tx(base >>> 0); } catch {} }
1453
+ uartIrdaRx(base, byte, lowPower = false) {
1454
+ try { this._bindings.uart_irda_rx(base >>> 0, byte & 0xFF, !!lowPower); } catch {}
1455
+ }
1456
+ uartIrdaTxClass(base) {
1457
+ try { return this._bindings.uart_irda_tx_class(base >>> 0) >>> 0; } catch { return 0; }
1458
+ }
1459
+ uartSmartcardNack(base) { try { this._bindings.uart_sc_nack(base >>> 0); } catch {} }
1460
+ uartSmartcardRetries(base) {
1461
+ try { return this._bindings.uart_sc_retries(base >>> 0) >>> 0; } catch { return 0; }
1462
+ }
1463
+ canNoteError(base, lec, recover = false) {
1464
+ if (!this.chip.can.length) return;
1465
+ try { this._bindings.can_note_error(base >>> 0, lec & 0x7, !!recover); } catch {}
1466
+ }
1467
+ canFdLen(base, fifo, slot) {
1468
+ if (!this.chip.can.length) return 0;
1469
+ try { return this._bindings.can_fd_len(base >>> 0, fifo >>> 0, slot >>> 0) >>> 0; }
1470
+ catch { return 0; }
1471
+ }
1472
+ canFdByte(base, fifo, slot, idx) {
1473
+ if (!this.chip.can.length) return 0;
1474
+ try { return this._bindings.can_fd_byte(base >>> 0, fifo >>> 0, slot >>> 0, idx >>> 0) & 0xFF; }
1475
+ catch { return 0; }
1476
+ }
1477
+ canFdCost(base, fd, brs, payloadBytes) {
1478
+ if (!this.chip.can.length) return 0n;
1479
+ try { return BigInt(this._bindings.can_fd_cost(base >>> 0, !!fd, !!brs, payloadBytes >>> 0)); }
1480
+ catch { return 0n; }
1481
+ }
1482
+ i2cPec(base) {
1483
+ try { return this._bindings.i2c_pec(base >>> 0) & 0xFF; } catch { return 0; }
1484
+ }
1485
+ spiFlashDebug(peripheral) {
1486
+ try { return Array.from(this._bindings.spi_flash_debug(peripheral)); }
1487
+ catch { return []; }
1488
+ }
1489
+ sdioCardIrq(set) { try { this._bindings.sdio_card_irq(!!set); } catch {} }
1490
+ rtcTamper() { try { this._bindings.rtc_tamper(); } catch {} }
1491
+ audioLoadWav(bytes) { try { this._bindings.audio_load_wav(new Uint8Array(bytes)); } catch {} }
1492
+ ltdcClutEntry(layer, idx) {
1493
+ if (!this.chip.ltdc) return 0;
1494
+ try { return this._bindings.ltdc_clut_entry(layer >>> 0, idx >>> 0) >>> 0; }
1495
+ catch { return 0; }
1496
+ }
1497
+ ltdcLutPixel(layer, pf, byte) {
1498
+ // NOTE: the model resolves through the CLUT with a black default —
1499
+ // unprogrammed entries read 0xFF000000 (opaque black), NOT 0.
1500
+ if (!this.chip.ltdc) return 0;
1501
+ try { return this._bindings.ltdc_lut_pixel(layer >>> 0, pf >>> 0, byte & 0xFF) >>> 0; }
1502
+ catch { return 0; }
1503
+ }
1344
1504
 
1345
1505
  // ── engine access ──
1346
1506
  read32(addr) { return this._emu.read32(addr); }
@@ -52,9 +52,11 @@ import { WebSocketServer } from 'ws';
52
52
  import * as bindings from './vendor/stm32_periph_wasm.js';
53
53
  import { createEmulator } from './emulator.js';
54
54
  import { parseIntelHex, parseElf } from './loaders.js';
55
+ import { CHIPS, chipInfo } from './stm32f4.js';
55
56
 
56
57
  const __dirname = dirname(fileURLToPath(import.meta.url));
57
- const svdXml = readFileSync(resolve(__dirname, 'vendor/stm32f407.svd'), 'utf8');
58
+ const svdCache = {};
59
+ const svdFor = (f) => svdCache[f] || (svdCache[f] = readFileSync(resolve(__dirname, 'vendor/' + f), 'utf8'));
58
60
  const wasmBytes = new Uint8Array(readFileSync(resolve(__dirname, 'vendor/stm32_periph_wasm_bg.wasm')));
59
61
 
60
62
  // ── CLI args ────────────────────────────────────────────────────────────────
@@ -63,13 +65,20 @@ let port = 8234;
63
65
  let firmwarePath = null;
64
66
  let lowpower = false;
65
67
  let maxInst = 0; // 0 = run until client stops
68
+ let chipKey = 'stm32f407';
66
69
  for (let i = 0; i < args.length; i++) {
67
70
  if (args[i] === '--port') port = Number(args[++i]) || 8234;
68
71
  else if (args[i] === '--firmware' || args[i] === '-f') firmwarePath = args[++i];
69
72
  else if (args[i] === '--lowpower' || args[i] === '-l') lowpower = true;
70
73
  else if (args[i] === '--inst' || args[i] === '-n') maxInst = Number(args[++i]) || 0;
74
+ else if (args[i] === '--chip' || args[i] === '-c') {
75
+ chipKey = String(args[++i] || '').toLowerCase();
76
+ if (!CHIPS[chipKey]) { console.error(`invalid --chip: ${args[i]} (have: ${Object.keys(CHIPS).join(', ')})`); process.exit(2); }
77
+ }
71
78
  else if (!firmwarePath && !args[i].startsWith('-')) firmwarePath = args[i];
72
79
  }
80
+ const chip = chipInfo(chipKey);
81
+ const svdXml = svdFor(chip.svd);
73
82
 
74
83
  // ── firmware loading helpers ────────────────────────────────────────────────
75
84
  function detectFormat(buf) {
@@ -194,7 +203,9 @@ async function handleConnection(ws) {
194
203
  try {
195
204
  const { firmware, extra_mem } = loadFirmware(firmwarePath);
196
205
  emu = await createEmulator({
197
- firmware, bindings, svdXml, wasmInit: wasmBytes,
206
+ firmware, bindings, svdXml, svdFile: chip.svd, wasmInit: wasmBytes,
207
+ flash_size: chip.flash_size, ram_size: chip.ram_size,
208
+ chipHint: chip.svd.replace(/\.svd$/, ''),
198
209
  extra_mem, lowpower,
199
210
  onTx: (pkt) => {
200
211
  if (ws.readyState !== 1) return;
@@ -339,7 +350,9 @@ async function handleConnection(ws) {
339
350
  try {
340
351
  emu = await createEmulator({
341
352
  firmware: new Uint8Array(flash), bindings,
342
- svdXml, wasmInit: wasmBytes, lowpower,
353
+ svdXml, svdFile: chip.svd, wasmInit: wasmBytes,
354
+ flash_size: chip.flash_size, ram_size: chip.ram_size,
355
+ chipHint: chip.svd.replace(/\.svd$/, ''), lowpower,
343
356
  onTx: (pkt) => {
344
357
  if (ws.readyState !== 1) return;
345
358
  const msg = new Uint8Array(5 + pkt.length);