stm32f4-emu 1.3.1 → 1.4.0
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 +16 -2
- package/index.d.ts +105 -1
- package/index.mjs +17 -5
- package/mcp/server.mjs +6 -3
- package/package.json +2 -2
- package/site/boards.js +8 -0
- package/site/components.js +258 -3
- package/site/stm32f4.js +164 -4
- package/site/ws-bridge.mjs +16 -3
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stm32f4-emu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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",
|
|
@@ -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
|
|
package/site/components.js
CHANGED
|
@@ -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()`)
|
|
4
|
-
//
|
|
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
|
|
15
|
-
// onData/output only ever fire on usart1 (usart2-
|
|
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-
|
|
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); }
|
package/site/ws-bridge.mjs
CHANGED
|
@@ -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
|
|
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,
|
|
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);
|