stm32f4-emu 1.2.1 → 1.3.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/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/index.d.ts CHANGED
@@ -18,7 +18,287 @@ export const bindings: any;
18
18
  export const unicornFactory: any;
19
19
  export const svdXml: string;
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
+ // Fault / model-harness methods (mirror wasm exports 1:1).
198
+ uartSetCts(base: number, asserted: boolean): void;
199
+ uartFaultRx(base: number, fe: boolean, pe: boolean): void;
200
+ uartIdle(base: number): void;
201
+ uartLinBreak(base: number): void;
202
+ uartBreakPending(base: number): boolean;
203
+ uartTxLen(base: number): number;
204
+ uartMuted(base: number): boolean;
205
+ spiFaultCrc(base: number): void;
206
+ spiFaultModf(base: number): void;
207
+ spiSlaveSelect(base: number, asserted: boolean): void;
208
+ spiSlaveClock(base: number, mosi: number): number;
209
+ spiSlaveGate(base: number): boolean;
210
+ i2cArmArbLoss(base: number): void;
211
+ i2cArmSmbusAlert(base: number, addr: number): void;
212
+ i2cSlaveAddress(base: number, addr: number, isRead: boolean): boolean;
213
+ i2cSlaveWrite(base: number, byte: number): void;
214
+ i2cSlaveRead(base: number): number;
215
+ i2cSlaveStop(base: number): void;
216
+ i2cSlaveStatus(base: number): number;
217
+ sdioFaultDataCrc(): void;
218
+ rccInjectFailure(srcMask: number, dead: boolean): void;
219
+ flashRdpLevel(): number;
220
+ flashSetRdp(levelByte: number): void;
221
+ rngSeedEntropy(words: number[] | Uint32Array): void;
222
+ rngEntropyAvail(): number;
223
+ rtcTamperPin(level: boolean): void;
224
+ rtcTimestamp(): void;
225
+ setIntrPending(irq: number): void;
226
+ hasPendingInterrupt(): boolean;
227
+ // Scope probes (read-only model state).
228
+ adcDualLatched(): boolean;
229
+ ethPpsCount(): number;
230
+ ethPpsLevel(): boolean;
231
+ ethLinkUp(): boolean;
232
+ ethSetLink(up: boolean): void;
233
+ ltdcScanline(): number;
234
+ ltdcFrameCount(): number;
235
+ audioRemaining(): number;
236
+ audioClear(): void;
237
+ dcmiSync(vsync: boolean, hsync: boolean, pclkDiv: number): void;
238
+ qspiMmapLive(): boolean;
239
+ qspiMmapRead(offset: number): number;
240
+ sdioBusWidth(): number;
241
+ sdioCardBlocks(): number;
242
+ sdioReadBlock(block: number): number[];
243
+ takeItm(port: number): number[];
244
+ itmPending(port: number): number;
245
+ takeFsmc(bank: number): number[];
246
+ pushFsmc(bank: number, values: number[] | Uint32Array): void;
247
+ regfileGet(peripheral: string, offset: number): number;
248
+ regfileSet(peripheral: string, offset: number, value: number): void;
249
+ get oled(): any;
250
+ get tft(): any;
251
+ get rtc(): any;
252
+ get buzzer(): any;
253
+ get camera(): any;
254
+ execute(cycles?: number): ExecuteResult;
255
+ step(cycles?: number): FacadeStepResult;
256
+ uartRx(byte: number): boolean;
257
+ get uartOutput(): string;
258
+ setSymbols(mapText: string): number;
259
+ resolveSymbol(pc: number): string | null;
260
+ read32(addr: number): number;
261
+ write32(addr: number, val: number): void;
262
+ memRead32(addr: number): number;
263
+ memWriteBytes(addr: number, bytes: Uint8Array | number[]): void;
264
+ periphRead(addr: number, width?: number): number;
265
+ periphWrite(addr: number, width: number, value: number): void;
266
+ addJsPeripheral(base: number, size: number, read: (addr: number, size: number) => number, write: (addr: number, value: number, size: number) => void): boolean;
267
+ pwrMode(): number;
268
+ pwrEstimate(): number;
269
+ swdHalted(): boolean;
270
+ swdHalt(): void;
271
+ swdResume(): void;
272
+ swdStep(): number;
273
+ swdAddWatch(kind: number, addr: number, len: number): number;
274
+ swdRemoveWatch(slot: number): void;
275
+ swdTakeTrip(): number[];
276
+ swdDpRead(addr: number): number;
277
+ swdDpWrite(addr: number, value: number): void;
278
+ swdApRead(bank: number, reg: number): number;
279
+ swdApWrite(bank: number, reg: number, value: number): void;
280
+ swdRegRead(idx: number): number;
281
+ swdRegWrite(idx: number, value: number): void;
282
+ jtagReset(): void;
283
+ jtagIr(ir: number): void;
284
+ jtagIdcode(): number;
285
+ jtagDp(addr: number, rnw: boolean, wdata: number): number;
286
+ jtagAp(bank: number, reg: number, rnw: boolean, wdata: number): number;
287
+ dmaController(ctl?: 1 | 2): DMAController;
288
+ getRegisters(): any;
289
+ getPc(): number;
290
+ getSp(): number;
291
+ faultInfo(): any;
292
+ takeFault(): [number, number] | null;
293
+ stop(): void;
294
+ reset(): void;
295
+ close(): void;
296
+ resetCpu(): void;
297
+ setNrst(asserted: boolean): boolean;
298
+ isNrstAsserted(): boolean;
299
+ bootPreset(image: any): void;
300
+ ledStatus(fwName: string, boardKey: string): any;
301
+ }
22
302
  export class LED {
23
303
  constructor(emu: any, port: string, num: number, opts?: { activeLow?: boolean });
24
304
  on(): void;
@@ -47,6 +327,7 @@ export class I2cRegisterDevice {
47
327
  constructor(emu: any, peripheral: string, opts?: any);
48
328
  }
49
329
 
330
+ // Component-attachment API (attach devices to an emulator handle).
50
331
  // Board LED map + host reset/boot control (see site/boards.js,
51
332
  // site/emulator.js, site/stm32f4.js).
52
333
  export interface BoardLed { bank: number; pin: number; label: string; }
package/index.mjs CHANGED
@@ -7,13 +7,17 @@ import { createEmulator } from './site/emulator.js';
7
7
  import { createNetSim } from './site/netsim.js';
8
8
  import { FIRMWARES } from './site/firmware.js';
9
9
  import { LED, Button, Pwm, I2cRegisterDevice, Potentiometer } from './site/components.js';
10
- import { STM32F4, GPIOPin, USART, DMAStream } from './site/stm32f4.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');
@@ -39,5 +43,5 @@ export async function createSTM32F407(opts = {}) {
39
43
  return createEmulator({ ...opts, firmware: bin, bindings, svdXml, wasmInit: wasmBytes });
40
44
  }
41
45
 
42
- export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, USART, DMAStream };
46
+ export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, GPIO, USART, SPI, I2C, DMAStream, DMAController, Display, CHIPS, chipInfo };
43
47
  export { boardLed, BOARD_LED, BOARD_LED_ALIASES } from './site/boards.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stm32f4-emu",
3
- "version": "1.2.1",
3
+ "version": "1.3.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",
@@ -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"
package/site/app.js CHANGED
@@ -2,8 +2,8 @@
2
2
  // Preset + custom (.bin/.hex/.elf/.map) firmware loading, Run/Stop/Reset,
3
3
  // an optional WebSocket gateway (real network stack) with a netsim fallback,
4
4
  // live UART terminal, GPIO/peripheral register readout, and packet viewer.
5
- import * as bindings from './vendor/stm32_periph_wasm.js?v=41';
6
- import { createEmulator } from './emulator.js?v=2';
5
+ import * as bindings from './vendor/stm32_periph_wasm.js?v=43';
6
+ import { createEmulator } from './emulator.js?v=3';
7
7
  import { createNetSim } from './netsim.js';
8
8
  import { createUsbHost } from './usbhost.js';
9
9
  import { boardsOf, boardForSelection, BOARDS, boardLed } from './boards.js?v=7';
@@ -142,6 +142,9 @@ const DEVICE_FIRMWARES = {
142
142
  buzzer_test_f401: { buzzer: { tim: 'TIM2' } },
143
143
  buzzer_test_f411: { buzzer: { tim: 'TIM2' } },
144
144
  buzzer_test_f429: { buzzer: { tim: 'TIM2' } },
145
+ audio_play_test_f401: { speaker: true },
146
+ audio_play_test_f411: { speaker: true },
147
+ audio_play_test_f429: { speaker: true },
145
148
  rtc_test_f401: { rtc: { i2c: 'I2C1', addr: 0x68, init: RTC_INIT } },
146
149
  rtc_test_f411: { rtc: { i2c: 'I2C1', addr: 0x68, init: RTC_INIT } },
147
150
  rtc_test_f429: { rtc: { i2c: 'I2C1', addr: 0x68, init: RTC_INIT } },
@@ -481,7 +484,7 @@ const boot = async () => {
481
484
  setStatus('booting…', 'stop');
482
485
  if (emu) { try { emu.close(); } catch (e) {} emu = null; }
483
486
  oledCacheKey = ''; tftCacheKey = ''; buzzerCacheKey = ''; rtcCacheKey = ''; ppsCacheKey = ''; TRACES.length = 0; { const cv = traceCanvas(); if (cv) cv._dma = []; } renderTraces();
484
- if (audioCtx) { try { audioCtx.close(); } catch (e) {} audioCtx = null; audioQueued = 0; }
487
+ if (audioCtx) { try { audioCtx.close(); } catch (e) {} audioCtx = null; audioQueued = 0; audioDropped = 0; if (window.__audioCtx) delete window.__audioCtx; }
485
488
  gpioDrivenHigh.clear();
486
489
 
487
490
  const fw = image.flash;
@@ -546,19 +549,24 @@ const boot = async () => {
546
549
  // ── local mode: WASM runs in the browser (default) ──
547
550
  // Board variant per firmware preset (SVD + flash/RAM sizes), honoring
548
551
  // the board selector when the preset supports the selected board.
549
- // NOTE (VENDOR_V): vendor asset versions (?v=41) must be bumped together
552
+ // NOTE (VENDOR_V): vendor asset versions (?v=43) must be bumped together
550
553
  // after every wasm-pack rebuild, or browsers keep the stale model.
551
554
  const { key: boardKey, board } = boardForSelection(image.name, boardSelectEl ? boardSelectEl.value : 'all');
552
- const svdXml = await fetch('vendor/' + board.svd + '?v=41').then((r) => r.text());
555
+ const svdXml = await fetch('vendor/' + board.svd + '?v=43').then((r) => r.text());
553
556
  if (id !== session) return;
554
557
 
555
558
  netsim = gw.connected ? null : createNetSim();
556
559
  // Scripted USB host when the demo preset boots locally.
557
560
  usbhost = (!bridgeUrl && image.name.startsWith('usb_cdc_test')) ? createUsbHost(bindings) : null;
558
- // Harness arms for eth_feat_test (local mode only): the guest
559
- // prints markers and spins, the page arms collisions + link (same
560
- // mechanism as the node matrix hooks).
561
- featHooks = (!bridgeUrl && image.name.startsWith('eth_feat_test') && bindings && typeof bindings.eth_arm_collision === 'function')
561
+ // Harness arms for collision tests (local mode only): the guest
562
+ // prints COLLIDE ARM and spins, the page arms the one-shot
563
+ // collision + link hooks (same mechanism as the node matrix).
564
+ // Covers eth_feat_test AND eth_pins_test (its COL phase needs the
565
+ // same arm-before-TX timing; without it PINS COL FAILs while the
566
+ // node harness passes).
567
+ featHooks = (!bridgeUrl && bindings && typeof bindings.eth_arm_collision === 'function'
568
+ && (image.name.startsWith('eth_feat_test') || image.name.startsWith('eth_pins_test'))
569
+ )
562
570
  ? { collideDone: 0, linkDown: false, linkUp: false } : null;
563
571
  gw.tx = 0; gw.rx = 0;
564
572
  if (gw.connected) setGwStatus(true, gwLabel());
@@ -569,7 +577,7 @@ const boot = async () => {
569
577
  svdFile: board.svd,
570
578
  flash_size: board.flash_size,
571
579
  ram_size: board.ram_size,
572
- wasmUrl: 'vendor/stm32_periph_wasm_bg.wasm?v=41',
580
+ wasmUrl: 'vendor/stm32_periph_wasm_bg.wasm?v=43',
573
581
  extra_mem: image.extraMem,
574
582
  uart_addr: image.uartAddr,
575
583
  enable_irqs: IRQ_FIRMWARES.has(image.name),
@@ -634,13 +642,14 @@ const loop = async (id) => {
634
642
  while (session === id) {
635
643
  if (!running) { await raf(); continue; }
636
644
  try {
637
- // eth_feat_test/eth_adv run at 20k-inst steps: their wire-rate
638
- // bands and race windows are calibrated for fine steps (the
639
- // node matrix uses 5k-20k); 100k steps would overshoot every
640
- // band (observed: eth_adv FRAG FAILs at 100k — both fragments
641
- // consumed before the reassembly loop polls — while 20k passes
642
- // 13/13; the node matrix runs both at 20k).
643
- const fineSteps = image && (image.name.startsWith('eth_feat_test') || image.name.startsWith('eth_adv'));
645
+ // eth_feat_test/eth_adv/eth_pins_test run at 20k-inst steps:
646
+ // their wire-rate bands and race windows are calibrated for
647
+ // fine steps (the node matrix uses 5k-20k); 100k steps would
648
+ // overshoot every band (observed: eth_adv FRAG FAILs at 100k —
649
+ // both fragments consumed before the reassembly loop polls —
650
+ // while 20k passes 13/13; eth_pins_test COL needs the arm to
651
+ // land inside the TX window; the node matrix runs all at 20k).
652
+ const fineSteps = image && (image.name.startsWith('eth_feat_test') || image.name.startsWith('eth_adv') || image.name.startsWith('eth_pins_test'));
644
653
  const res = await emu.step(fineSteps ? 20000 : undefined);
645
654
  totalInst = res.instCount;
646
655
  stepsDone++;
@@ -663,7 +672,7 @@ const loop = async (id) => {
663
672
  try {
664
673
  if (!bridgeUrl && image && image.name.startsWith('dcmi_test') && bindings.dcmi_feed_frame) driveDcmi();
665
674
  } catch (e) {}
666
- // Harness arms for eth_feat_test (local mode only).
675
+ // Harness arms for collision tests (local mode only; see boot).
667
676
  try {
668
677
  if (featHooks) {
669
678
  const arms = uartBuf.split('COLLIDE ARM').length - 1;
@@ -1194,6 +1203,13 @@ const renderBuzzer = () => {
1194
1203
 
1195
1204
  const speakerInfo = $('speakerInfo');
1196
1205
  let audioCtx = null, audioNextTime = 0, audioQueued = 0;
1206
+ // Cap on scheduled-ahead audio: the guest produces faster than realtime
1207
+ // (a tone loop emits unbounded samples per second of wall time), so an
1208
+ // uncapped scheduler piles up hundreds of seconds of BufferSources —
1209
+ // audible only after a huge delay, plus unbounded graph/memory growth.
1210
+ // MAX_AHEAD keeps ~2 s scheduled; excess samples are dropped (counted).
1211
+ const AUDIO_MAX_AHEAD_S = 2.0;
1212
+ let audioDropped = 0;
1197
1213
  const renderSpeaker = () => {
1198
1214
  if (!emu || !emu.takeSpeakerSamples) { speakerInfo.textContent = 'no audio firmware'; return; }
1199
1215
  const samples = emu.takeSpeakerSamples();
@@ -1201,12 +1217,19 @@ const renderSpeaker = () => {
1201
1217
  if (!audioCtx) {
1202
1218
  try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
1203
1219
  if (!audioCtx) { speakerInfo.textContent = 'WebAudio unavailable'; return; }
1204
- audioNextTime = audioCtx.currentTime + 0.05;
1220
+ audioNextTime = Math.max(audioNextTime, audioCtx.currentTime + 0.05);
1221
+ window.__audioCtx = audioCtx;
1205
1222
  }
1206
1223
  if (audioCtx.state === 'suspended') audioCtx.resume();
1224
+ // Rebase late schedulers (tab was hidden / long pause): never schedule
1225
+ // in the past, never more than MAX_AHEAD ahead of now.
1226
+ audioNextTime = Math.max(audioNextTime, audioCtx.currentTime + 0.02);
1207
1227
  let off = 0;
1208
1228
  while (off < samples.length) {
1209
- const n = Math.min(4096, samples.length - off);
1229
+ const headroom = AUDIO_MAX_AHEAD_S - (audioNextTime - audioCtx.currentTime);
1230
+ if (headroom <= 0) { audioDropped += samples.length - off; break; }
1231
+ const maxN = Math.floor(headroom * audioCtx.sampleRate);
1232
+ const n = Math.min(4096, samples.length - off, Math.max(1, maxN));
1210
1233
  const buf = audioCtx.createBuffer(1, n, audioCtx.sampleRate);
1211
1234
  const ch = buf.getChannelData(0);
1212
1235
  for (let i = 0; i < n; i++) ch[i] = samples[off + i];
@@ -1218,7 +1241,10 @@ const renderSpeaker = () => {
1218
1241
  off += n;
1219
1242
  audioQueued += n;
1220
1243
  }
1221
- speakerInfo.textContent = `I2S capture playing — ${(audioQueued / audioCtx.sampleRate).toFixed(1)} s queued`;
1244
+ window.__audioQueued = audioQueued;
1245
+ window.__audioDropped = audioDropped;
1246
+ const ahead = Math.max(0, audioNextTime - audioCtx.currentTime);
1247
+ speakerInfo.textContent = `I2S capture playing — ${(audioQueued / audioCtx.sampleRate).toFixed(1)} s queued (${ahead.toFixed(1)} s ahead${audioDropped ? `, ${audioDropped} dropped` : ''})`;
1222
1248
  };
1223
1249
 
1224
1250
  let rtcCacheKey = '';
package/site/console.html CHANGED
@@ -957,7 +957,7 @@ canvas {
957
957
  · <a href="about.html">About</a>
958
958
  </footer>
959
959
 
960
- <script type="module" src="app.js?v=50"></script>
960
+ <script type="module" src="app.js?v=52"></script>
961
961
  <script>
962
962
  // Custom firmware dropdown (replaces native <select>)
963
963
  (() => {
package/site/emulator.js CHANGED
@@ -461,6 +461,10 @@ export async function createEmulator(opts) {
461
461
  hint = String(opts.chipHint || opts.svdFile || '');
462
462
  hint = hint.split('/').pop().split('.').slice(0, -1).join('.') || hint;
463
463
  } catch {}
464
+ // compat[0] may carry no SVD filename (older callers pass none):
465
+ // default to the F407 identity rather than an empty hint that
466
+ // leaves IDCODE at its reset value (comprehensive_test pins 0x413).
467
+ if (!hint) hint = 'stm32f407';
464
468
  init_svd_chip(svdXml, hint);
465
469
  } else init_svd(svdXml);
466
470
  }