stm32f4-emu 1.0.0 → 1.1.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 +17 -16
- package/cli.mjs +11 -6
- package/index.d.ts +48 -0
- package/index.mjs +6 -11
- package/package.json +30 -13
- package/site/app.js +112 -33
- package/site/emulator.d.ts +81 -0
- package/site/emulator.js +275 -841
- package/site/firmware.js +6 -2
- package/site/index.html +13 -9
- package/site/netsim.js +15 -17
- package/site/remote-emu.js +20 -2
- package/site/stm32f4.js +276 -0
- package/site/vendor/stm32_periph_wasm.d.ts +171 -19
- package/site/vendor/stm32_periph_wasm.js +552 -101
- package/site/vendor/stm32_periph_wasm_bg.wasm +0 -0
- package/site/vendor/stm32_periph_wasm_bg.wasm.d.ts +53 -16
- package/site/ws-bridge.mjs +126 -14
- package/tools/make_firmware.mjs +79 -0
- package/site/vendor/unicorn_arm.cjs +0 -0
- package/site/vendor/unicorn_arm.js +0 -0
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
[](https://github.com/danish9661/stm32F4-emulator/actions/workflows/pages.yml)
|
|
8
8
|
|
|
9
9
|
An STM32F407 microcontroller emulator that runs real Cortex-M4 firmware. It
|
|
10
|
-
combines a **
|
|
10
|
+
combines a **Rust CPU core** (a WASM-native Thumb-2 interpreter with exact
|
|
11
|
+
Cortex-M exception entry/return, including the VFPv4-SP FPU) with a
|
|
11
12
|
**Rust peripheral model** (RCC, USART, GPIO, DMA, ETH, TIM, NVIC, ...) also
|
|
12
13
|
compiled to WASM — so the whole machine runs headless in **Node.js or a
|
|
13
14
|
browser tab**, with no SDL, no native deps, no hardware.
|
|
@@ -32,18 +33,18 @@ scripted network (netsim) as fallback, a live UART terminal (with **UART
|
|
|
32
33
|
RX input** — type into the console and the firmware reads it; newline
|
|
33
34
|
characters excluded per HTML spec, see AGENTS.md §11), GPIO pin readout
|
|
34
35
|
for banks A–E, and key peripheral registers. Interrupt-driven firmware
|
|
35
|
-
(`rx_interrupt_test`, `rx_crypto_test`)
|
|
36
|
-
|
|
36
|
+
(`rx_interrupt_test`, `rx_crypto_test`) gets inline guest-IRQ delivery;
|
|
37
|
+
polling firmware (the ETH demos) never uses it. For automation, a
|
|
37
38
|
preset can auto-boot via the URL: `?fw=eth_http`, `?fw=blinky`, `?fw=crypto_test`, …
|
|
38
39
|
|
|
39
40
|
## DOOM (in the browser)
|
|
40
41
|
|
|
41
42
|
**[`site/doom.html`](site/doom.html)** runs DOOM 1 shareware
|
|
42
|
-
(doomgeneric, ported to the emulated F407) at
|
|
43
|
-
headless-Chrome-verified browser page — playable
|
|
44
|
-
35 fps
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
(doomgeneric, ported to the emulated F407) at the full 35 fps in a
|
|
44
|
+
headless-Chrome-verified browser page — playable: at ~1M guest instructions
|
|
45
|
+
per rendered frame, 35 fps needs ~32 MIPS and the Rust core delivers ~65
|
|
46
|
+
(details and the measurements in AGENTS.md §16/§22). Because the guest mixes
|
|
47
|
+
one frame of audio
|
|
47
48
|
per rendered frame, sound below 35 fps plays slightly slow and pitched-down
|
|
48
49
|
rather than breaking up — the worklet rate-matches instead of inserting
|
|
49
50
|
gaps, and the stats line reports it (`audio 0.72x`). The page: 320×200
|
|
@@ -182,9 +183,9 @@ watchdog timeouts close to spec) will diverge — see docs/progress-and-future.m
|
|
|
182
183
|
## Architecture
|
|
183
184
|
|
|
184
185
|
```
|
|
185
|
-
firmware .bin ──►
|
|
186
|
+
firmware .bin ──► Rust WASM CPU (Thumb-2 + NVIC/exceptions)
|
|
186
187
|
│
|
|
187
|
-
|
|
188
|
+
peripheral MMIO ──► Rust peripheral model (WASM)
|
|
188
189
|
RCC USART GPIO DMA ETH TIM NVIC
|
|
189
190
|
│
|
|
190
191
|
UART out / ETH TX frames ──► driver (cli.mjs / site/emulator.js)
|
|
@@ -192,9 +193,10 @@ firmware .bin ──► Unicorn WASM CPU ──► memory hooks
|
|
|
192
193
|
RX frames injected (netsim, or real gVisor gateway)
|
|
193
194
|
```
|
|
194
195
|
|
|
195
|
-
- **CPU**:
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
- **CPU**: a pure-Rust Cortex-M4 Thumb-2 interpreter compiled to WASM;
|
|
197
|
+
peripheral accesses call straight into the Rust model, and guest IRQs
|
|
198
|
+
(SysTick, ETH, USART, SVC/PendSV) are delivered inline with exact
|
|
199
|
+
exception stacking — no native deps, no JIT, no hooks.
|
|
198
200
|
- **Peripherals**: a `wasm-bindgen` crate (`stm32-periph-wasm/`); registers
|
|
199
201
|
and bit fields come from the vendor SVD (`monox/stm32f407.svd`).
|
|
200
202
|
- **Ethernet**: TX is captured from the DMA descriptors; RX frames are
|
|
@@ -223,8 +225,8 @@ firmware .bin ──► Unicorn WASM CPU ──► memory hooks
|
|
|
223
225
|
│ ├── test_blinky.mjs Node blinky GPIO test (npm test)
|
|
224
226
|
│ ├── test_rx_interrupt.mjs Node UART-interrupt test (npm test)
|
|
225
227
|
│ ├── test_component_*.mjs Component-API tests, one firmware each (npm test)
|
|
226
|
-
│ ├──
|
|
227
|
-
│ └── vendor/ Browser WASM build, SVD
|
|
228
|
+
│ ├── test_doom_wasm.mjs Node DOOM boot/menu/gameplay/save test
|
|
229
|
+
│ └── vendor/ Browser WASM build (CPU + peripherals), SVD
|
|
228
230
|
├── index.mjs, package.json npm package entry (stm32f4-emu)
|
|
229
231
|
├── mcp/ MCP server (drive the emulator from an AI agent)
|
|
230
232
|
├── .github/workflows/ CI (Linux/Windows/macOS test matrix) + Pages deploy
|
|
@@ -296,6 +298,5 @@ rebuild — delete it so the vendor assets stay tracked/committed.
|
|
|
296
298
|
## License & Credits
|
|
297
299
|
|
|
298
300
|
- **License**: GPL-3.0-only. See [LICENSE](LICENSE).
|
|
299
|
-
- **Unicorn CPU Core**: Powered by [Unicorn.js](https://github.com/AlexAltea/unicorn.js) by [Alex Altea](https://github.com/AlexAltea) (WASM/JS port of the [Unicorn Engine](https://www.unicorn-engine.org/) CPU emulator, derived from QEMU, licensed under GPLv2).
|
|
300
301
|
- **Heritage**: Fork and continuation of [nviennot/stm32-emulator](https://github.com/nviennot/stm32-emulator) (native SDL 3D printer emulator by Nicolas Viennot). The headless WASM peripheral model, networking stack, browser demo, virtual components API, MCP server, and npm package are new work built on that base.
|
|
301
302
|
- **DOOM**: Ported using [doomgeneric](https://github.com/ozkl/doomgeneric) by Ozkan Sezgin.
|
package/cli.mjs
CHANGED
|
@@ -10,15 +10,12 @@
|
|
|
10
10
|
// emulator advances the virtual RTC until a wakeup source (e.g. an RTC alarm)
|
|
11
11
|
// fires — so firmware that enters STOP (e.g. deep_sleep_demo.bin) runs.
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
|
-
import { createRequire } from 'node:module';
|
|
14
13
|
import { fileURLToPath } from 'node:url';
|
|
15
14
|
import { dirname, resolve } from 'node:path';
|
|
16
15
|
import * as bindings from './site/vendor/stm32_periph_wasm.js';
|
|
17
16
|
import { createEmulator } from './site/emulator.js';
|
|
18
17
|
import { parseIntelHex, parseElf } from './site/loaders.js';
|
|
19
18
|
|
|
20
|
-
const require = createRequire(import.meta.url);
|
|
21
|
-
const unicornFactory = require('./site/vendor/unicorn_arm.cjs');
|
|
22
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
20
|
const svdXml = readFileSync(resolve(__dirname, 'site/vendor/stm32f407.svd'), 'utf8');
|
|
24
21
|
const wasmBytes = new Uint8Array(readFileSync(resolve(__dirname, 'site/vendor/stm32_periph_wasm_bg.wasm')));
|
|
@@ -35,7 +32,7 @@ Arguments:
|
|
|
35
32
|
Options:
|
|
36
33
|
-n, --inst <N> instruction budget to run (default 20000000)
|
|
37
34
|
-f, --format <fmt> firmware format: auto|bin|hex|elf (default auto)
|
|
38
|
-
-v, --verbose trace
|
|
35
|
+
-v, --verbose trace guest PCs to stderr (capped per step)
|
|
39
36
|
-l, --lowpower halt on WFI/WFE and advance the virtual RTC until wakeup
|
|
40
37
|
-h, --help show this help
|
|
41
38
|
-V, --version show version
|
|
@@ -119,8 +116,8 @@ async function main() {
|
|
|
119
116
|
let emu;
|
|
120
117
|
try {
|
|
121
118
|
emu = await createEmulator({
|
|
122
|
-
firmware, bindings,
|
|
123
|
-
extra_mem,
|
|
119
|
+
firmware, bindings, svdXml, wasmInit: wasmBytes,
|
|
120
|
+
extra_mem, lowpower,
|
|
124
121
|
});
|
|
125
122
|
} catch (e) {
|
|
126
123
|
fail(`emulator failed to load firmware: ${e.message}`);
|
|
@@ -132,7 +129,15 @@ async function main() {
|
|
|
132
129
|
try {
|
|
133
130
|
while (remaining > 0) {
|
|
134
131
|
const take = Math.min(STEP, remaining);
|
|
132
|
+
if (verbose) { try { emu.traceStart(); } catch {} }
|
|
135
133
|
const r = emu.step(take);
|
|
134
|
+
if (verbose) {
|
|
135
|
+
try {
|
|
136
|
+
const pcs = emu.takeTrace() || [];
|
|
137
|
+
const tail = pcs.slice(-8).map((p) => `0x${(p >>> 0).toString(16)}`).join(' ');
|
|
138
|
+
if (tail) process.stderr.write(`[trace]${tail}\n`);
|
|
139
|
+
} catch {} finally { try { emu.traceStop(); } catch {} }
|
|
140
|
+
}
|
|
136
141
|
remaining -= (r.instCount - lastInst);
|
|
137
142
|
lastInst = r.instCount;
|
|
138
143
|
const u = emu.drainUart();
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { createEmulator, CreateEmulatorOpts, EmulatorHandle } from './site/emulator.js';
|
|
4
|
+
|
|
5
|
+
export * from './site/emulator.js';
|
|
6
|
+
|
|
7
|
+
export interface CreateSTM32F407Opts extends CreateEmulatorOpts {
|
|
8
|
+
/** A Uint8Array, or a key into the bundled FIRMWARES table. */
|
|
9
|
+
firmware: Uint8Array | string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createSTM32F407(opts?: CreateSTM32F407Opts): Promise<EmulatorHandle>;
|
|
13
|
+
export function decodeFirmware(key: string): Uint8Array;
|
|
14
|
+
export function createNetSim(opts?: any): any;
|
|
15
|
+
|
|
16
|
+
export const FIRMWARES: Record<string, { bytes: string; [key: string]: unknown }>;
|
|
17
|
+
export const bindings: any;
|
|
18
|
+
export const unicornFactory: any;
|
|
19
|
+
export const svdXml: string;
|
|
20
|
+
|
|
21
|
+
// Component-attachment API (attach devices to an emulator handle).
|
|
22
|
+
export class LED {
|
|
23
|
+
constructor(emu: any, port: string, num: number, opts?: { activeLow?: boolean });
|
|
24
|
+
on(): void;
|
|
25
|
+
off(): void;
|
|
26
|
+
toggle(): void;
|
|
27
|
+
read(): boolean;
|
|
28
|
+
onChange(cb: (on: boolean) => void): void;
|
|
29
|
+
}
|
|
30
|
+
export class Button {
|
|
31
|
+
constructor(emu: any, port: string, num: number, opts?: { activeLow?: boolean });
|
|
32
|
+
press(): void;
|
|
33
|
+
release(): void;
|
|
34
|
+
read(): boolean;
|
|
35
|
+
on(evt: 'down' | 'up', cb: () => void): void;
|
|
36
|
+
}
|
|
37
|
+
export class Pwm {
|
|
38
|
+
constructor(emu: any, timer: string, channel?: number, opts?: { clockHz?: number });
|
|
39
|
+
setDuty(percent: number): void;
|
|
40
|
+
read(): number;
|
|
41
|
+
}
|
|
42
|
+
export class Potentiometer {
|
|
43
|
+
constructor(emu: any, peripheral: string, channel?: number, opts?: { min?: number; max?: number });
|
|
44
|
+
set(value: number): void;
|
|
45
|
+
}
|
|
46
|
+
export class I2cRegisterDevice {
|
|
47
|
+
constructor(emu: any, peripheral: string, opts?: any);
|
|
48
|
+
}
|
package/index.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// stm32f4-emu — Node API entry.
|
|
2
2
|
// Wraps the browser/Node-universal emulator.js with the bundled assets
|
|
3
|
-
// (SVD, wasm bindings
|
|
3
|
+
// (SVD, wasm bindings) so Node consumers get a one-call setup.
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
5
|
import * as bindings from './site/vendor/stm32_periph_wasm.js';
|
|
7
6
|
import { createEmulator } from './site/emulator.js';
|
|
8
7
|
import { createNetSim } from './site/netsim.js';
|
|
@@ -14,23 +13,19 @@ import { STM32F4, GPIOPin, USART, DMAStream } from './site/stm32f4.js';
|
|
|
14
13
|
// `firmware` option is optional (defer to loadBin/loadHex/loadELF after
|
|
15
14
|
// create), matching the rp2040js / avr8js ergonomics.
|
|
16
15
|
STM32F4.create = (opts = {}) => STM32F4._create({
|
|
17
|
-
bindings,
|
|
16
|
+
bindings, svdXml, wasmInit: wasmBytes, ...opts,
|
|
18
17
|
});
|
|
19
18
|
|
|
20
|
-
const require = createRequire(import.meta.url);
|
|
21
|
-
const unicornFactory = require('./site/vendor/unicorn_arm.cjs');
|
|
22
|
-
|
|
23
19
|
const svdXml = readFileSync(new URL('./site/vendor/stm32f407.svd', import.meta.url), 'utf8');
|
|
24
20
|
const wasmBytes = new Uint8Array(readFileSync(new URL('./site/vendor/stm32_periph_wasm_bg.wasm', import.meta.url)));
|
|
25
21
|
|
|
26
22
|
// Decode a base64-encoded firmware from FIRMWARES.
|
|
23
|
+
// Uses single-pass Uint8Array.from for fewer allocations than manual loop.
|
|
27
24
|
export function decodeFirmware(key) {
|
|
28
25
|
const fw = FIRMWARES[key];
|
|
29
26
|
if (!fw) throw new Error(`unknown firmware '${key}' (have: ${Object.keys(FIRMWARES).join(', ')})`);
|
|
30
27
|
const bin = atob(fw.bytes);
|
|
31
|
-
|
|
32
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
33
|
-
return out;
|
|
28
|
+
return Uint8Array.from(bin, c => c.charCodeAt(0));
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
// Convenience: create an emulator with the bundled STM32F407 assets.
|
|
@@ -41,7 +36,7 @@ export async function createSTM32F407(opts = {}) {
|
|
|
41
36
|
const { firmware } = opts;
|
|
42
37
|
const bin = typeof firmware === 'string' ? decodeFirmware(firmware) : firmware;
|
|
43
38
|
if (!bin) throw new Error('createSTM32F407 requires `firmware` (Uint8Array or a FIRMWARES key)');
|
|
44
|
-
return createEmulator({ ...opts, firmware: bin, bindings,
|
|
39
|
+
return createEmulator({ ...opts, firmware: bin, bindings, svdXml, wasmInit: wasmBytes });
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
export { createEmulator, createNetSim, FIRMWARES, bindings,
|
|
42
|
+
export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, USART, DMAStream };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stm32f4-emu",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "STM32F407 emulator:
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "STM32F407 emulator: Rust CPU + peripheral model (WASM). Runs real Cortex-M4 firmware in Node or the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
7
7
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"stm32f4-mcp": "./mcp/server.mjs",
|
|
10
10
|
"stm32f4-bridge": "./site/ws-bridge.mjs"
|
|
11
11
|
},
|
|
12
|
+
"types": "index.d.ts",
|
|
12
13
|
"exports": {
|
|
13
14
|
".": "./index.mjs",
|
|
14
15
|
"./components": "./site/components.js",
|
|
@@ -18,26 +19,29 @@
|
|
|
18
19
|
"./loaders": "./site/loaders.js",
|
|
19
20
|
"./stm32f4": "./site/stm32f4.js",
|
|
20
21
|
"./vendor": "./site/vendor/stm32_periph_wasm.js",
|
|
21
|
-
"./vendor/unicorn": "./site/vendor/unicorn_arm.cjs",
|
|
22
22
|
"./remote": "./site/remote-emu.js",
|
|
23
|
-
"./ws-bridge": "./site/ws-bridge.mjs"
|
|
24
|
-
"./site": "./site/index.html"
|
|
23
|
+
"./ws-bridge": "./site/ws-bridge.mjs"
|
|
25
24
|
},
|
|
26
25
|
"files": [
|
|
27
26
|
"index.mjs",
|
|
27
|
+
"index.d.ts",
|
|
28
28
|
"cli.mjs",
|
|
29
29
|
"README.md",
|
|
30
|
+
"LICENSE",
|
|
30
31
|
"site/emulator.js",
|
|
32
|
+
"site/emulator.d.ts",
|
|
33
|
+
"site/stm32f4.js",
|
|
31
34
|
"site/components.js",
|
|
32
|
-
"mcp/server.mjs",
|
|
33
35
|
"site/netsim.js",
|
|
34
36
|
"site/firmware.js",
|
|
35
37
|
"site/loaders.js",
|
|
36
|
-
"site/index.html",
|
|
37
|
-
"site/app.js",
|
|
38
38
|
"site/remote-emu.js",
|
|
39
39
|
"site/ws-bridge.mjs",
|
|
40
|
-
"site/
|
|
40
|
+
"site/app.js",
|
|
41
|
+
"site/index.html",
|
|
42
|
+
"site/vendor/",
|
|
43
|
+
"mcp/server.mjs",
|
|
44
|
+
"tools/make_firmware.mjs"
|
|
41
45
|
],
|
|
42
46
|
"keywords": [
|
|
43
47
|
"stm32",
|
|
@@ -45,7 +49,6 @@
|
|
|
45
49
|
"emulator",
|
|
46
50
|
"arm",
|
|
47
51
|
"cortex-m4",
|
|
48
|
-
"unicorn",
|
|
49
52
|
"wasm",
|
|
50
53
|
"embedded",
|
|
51
54
|
"firmware"
|
|
@@ -63,7 +66,7 @@
|
|
|
63
66
|
"node": ">=20"
|
|
64
67
|
},
|
|
65
68
|
"scripts": {
|
|
66
|
-
"test": "node site/test_stm32f4_api.mjs && node site/test_stm32f4_periph.mjs && node site/test_ws_bridge.mjs && node site/test_flow.mjs && node site/test_blinky.mjs && node site/test_rx_interrupt.mjs && 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_multi_instance.mjs && node site/test_fsmc_dcmi.mjs && node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/probe_freertos.mjs && node site/test_candemo.mjs && node site/test_lowpower.mjs && node site/test_edge_cases.mjs && node site/test_can_inject.mjs && node site/test_watchdog.mjs && node site/test_wwdg.mjs && node site/test_wwdg_window.mjs && node site/test_tim_capture.mjs && node site/test_qspi.mjs && node site/test_qspi_cdp.mjs && node site/test_browser.mjs",
|
|
69
|
+
"test": "node site/test_stm32f4_api.mjs && node site/test_stm32f4_periph.mjs && node site/test_ws_bridge.mjs && node site/test_flow.mjs && node site/test_blinky.mjs && node site/test_rx_interrupt.mjs && 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_multi_instance.mjs && node site/test_fsmc_dcmi.mjs && node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/probe_freertos.mjs && node site/test_candemo.mjs && node site/test_lowpower.mjs && node site/test_edge_cases.mjs && node site/test_can_inject.mjs && node site/test_watchdog.mjs && node site/test_wwdg.mjs && node site/test_wwdg_window.mjs && node site/test_tim_capture.mjs && node site/test_qspi.mjs && node site/test_fpu.mjs && node site/test_fpu_irq.mjs && node site/test_mpu.mjs && node site/test_usb.mjs && node site/test_qspi_cdp.mjs && node site/test_browser.mjs",
|
|
67
70
|
"test:unit": "node site/test_stm32f4_api.mjs && node site/test_stm32f4_periph.mjs && node site/test_ws_bridge.mjs && node site/test_flow.mjs && node site/test_blinky.mjs && node site/test_rx_interrupt.mjs && 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_multi_instance.mjs && node site/test_fsmc_dcmi.mjs && node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/probe_freertos.mjs && node site/test_candemo.mjs && node site/test_lowpower.mjs && node site/test_edge_cases.mjs && node site/test_can_inject.mjs && node site/test_watchdog.mjs && node site/test_wwdg.mjs && node site/test_wwdg_window.mjs && node site/test_tim_capture.mjs && node site/test_qspi.mjs",
|
|
68
71
|
"test:bridge": "node site/test_ws_bridge.mjs",
|
|
69
72
|
"test:bridge:browser": "node site/test_bridge_cdp.mjs",
|
|
@@ -79,6 +82,9 @@
|
|
|
79
82
|
"test:timcap": "node site/test_tim_capture.mjs",
|
|
80
83
|
"test:lowpower": "node site/test_lowpower.mjs",
|
|
81
84
|
"test:qspi": "node site/test_qspi.mjs",
|
|
85
|
+
"test:fpu": "node site/test_fpu.mjs",
|
|
86
|
+
"test:fpuirq": "node site/test_fpu_irq.mjs",
|
|
87
|
+
"test:mpu": "node site/test_mpu.mjs",
|
|
82
88
|
"test:qspi:browser": "node site/test_qspi_cdp.mjs",
|
|
83
89
|
"test:browser": "node site/test_browser.mjs",
|
|
84
90
|
"test:edge": "node site/test_edge_cases.mjs",
|
|
@@ -89,7 +95,15 @@
|
|
|
89
95
|
"bridge": "node site/ws-bridge.mjs",
|
|
90
96
|
"firmwares": "node tools/make_firmware.mjs",
|
|
91
97
|
"serve": "python3 -m http.server 8123 --directory site",
|
|
92
|
-
"prepack": "node tools/make_firmware.mjs"
|
|
98
|
+
"prepack": "node tools/make_firmware.mjs",
|
|
99
|
+
"typecheck": "tsc --noEmit",
|
|
100
|
+
"lint": "eslint site mcp tools index.mjs cli.mjs",
|
|
101
|
+
"format": "prettier --check .",
|
|
102
|
+
"format:write": "prettier --write .",
|
|
103
|
+
"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'",
|
|
104
|
+
"build:wasm:node": "wasm-pack build stm32-periph-wasm --target nodejs --out-dir pkg",
|
|
105
|
+
"build:wasm:opt": "wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm",
|
|
106
|
+
"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"
|
|
93
107
|
},
|
|
94
108
|
"dependencies": {
|
|
95
109
|
"ws": "^8.21.3"
|
|
@@ -109,6 +123,9 @@
|
|
|
109
123
|
"devDependencies": {
|
|
110
124
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
111
125
|
"playwright": "^1.62.1",
|
|
112
|
-
"zod": "^3.25 || ^4.0"
|
|
126
|
+
"zod": "^3.25 || ^4.0",
|
|
127
|
+
"typescript": "^5.6.0",
|
|
128
|
+
"eslint": "^9.0.0",
|
|
129
|
+
"prettier": "^3.0.0"
|
|
113
130
|
}
|
|
114
131
|
}
|
package/site/app.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
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';
|
|
5
|
+
import * as bindings from './vendor/stm32_periph_wasm.js?v=13';
|
|
6
6
|
import { createEmulator } from './emulator.js';
|
|
7
7
|
import { createNetSim } from './netsim.js';
|
|
8
|
-
import {
|
|
8
|
+
import { createUsbHost } from './usbhost.js';
|
|
9
|
+
import { FIRMWARES } from './firmware.js?v=9';
|
|
9
10
|
import { parseIntelHex, parseElf, parseMap } from './loaders.js';
|
|
10
11
|
import { createRemoteEmulator } from './remote-emu.js';
|
|
11
12
|
|
|
@@ -13,22 +14,19 @@ const $ = (id) => document.getElementById(id);
|
|
|
13
14
|
const uartEl = $('uart'), statusEl = $('statusText'), dotEl = $('dot');
|
|
14
15
|
const framesEl = $('frames');
|
|
15
16
|
|
|
16
|
-
const decodeB64 = (b64) =>
|
|
17
|
-
|
|
18
|
-
const u = new Uint8Array(bin.length);
|
|
19
|
-
for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i);
|
|
20
|
-
return u;
|
|
21
|
-
};
|
|
17
|
+
const decodeB64 = (b64) => Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
18
|
+
const HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'));
|
|
22
19
|
const hex = (arr, n = 32) => {
|
|
23
20
|
let s = '';
|
|
24
|
-
|
|
21
|
+
const len = Math.min(arr.length, n);
|
|
22
|
+
for (let i = 0; i < len; i++) s += HEX_TABLE[arr[i]] + ' ';
|
|
25
23
|
if (arr.length > n) s += '…';
|
|
26
24
|
return s.trim();
|
|
27
25
|
};
|
|
28
26
|
const hex32 = (v) => '0x' + (v >>> 0).toString(16).padStart(8, '0');
|
|
29
27
|
|
|
30
28
|
let session = 0;
|
|
31
|
-
let emu = null, netsim = null, running = false;
|
|
29
|
+
let emu = null, netsim = null, usbhost = null, running = false;
|
|
32
30
|
let uartBuf = '', totalInst = 0, t0 = performance.now(), lastInst = 0, lastT = t0;
|
|
33
31
|
let stepsDone = 0;
|
|
34
32
|
let image = null; // { flash, ram, extraMem, entry, symbols, name, uartAddr }
|
|
@@ -46,12 +44,12 @@ const ETH_RX_MAP = {
|
|
|
46
44
|
// Interrupt-driven firmware: the emulator pumps guest IRQ handlers (USART RXNE
|
|
47
45
|
// etc.). OFF for ETH firmware — the driver signals completion via SRAM
|
|
48
46
|
// irq_flag and the guest ETH_IRQHandler would double-process DMASR/rx_desc.
|
|
49
|
-
const IRQ_FIRMWARES = new Set(['rx_interrupt_test', 'rx_crypto_test', 'comprehensive_test', 'eth_irq_test']);
|
|
47
|
+
const IRQ_FIRMWARES = new Set(['rx_interrupt_test', 'rx_crypto_test', 'comprehensive_test', 'eth_irq_test', 'edge_test', 'periph_test', 'fpu_irq_test', 'mpu_test']);
|
|
50
48
|
|
|
51
49
|
// Interrupt-driven ETH firmware: the guest ETH_IRQHandler (run by the pump)
|
|
52
50
|
// reads DMASR and scans rx_desc itself, so the driver must not write the
|
|
53
51
|
// SRAM irq_flag/rx_frame_idx globals (irq_eth mode in emulator.js).
|
|
54
|
-
const IRQ_ETH_FIRMWARES = new Set(['eth_irq_test']);
|
|
52
|
+
const IRQ_ETH_FIRMWARES = new Set(['eth_irq_test', 'eth_dhcp', 'eth_test']);
|
|
55
53
|
|
|
56
54
|
// Firmwares that exercise the WFI/STOP low-power path: the emulator halts the
|
|
57
55
|
// core on WFI and advances the virtual RTC until an alarm/interrupt wakes it.
|
|
@@ -75,7 +73,38 @@ const DEVICE_FIRMWARES = {
|
|
|
75
73
|
audio_play_test: { speaker: true },
|
|
76
74
|
rtc_test: { rtc: { i2c: 'I2C1', addr: 0x68, init: RTC_INIT } },
|
|
77
75
|
qspi_test: { qspi: [{ peripheral: 'QUADSPI', size: 256 }] },
|
|
78
|
-
|
|
76
|
+
// new_periph_test exercises DCMI polled capture (DR increments + FNE),
|
|
77
|
+
// which needs a sensor frame — same idea as the audio_test WAV seed.
|
|
78
|
+
// Ramp pixels so consecutive DR reads differ.
|
|
79
|
+
new_periph_test: { camera: { width: 64, height: 48, pixels: Uint8Array.from({ length: 64 * 48 }, (_, i) => i & 0xFF) } },
|
|
80
|
+
deep_periph_test: { camera: { width: 64, height: 48, pixels: Uint8Array.from({ length: 64 * 48 }, (_, i) => i & 0xFF) } },
|
|
81
|
+
// periph_test talks to an I2C EEPROM at 0x50 (write/readback + blank
|
|
82
|
+
// check at addr 0). Blank image; the model handles the R/W protocol.
|
|
83
|
+
periph_test: { i2c_eeprom: [{ peripheral: 'I2C1', address: 0x50, data: new Uint8Array(256).fill(0xFF) }] },
|
|
84
|
+
edge_test: { i2c_eeprom: [{ peripheral: 'I2C1', address: 0x50, data: new Uint8Array(256).fill(0xFF) }], spi_flash: [{ peripheral: 'SPI3', jedec_id: 0xEF4016, size: 0x200000, cs: null, data: new Uint8Array(0x200000).fill(0xFF) }] },
|
|
85
|
+
// spi_tft_test talks to an SPI NOR flash on SPI3 (software CS PB12).
|
|
86
|
+
// Without it JEDEC/status reads return 0xFF and the checks FAIL.
|
|
87
|
+
// Blank 2 MB image, same shape as site/test_spi_flash.mjs.
|
|
88
|
+
spi_tft_test: { spi_flash: [{ peripheral: 'SPI3', jedec_id: 0xEF4015, size: 0x200000, cs: 'PB12', data: new Uint8Array(0x200000).fill(0xFF) }] },
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// audio_test needs the same 64-sample PCM16 WAV the node harness
|
|
92
|
+
// (site/test_audio.mjs) loads via audio_load_wav — without it the I2S DMA
|
|
93
|
+
// reads fall back to generated audio and the firmware's checksum FAILs.
|
|
94
|
+
function makeAudioTestWav() {
|
|
95
|
+
const N = 64, dataLen = N * 2;
|
|
96
|
+
const w = new Uint8Array(44 + dataLen);
|
|
97
|
+
const dv = new DataView(w.buffer);
|
|
98
|
+
const wstr = (o, s) => { for (let i = 0; i < s.length; i++) w[o + i] = s.charCodeAt(i); };
|
|
99
|
+
wstr(0, 'RIFF'); dv.setUint32(4, 36 + dataLen, true); wstr(8, 'WAVE');
|
|
100
|
+
wstr(12, 'fmt '); dv.setUint32(16, 16, true);
|
|
101
|
+
dv.setUint16(20, 1, true); dv.setUint16(22, 1, true);
|
|
102
|
+
dv.setUint32(24, 44100, true); dv.setUint32(28, 44100 * 2, true);
|
|
103
|
+
dv.setUint16(32, 2, true); dv.setUint16(34, 16, true);
|
|
104
|
+
wstr(36, 'data'); dv.setUint32(40, dataLen, true);
|
|
105
|
+
for (let i = 0; i < N; i++) dv.setInt16(44 + i * 2, (i * 300 + 7) & 0xFFFF, true);
|
|
106
|
+
return w;
|
|
107
|
+
}
|
|
79
108
|
|
|
80
109
|
// ── status + UART ──────────────────────────────────────────────────────────
|
|
81
110
|
const setStatus = (text, cls) => {
|
|
@@ -83,10 +112,25 @@ const setStatus = (text, cls) => {
|
|
|
83
112
|
dotEl.className = 'dot ' + cls;
|
|
84
113
|
};
|
|
85
114
|
|
|
115
|
+
let uartChunks = [], uartLen = 0;
|
|
86
116
|
const appendUart = (chunk) => {
|
|
87
117
|
if (!chunk) return;
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
uartChunks.push(chunk);
|
|
119
|
+
uartLen += chunk.length;
|
|
120
|
+
// Keep last 200k chars without repeatedly copying 200k string (old: uartBuf+=chunk; slice)
|
|
121
|
+
while (uartLen > 200000) {
|
|
122
|
+
const first = uartChunks[0];
|
|
123
|
+
const excess = uartLen - 200000;
|
|
124
|
+
if (first.length <= excess) {
|
|
125
|
+
uartLen -= first.length;
|
|
126
|
+
uartChunks.shift();
|
|
127
|
+
} else {
|
|
128
|
+
uartChunks[0] = first.slice(excess);
|
|
129
|
+
uartLen = 200000;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
uartBuf = uartChunks.join('');
|
|
90
134
|
const wasAtBottom = uartEl.scrollTop + uartEl.clientHeight >= uartEl.scrollHeight - 8;
|
|
91
135
|
uartEl.textContent = uartBuf;
|
|
92
136
|
if (wasAtBottom && $('chkAuto').checked) uartEl.scrollTop = uartEl.scrollHeight;
|
|
@@ -202,7 +246,7 @@ $('btnBoot').addEventListener('click', async () => {
|
|
|
202
246
|
setBusy(false);
|
|
203
247
|
$('btnRun').textContent = 'Run';
|
|
204
248
|
setStatus('loading firmware…', 'stop');
|
|
205
|
-
uartEl.textContent = uartBuf = '';
|
|
249
|
+
uartEl.textContent = uartBuf = ''; uartChunks = []; uartLen = 0;
|
|
206
250
|
framesEl.textContent = '';
|
|
207
251
|
totalInst = 0; stepsDone = 0;
|
|
208
252
|
t0 = lastT = performance.now(); lastInst = 0;
|
|
@@ -273,7 +317,7 @@ const boot = async () => {
|
|
|
273
317
|
if (audioCtx) { try { audioCtx.close(); } catch (e) {} audioCtx = null; audioQueued = 0; }
|
|
274
318
|
|
|
275
319
|
const fw = image.flash;
|
|
276
|
-
uartEl.textContent = uartBuf = '';
|
|
320
|
+
uartEl.textContent = uartBuf = ''; uartChunks = []; uartLen = 0;
|
|
277
321
|
framesEl.textContent = '';
|
|
278
322
|
totalInst = 0; stepsDone = 0;
|
|
279
323
|
t0 = lastT = performance.now(); lastInst = 0;
|
|
@@ -282,6 +326,7 @@ const boot = async () => {
|
|
|
282
326
|
if (bridgeUrl) {
|
|
283
327
|
// ── remote mode: Node runs the emulator, browser is a thin client ──
|
|
284
328
|
netsim = null;
|
|
329
|
+
usbhost = null;
|
|
285
330
|
gw.tx = 0; gw.rx = 0;
|
|
286
331
|
try {
|
|
287
332
|
emu = await createRemoteEmulator(bridgeUrl, {
|
|
@@ -323,17 +368,21 @@ const boot = async () => {
|
|
|
323
368
|
}
|
|
324
369
|
} else {
|
|
325
370
|
// ── local mode: WASM runs in the browser (default) ──
|
|
326
|
-
|
|
371
|
+
// NOTE (VENDOR_V): vendor asset versions (?v=13) must be bumped together
|
|
372
|
+
// after every wasm-pack rebuild, or browsers keep the stale model.
|
|
373
|
+
const svdXml = await fetch('vendor/stm32f407.svd?v=13').then((r) => r.text());
|
|
327
374
|
if (id !== session) return;
|
|
328
375
|
|
|
329
376
|
netsim = gw.connected ? null : createNetSim();
|
|
377
|
+
// Scripted USB host when the demo preset boots locally.
|
|
378
|
+
usbhost = (!bridgeUrl && image.name === 'usb_cdc_test') ? createUsbHost(bindings) : null;
|
|
330
379
|
gw.tx = 0; gw.rx = 0;
|
|
331
380
|
if (gw.connected) setGwStatus(true, gwLabel());
|
|
332
381
|
emu = await createEmulator({
|
|
333
382
|
firmware: fw,
|
|
334
383
|
bindings,
|
|
335
|
-
unicorn: MUnicorn,
|
|
336
384
|
svdXml,
|
|
385
|
+
wasmUrl: 'vendor/stm32_periph_wasm_bg.wasm?v=13',
|
|
337
386
|
extra_mem: image.extraMem,
|
|
338
387
|
uart_addr: image.uartAddr,
|
|
339
388
|
enable_irqs: IRQ_FIRMWARES.has(image.name),
|
|
@@ -356,9 +405,25 @@ const boot = async () => {
|
|
|
356
405
|
},
|
|
357
406
|
});
|
|
358
407
|
if (id !== session) { emu.close(); return; }
|
|
408
|
+
// Seed the model's audio source for audio_test (see makeAudioTestWav).
|
|
409
|
+
if (image.name === 'audio_test' && bindings.audio_load_wav) {
|
|
410
|
+
try { bindings.audio_load_wav(makeAudioTestWav()); } catch (e) {}
|
|
411
|
+
}
|
|
412
|
+
// Pre-feed one camera frame so DCMI capture has sensor data from the
|
|
413
|
+
// very first step (the firmware reads DR immediately after setting
|
|
414
|
+
// CAPTURE, before any per-step feed runs).
|
|
415
|
+
if ((image.name === 'new_periph_test' || image.name === 'deep_periph_test') && emu.camera) {
|
|
416
|
+
try {
|
|
417
|
+
const cam = DEVICE_FIRMWARES[image.name].camera;
|
|
418
|
+
emu.camera.feed(cam.width, cam.height, cam.pixels);
|
|
419
|
+
} catch (e) {}
|
|
420
|
+
}
|
|
359
421
|
}
|
|
360
422
|
|
|
361
|
-
|
|
423
|
+
const bootTags = bridgeUrl
|
|
424
|
+
? `(bridge ${bridgeUrl})`
|
|
425
|
+
: (gw.connected ? '(gateway)' : '(netsim)');
|
|
426
|
+
appendUart(`── booted ${image.name} ${bootTags} ──\r\n`);
|
|
362
427
|
window.__emu = emu; // debug handle (CDP smoke tests)
|
|
363
428
|
window.__bindings = bindings;
|
|
364
429
|
running = true;
|
|
@@ -382,8 +447,13 @@ const loop = async (id) => {
|
|
|
382
447
|
return;
|
|
383
448
|
}
|
|
384
449
|
appendUart(emu.drainUart());
|
|
385
|
-
|
|
386
|
-
|
|
450
|
+
// Scripted USB host for the usb_cdc_test preset (local mode only —
|
|
451
|
+
// USB has no gateway backend; this is the netsim equivalent).
|
|
452
|
+
try {
|
|
453
|
+
if (usbhost && !usbhost.done) usbhost.frame(uartBuf);
|
|
454
|
+
} catch (e) {}
|
|
455
|
+
await refreshStats();
|
|
456
|
+
await renderLtdc();
|
|
387
457
|
renderDevices();
|
|
388
458
|
await raf();
|
|
389
459
|
}
|
|
@@ -414,7 +484,7 @@ $('btnReset').addEventListener('click', () => {
|
|
|
414
484
|
}
|
|
415
485
|
if (image) boot();
|
|
416
486
|
});
|
|
417
|
-
$('btnClear').addEventListener('click', () => { uartEl.textContent = uartBuf = ''; });
|
|
487
|
+
$('btnClear').addEventListener('click', () => { uartEl.textContent = uartBuf = ''; uartChunks = []; uartLen = 0; });
|
|
418
488
|
$('btnGw').addEventListener('click', connectGateway);
|
|
419
489
|
|
|
420
490
|
const sendRx = (term) => {
|
|
@@ -584,18 +654,22 @@ let ltdcCtx = ltdcCanvas.getContext('2d');
|
|
|
584
654
|
let ltdcCacheKey = '';
|
|
585
655
|
let ltdcOff = document.createElement('canvas');
|
|
586
656
|
|
|
587
|
-
const renderLtdc = () => {
|
|
657
|
+
const renderLtdc = async () => {
|
|
588
658
|
if (!emu || !ltdcCanvas.width) return;
|
|
589
659
|
try {
|
|
590
|
-
const gcr =
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
const
|
|
660
|
+
const [gcr, l1cr, pfRaw, cfbarRaw, cfblrRaw, cfblnrRaw, whpcrRaw, wvpcrRaw] = await Promise.all([
|
|
661
|
+
emu.read32(LTDC + 0x18), emu.read32(LTDC + 0x84), emu.read32(LTDC + 0x94),
|
|
662
|
+
emu.read32(LTDC + 0xAC), emu.read32(LTDC + 0xB0), emu.read32(LTDC + 0xB4),
|
|
663
|
+
emu.read32(LTDC + 0x88), emu.read32(LTDC + 0x8C),
|
|
664
|
+
]);
|
|
665
|
+
const gcrV = gcr >>> 0, l1crV = l1cr >>> 0;
|
|
666
|
+
if (!(gcrV & 1) || !(l1crV & 1)) { ltdcInfo.textContent = 'scanout idle'; return; }
|
|
667
|
+
const pf = pfRaw & 7;
|
|
668
|
+
const cfbar = cfbarRaw >>> 0;
|
|
669
|
+
const cfblr = cfblrRaw >>> 0;
|
|
670
|
+
const cfblnr = cfblnrRaw & 0x7FF;
|
|
671
|
+
const whpcr = whpcrRaw >>> 0;
|
|
672
|
+
const wvpcr = wvpcrRaw >>> 0;
|
|
599
673
|
const bpp = pf === 2 ? 2 : 4;
|
|
600
674
|
const lineBytes = Math.min(cfblr & 0x1FFF, 640 * bpp);
|
|
601
675
|
const w = Math.min((((whpcr & 0xFFF) + 1) || (lineBytes / bpp)) | 0, 640);
|
|
@@ -609,6 +683,11 @@ const renderLtdc = () => {
|
|
|
609
683
|
const key = pf + ':' + w + 'x' + lines + ':' + cfbar + ':' + pitch + ':' + (bindings.ltdc_get_frame_count ? bindings.ltdc_get_frame_count() : 0);
|
|
610
684
|
if (key === ltdcCacheKey) return;
|
|
611
685
|
ltdcCacheKey = key;
|
|
686
|
+
// Bridge mode has no direct uc.mem_read — skip framebuffer blit
|
|
687
|
+
if (!emu.uc || typeof emu.uc.mem_read !== 'function') {
|
|
688
|
+
ltdcInfo.textContent = `layer0 ${w}×${lines} ${pf === 2 ? 'RGB565' : 'ARGB8888'} @ ${hex32(cfbar)} (bridge — no fb read)`;
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
612
691
|
// Read (only the used width bytes per line — the model guest RAM is
|
|
613
692
|
// byte-exact, so no opacity/color-key handling beyond the format).
|
|
614
693
|
const rowBytes = Math.min(w * bpp, pitch || w * bpp);
|