stm32f4-emu 1.2.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/README.md CHANGED
@@ -90,13 +90,13 @@ npm run serve # then open http://127.0.0.1:8123
90
90
  npm test # == node site/test_flow.mjs
91
91
 
92
92
  # websocket bridge: headless Node serves the emulator, browser is a thin UI
93
- npm run bridge -- blinky/blinky.bin --port 8234
93
+ npm run bridge -- firmware/blinky/blinky.bin --port 8234
94
94
  # then open http://127.0.0.1:8123/console.html?bridge=ws://127.0.0.1:8234
95
95
 
96
96
  # gateway-backed run: firmware talks to a REAL network stack (gVisor)
97
97
  cd stm32-periph-wasm/pkg
98
- node cli.mjs ../../eth_http/eth_http.bin 10000000 \
99
- --gateway --config=../../eth_http/config.yaml
98
+ node cli.mjs ../../firmware/eth_http/eth_http.bin 10000000 \
99
+ --gateway --config=../../firmware/eth_http/config.yaml
100
100
  # (requires an HTTP server at 127.0.0.1:8092; see AGENTS.md §10)
101
101
  ```
102
102
 
@@ -132,6 +132,11 @@ custom SPI/I2C devices) to pins and buses with the component API —
132
132
  `ext_devices.spiDevices`/`i2cDevices` for your own bus protocol. See
133
133
  [docs/components.md](docs/components.md).
134
134
 
135
+ For platform integration (Wokwi/OpenHW/Velxio) use the `STM32F4` facade —
136
+ factories, GPIO/USART/SPI/I2C objects, polled event callbacks, DMA
137
+ controller, live displays, SWD/JTAG/power/probe helpers. See
138
+ [docs/facade.md](docs/facade.md).
139
+
135
140
  ## Drive it from an AI agent (MCP)
136
141
 
137
142
  `mcp/server.mjs` exposes the emulator over the Model Context Protocol —
@@ -155,7 +160,7 @@ is a thin UI. Zero impact on the existing local WASM path:
155
160
 
156
161
  ```bash
157
162
  # 1. Start the bridge in Node (serves the emulator over WS on port 8234)
158
- node site/ws-bridge.mjs eth_http/eth_http.bin --port 8234
163
+ node site/ws-bridge.mjs firmware/eth_http/eth_http.bin --port 8234
159
164
 
160
165
  # 2. Open the browser console with the bridge URL param
161
166
  open "http://127.0.0.1:8123/console.html?bridge=ws://127.0.0.1:8234"
@@ -214,7 +219,7 @@ firmware .bin ──► Rust WASM CPU (Thumb-2 + NVIC/exceptions)
214
219
  (SysTick, ETH, USART, SVC/PendSV) are delivered inline with exact
215
220
  exception stacking — no native deps, no JIT, no hooks.
216
221
  - **Peripherals**: a `wasm-bindgen` crate (`stm32-periph-wasm/`); registers
217
- and bit fields come from the vendor SVD (`monox/stm32f407.svd`).
222
+ and bit fields come from the vendor SVD (`site/vendor/stm32f407.svd`).
218
223
  - **Ethernet**: TX is captured from the DMA descriptors (OWN/FS/LS poll
219
224
  demand); RX frames are delivered head-only with FS+LS status, IPHCE/PCE
220
225
  checksum status, and PTP snapshots for event messages. The full MAC
@@ -249,13 +254,13 @@ firmware .bin ──► Rust WASM CPU (Thumb-2 + NVIC/exceptions)
249
254
  ├── index.mjs, package.json npm package entry (stm32f4-emu)
250
255
  ├── mcp/ MCP server (drive the emulator from an AI agent)
251
256
  ├── .github/workflows/ CI (Linux/Windows/macOS test matrix) + Pages deploy
252
- ├── tools/make_firmware.mjs Regenerates site/firmware.js from eth_*/.bin
257
+ ├── tools/make_firmware.mjs Regenerates site/firmware.js from firmware/*/.bin
253
258
  ├── stm32-periph-wasm/ Rust peripheral model (WASM build + pkg/)
254
- ├── eth_http/ eth_dhcp/ eth_test/ Sample network firmwares + configs
255
- ├── doom/ DOOM 1 port (doomgeneric f407 target + WAD path)
259
+ ├── firmware/eth_http/ eth_dhcp/ eth_test/ Sample network firmwares + configs
260
+ ├── firmware/doom/ DOOM 1 port (doomgeneric f407 target + WAD path)
256
261
  ├── openhw-local-gateway/ Go gateway (gVisor network stack)
257
262
  ├── scripts/verify_ethernet.sh Regression runner for all three firmwares
258
- ├── src/, monox/, saturn/ Native SDL emulator (upstream heritage)
263
+ ├── hardware/monox/, hardware/saturn/ Upstream printer firmwares (heritage)
259
264
  └── AGENTS.md Full architecture, build steps, runbook
260
265
  ```
261
266
 
@@ -267,10 +272,9 @@ cd stm32-periph-wasm && wasm-pack build --release --target nodejs
267
272
 
268
273
  # firmware (bare-metal Makefiles; toolchain from the Arduino core)
269
274
  TOOLCHAIN="$HOME/.arduino15/packages/STMicroelectronics/tools/xpack-arm-none-eabi-gcc/14.2.1-1.1/bin/arm-none-eabi-" \
270
- make -C eth_http # also eth_dhcp, eth_test
275
+ make -C firmware/eth_http # also eth_dhcp, eth_test (all firmware lives in firmware/)
271
276
 
272
- # native SDL emulator (upstream heritage, not the headless path)
273
- cd stm32-emulator-main && cargo build --release
277
+ # (no native SDL emulator in this tree — the headless WASM path above is the only backend)
274
278
  ```
275
279
 
276
280
  `wasm-pack` writes `site/vendor/.gitignore` containing `*` after a browser
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,10 +15,343 @@ 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
- // Component-attachment API (attach devices to an emulator handle).
21
+ // STM32F4 high-level facade (see site/stm32f4.js). F1-API parity surface:
22
+ // factories, execute/step results, GPIO/USART/SPI/I2C classes, symbol
23
+ // helpers, and engine pass-throughs. TX is a single shared model buffer
24
+ // (only usart1.onData/output fire); event callbacks beyond SPI/I2C taps
25
+ // are absent by design (no core event queue on F4).
26
+ export interface ExecuteResult { instCount: number; stopped: boolean; }
27
+ export interface FacadeStepResult { pc: number; instCount: number; stopped: boolean; }
28
+ export class GPIOPin {
29
+ constructor(mcu: STM32F4, port: string, pin: number);
30
+ on(event: 'change', cb: (high: boolean) => void): () => void;
31
+ addListener(cb: (high: boolean) => void): () => void;
32
+ read(): boolean;
33
+ readInput(): boolean;
34
+ setInput(high: boolean): void;
35
+ setInputValue(high: boolean): void;
36
+ detach(): void;
37
+ }
38
+ export class GPIO {
39
+ constructor(mcu: STM32F4);
40
+ pin(port: string | number, pin: number): GPIOPin;
41
+ }
42
+ export interface ChipRecord {
43
+ key: string;
44
+ svd: string;
45
+ flash_size: number;
46
+ ram_size: number;
47
+ maxClockMHz: number;
48
+ label: string;
49
+ idcode: number;
50
+ usarts: number[];
51
+ timers: number[];
52
+ can: number[];
53
+ dac: boolean;
54
+ ltdc: boolean;
55
+ gpioBanks: number;
56
+ spi: number[];
57
+ i2c: number[];
58
+ uart78: number[];
59
+ i2s: number[];
60
+ sai: number[];
61
+ sdio: boolean;
62
+ dcmi: boolean;
63
+ crc: boolean;
64
+ cryp: boolean;
65
+ hash: boolean;
66
+ rng: boolean;
67
+ fsmc: boolean;
68
+ dma2d: boolean;
69
+ qspi: boolean;
70
+ }
71
+ export declare const CHIPS: Record<string, Omit<ChipRecord, 'key'>>;
72
+ export declare function chipInfo(key?: string): ChipRecord;
73
+ export class USART {
74
+ readonly n: number;
75
+ onData: ((byte: number) => void) | null;
76
+ constructor(mcu: STM32F4, n: number);
77
+ send(data: string | Uint8Array | number[]): void;
78
+ sendData(data: string | Uint8Array | number[]): void;
79
+ get output(): string;
80
+ }
81
+ export class SPI {
82
+ readonly ch: number;
83
+ onTransfer: ((channel: string, tx: number[], rx: number[]) => void) | null;
84
+ constructor(mcu: STM32F4, ch: number);
85
+ injectMiso(bytes: Uint8Array | number[]): void;
86
+ }
87
+ export class I2C {
88
+ readonly ch: number;
89
+ onStart: ((addr: number, isRead: boolean) => void) | null;
90
+ onWrite: ((byte: number) => void) | null;
91
+ onRead: (() => void) | null;
92
+ onStop: (() => void) | null;
93
+ constructor(mcu: STM32F4, ch: number);
94
+ injectRx(bytes: Uint8Array | number[]): void;
95
+ }
96
+ export class DMAStream {
97
+ constructor(mcu: STM32F4, streamIndex: number);
98
+ pendingCount(): number;
99
+ setCompleted(success?: boolean): void;
100
+ tcif(): boolean;
101
+ htif(): boolean;
102
+ cr(): number;
103
+ ndtr(): number;
104
+ }
105
+ export class DMAController {
106
+ constructor(mcu: STM32F4, controller?: 1 | 2);
107
+ lisr(): number;
108
+ hisr(): number;
109
+ tcif(s: number): boolean;
110
+ htif(s: number): boolean;
111
+ cr(s: number): number;
112
+ ndtr(s: number): number;
113
+ par(s: number): number;
114
+ m0ar(s: number): number;
115
+ m1ar(s: number): number;
116
+ fcr(s: number): number;
117
+ stream(s: number): DMAStream;
118
+ }
119
+ export class Display {
120
+ constructor(mcu: STM32F4);
121
+ get oled(): { w: number; h: number; fb: Uint8Array; frame: number } | null;
122
+ get tft(): { w: number; h: number; fb: Uint8Array; frame: () => number } | null;
123
+ ltdc(): { w: number; h: number; pf: string; fb: Uint8Array; addr: number; pitch: number } | null;
124
+ }
125
+ export class STM32F4 {
126
+ readonly gpio: GPIO;
127
+ readonly usart: USART;
128
+ // Slots for absent-silicon USARTs are null (F401/F411: 3,4,5).
129
+ readonly usart1: USART; readonly usart2: USART; readonly usart3: USART | null;
130
+ readonly usart4: USART | null; readonly usart5: USART | null; readonly usart6: USART;
131
+ readonly usart7: USART | null; readonly usart8: USART | null;
132
+ readonly usarts: Record<number, USART>;
133
+ readonly spi1: SPI | null; readonly spi2: SPI; readonly spi3: SPI;
134
+ readonly spi4: SPI | null; readonly spi5: SPI | null; readonly spi6: SPI | null;
135
+ readonly spiBus: Record<number, SPI>;
136
+ readonly i2c1: I2C; readonly i2c2: I2C; readonly i2c3: I2C;
137
+ readonly i2cBus: Record<number, I2C>;
138
+ readonly spi: { specs: any[]; pushMiso(peripheral: string, bytes: Uint8Array | number[]): void };
139
+ readonly i2c: { specs: any[]; pushRx(peripheral: string, bytes: Uint8Array | number[]): void };
140
+ readonly dma: { stream(index: number): DMAStream; controller(ctl: 1 | 2): DMAController };
141
+ readonly display: Display;
142
+ readonly chip: ChipRecord;
143
+ readonly chipSvdXml: string | null;
144
+ // Wokwi/OpenHW/Velxio integration callbacks (polled dispatch, see site/stm32f4.js).
145
+ onExtiEdge: ((line: number) => void) | null;
146
+ onCanTx: ((can: number) => void) | null;
147
+ onCanRx: ((can: number, id: number, len: number, data: number[]) => void) | null;
148
+ onTimUpdate: ((tim: number) => void) | null;
149
+ onTimCapture: ((tim: number, ch: number, val: number) => void) | null;
150
+ onDmaTc: ((controller: number, stream: number) => void) | null;
151
+ onWdogReset: ((which: number) => void) | null;
152
+ onUsbIn: ((ep: number, data: number[]) => void) | null;
153
+ onUsbHsIn: ((ep: number, data: number[]) => void) | null;
154
+ onItmByte: ((port: number, byte: number) => void) | null;
155
+ onFsmcAccess: ((bank: number, off: number, write: boolean, size: number, val: number) => void) | null;
156
+ onAdcDone: ((adc: number, chan: number, value: number) => void) | null;
157
+ onDacWrite: ((chan: number, value: number) => void) | null;
158
+ onCrcResult: ((value: number) => void) | null;
159
+ onRtcAlarm: ((which: number) => void) | null;
160
+ onI2cAlert: ((peripheral: number, asserted: boolean) => void) | null;
161
+ onHostTx: ((...args: any[]) => void) | null;
162
+ onHostRx: ((...args: any[]) => void) | null;
163
+ constructor(emu: any, bindings?: any, chip?: ChipRecord, chipSvdXml?: string | null);
164
+ static create(opts?: any): Promise<STM32F4>;
165
+ static fromELF(buf: Uint8Array | ArrayBuffer, opts?: any): Promise<STM32F4>;
166
+ static fromBin(buf: Uint8Array | ArrayBuffer, opts?: any): Promise<STM32F4>;
167
+ static fromHex(text: string, opts?: any): Promise<STM32F4>;
168
+ loadBin(bytes: Uint8Array | ArrayBuffer, base?: number): void;
169
+ loadHex(text: string): void;
170
+ loadELF(bytes: Uint8Array | ArrayBuffer): void;
171
+ // Live peripheral injection (anytime unless noted).
172
+ setAdcChannel(peripheral: string, channel: number, value: number): void;
173
+ clearAdcChannel(peripheral: string, channel: number): void;
174
+ takeAdcDma(): number[];
175
+ canInject(id: number, dlc: number, data: Uint8Array | number[]): void;
176
+ canInjectFd(id: number, data: Uint8Array | number[], brs?: boolean): void;
177
+ timInjectCapture(timer: number, ch: number): void;
178
+ timPwmPulseUs(timer: number, ch: number, clockHz: number): number;
179
+ timOcMode(timer: number, ch: number): number;
180
+ timMoe(timer: number): boolean;
181
+ timEncoderStep(timer: number, ti: number, rising: boolean): void;
182
+ timBreakInput(timer: number, asserted: boolean): void;
183
+ dacTrigger(ch: number, src: number, dmaStaged?: boolean): void;
184
+ dacUnderrun(ch: number): boolean;
185
+ takeSpeakerSamples(): Float32Array;
186
+ usbInjectSetup(bytes: Uint8Array | number[]): boolean;
187
+ usbInjectOut(ep: number, bytes: Uint8Array | number[]): boolean;
188
+ usbTakeIn(ep: number): number[];
189
+ usbReset(): void;
190
+ usbEnumerated(): void;
191
+ usbHsInjectSetup(bytes: Uint8Array | number[]): boolean;
192
+ usbHsInjectOut(ep: number, bytes: Uint8Array | number[]): boolean;
193
+ usbHsTakeIn(ep: number): number[];
194
+ usbHsReset(): void;
195
+ usbHsEnumerated(): void;
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;
250
+ // Fault / model-harness methods (mirror wasm exports 1:1).
251
+ uartSetCts(base: number, asserted: boolean): void;
252
+ uartFaultRx(base: number, fe: boolean, pe: boolean): void;
253
+ uartIdle(base: number): void;
254
+ uartLinBreak(base: number): void;
255
+ uartBreakPending(base: number): boolean;
256
+ uartTxLen(base: number): number;
257
+ uartMuted(base: number): boolean;
258
+ spiFaultCrc(base: number): void;
259
+ spiFaultModf(base: number): void;
260
+ spiSlaveSelect(base: number, asserted: boolean): void;
261
+ spiSlaveClock(base: number, mosi: number): number;
262
+ spiSlaveGate(base: number): boolean;
263
+ i2cArmArbLoss(base: number): void;
264
+ i2cArmSmbusAlert(base: number, addr: number): void;
265
+ i2cSlaveAddress(base: number, addr: number, isRead: boolean): boolean;
266
+ i2cSlaveWrite(base: number, byte: number): void;
267
+ i2cSlaveRead(base: number): number;
268
+ i2cSlaveStop(base: number): void;
269
+ i2cSlaveStatus(base: number): number;
270
+ sdioFaultDataCrc(): void;
271
+ rccInjectFailure(srcMask: number, dead: boolean): void;
272
+ flashRdpLevel(): number;
273
+ flashSetRdp(levelByte: number): void;
274
+ rngSeedEntropy(words: number[] | Uint32Array): void;
275
+ rngEntropyAvail(): number;
276
+ rtcTamperPin(level: boolean): void;
277
+ rtcTimestamp(): void;
278
+ setIntrPending(irq: number): void;
279
+ hasPendingInterrupt(): boolean;
280
+ // Scope probes (read-only model state).
281
+ adcDualLatched(): boolean;
282
+ ethPpsCount(): number;
283
+ ethPpsLevel(): boolean;
284
+ ethLinkUp(): boolean;
285
+ ethSetLink(up: boolean): void;
286
+ ltdcScanline(): number;
287
+ ltdcFrameCount(): number;
288
+ audioRemaining(): number;
289
+ audioClear(): void;
290
+ dcmiSync(vsync: boolean, hsync: boolean, pclkDiv: number): void;
291
+ qspiMmapLive(): boolean;
292
+ qspiMmapRead(offset: number): number;
293
+ sdioBusWidth(): number;
294
+ sdioCardBlocks(): number;
295
+ sdioReadBlock(block: number): number[];
296
+ takeItm(port: number): number[];
297
+ itmPending(port: number): number;
298
+ takeFsmc(bank: number): number[];
299
+ pushFsmc(bank: number, values: number[] | Uint32Array): void;
300
+ regfileGet(peripheral: string, offset: number): number;
301
+ regfileSet(peripheral: string, offset: number, value: number): void;
302
+ get oled(): any;
303
+ get tft(): any;
304
+ get rtc(): any;
305
+ get buzzer(): any;
306
+ get camera(): any;
307
+ execute(cycles?: number): ExecuteResult;
308
+ step(cycles?: number): FacadeStepResult;
309
+ uartRx(byte: number): boolean;
310
+ get uartOutput(): string;
311
+ setSymbols(mapText: string): number;
312
+ resolveSymbol(pc: number): string | null;
313
+ read32(addr: number): number;
314
+ write32(addr: number, val: number): void;
315
+ memRead32(addr: number): number;
316
+ memWriteBytes(addr: number, bytes: Uint8Array | number[]): void;
317
+ periphRead(addr: number, width?: number): number;
318
+ periphWrite(addr: number, width: number, value: number): void;
319
+ addJsPeripheral(base: number, size: number, read: (addr: number, size: number) => number, write: (addr: number, value: number, size: number) => void): boolean;
320
+ pwrMode(): number;
321
+ pwrEstimate(): number;
322
+ swdHalted(): boolean;
323
+ swdHalt(): void;
324
+ swdResume(): void;
325
+ swdStep(): number;
326
+ swdAddWatch(kind: number, addr: number, len: number): number;
327
+ swdRemoveWatch(slot: number): void;
328
+ swdTakeTrip(): number[];
329
+ swdDpRead(addr: number): number;
330
+ swdDpWrite(addr: number, value: number): void;
331
+ swdApRead(bank: number, reg: number): number;
332
+ swdApWrite(bank: number, reg: number, value: number): void;
333
+ swdRegRead(idx: number): number;
334
+ swdRegWrite(idx: number, value: number): void;
335
+ jtagReset(): void;
336
+ jtagIr(ir: number): void;
337
+ jtagIdcode(): number;
338
+ jtagDp(addr: number, rnw: boolean, wdata: number): number;
339
+ jtagAp(bank: number, reg: number, rnw: boolean, wdata: number): number;
340
+ dmaController(ctl?: 1 | 2): DMAController;
341
+ getRegisters(): any;
342
+ getPc(): number;
343
+ getSp(): number;
344
+ faultInfo(): any;
345
+ takeFault(): [number, number] | null;
346
+ stop(): void;
347
+ reset(): void;
348
+ close(): void;
349
+ resetCpu(): void;
350
+ setNrst(asserted: boolean): boolean;
351
+ isNrstAsserted(): boolean;
352
+ bootPreset(image: any): void;
353
+ ledStatus(fwName: string, boardKey: string): any;
354
+ }
22
355
  export class LED {
23
356
  constructor(emu: any, port: string, num: number, opts?: { activeLow?: boolean });
24
357
  on(): void;
@@ -46,7 +379,59 @@ export class Potentiometer {
46
379
  export class I2cRegisterDevice {
47
380
  constructor(emu: any, peripheral: string, opts?: any);
48
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
+ }
49
433
 
434
+ // Component-attachment API (attach devices to an emulator handle).
50
435
  // Board LED map + host reset/boot control (see site/boards.js,
51
436
  // site/emulator.js, site/stm32f4.js).
52
437
  export interface BoardLed { bank: number; pin: number; label: string; }
package/index.mjs CHANGED
@@ -6,14 +6,18 @@ 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';
10
- import { STM32F4, GPIOPin, USART, DMAStream } from './site/stm32f4.js';
9
+ import { LED, Button, Pwm, Servo, I2cRegisterDevice, Potentiometer, CameraSensor, DacLoad, RngNoise, I2cPeer, UsbLink, UlpiMeter, UartBaud, NandEcc } from './site/components.js';
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
13
13
  // `firmware` option is optional (defer to loadBin/loadHex/loadELF after
14
14
  // create), matching the rp2040js / avr8js ergonomics.
15
+ // NOTE: svdXml is deliberately NOT injected here. STM32F4._create resolves
16
+ // the SVD per `chip` (F401/F411/F407/F429) itself; injecting the F407 default
17
+ // would shadow the chip map (right sizes + IDCODE, wrong registers). A caller
18
+ // passing svdXml explicitly still wins (override path in _create).
15
19
  STM32F4.create = (opts = {}) => STM32F4._create({
16
- bindings, svdXml, wasmInit: wasmBytes, ...opts,
20
+ bindings, wasmInit: wasmBytes, ...opts,
17
21
  });
18
22
 
19
23
  const svdXml = readFileSync(new URL('./site/vendor/stm32f407.svd', import.meta.url), 'utf8');
@@ -28,16 +32,28 @@ export function decodeFirmware(key) {
28
32
  return Uint8Array.from(bin, c => c.charCodeAt(0));
29
33
  }
30
34
 
31
- // Convenience: create an emulator with the bundled STM32F407 assets.
35
+ // Convenience: create an emulator with the bundled assets.
32
36
  // await createSTM32F407({ firmware }) // firmware: Uint8Array
33
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.
34
41
  // All extra options pass through to createEmulator().
35
42
  export async function createSTM32F407(opts = {}) {
36
- const { firmware } = opts;
43
+ const { firmware, chip = 'stm32f407' } = opts;
37
44
  const bin = typeof firmware === 'string' ? decodeFirmware(firmware) : firmware;
38
45
  if (!bin) throw new Error('createSTM32F407 requires `firmware` (Uint8Array or a FIRMWARES key)');
39
- 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
+ });
40
56
  }
41
57
 
42
- export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, USART, DMAStream };
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 };
43
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.2.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",
@@ -104,7 +104,7 @@
104
104
  "lint": "eslint site mcp tools index.mjs cli.mjs",
105
105
  "format": "prettier --check .",
106
106
  "format:write": "prettier --write .",
107
- "build:wasm": "wasm-pack build stm32-periph-wasm --target web --out-dir ../site/vendor && rm -f site/vendor/.gitignore && cp monox/stm32f407.svd site/vendor/ 2>/dev/null || true; command -v wasm-opt >/dev/null 2>&1 && wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm || echo 'wasm-opt not found, skipping'",
107
+ "build:wasm": "wasm-pack build stm32-periph-wasm --target web --out-dir ../site/vendor && rm -f site/vendor/.gitignore && git checkout -- site/vendor/stm32f407.svd site/vendor/stm32f401.svd site/vendor/stm32f411.svd site/vendor/stm32f429.svd 2>/dev/null || true; command -v wasm-opt >/dev/null 2>&1 && wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm || echo 'wasm-opt not found, skipping'",
108
108
  "build:wasm:node": "wasm-pack build stm32-periph-wasm --target nodejs --out-dir pkg",
109
109
  "build:wasm:opt": "wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm",
110
110
  "test:wasm": "node site/test_wasm_cpu.mjs && node site/test_flow_wasm.mjs && node site/test_audio_wasm.mjs && node site/test_wasm_multi.mjs && node site/test_lowpower_wasm.mjs && node site/probe_freertos.mjs && node site/test_doom_wasm.mjs"