stm32f4-emu 0.1.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 +53 -24
- package/cli.mjs +157 -0
- package/index.d.ts +48 -0
- package/index.mjs +12 -9
- package/mcp/server.mjs +20 -0
- package/package.json +62 -14
- package/site/app.js +346 -73
- package/site/emulator.d.ts +81 -0
- package/site/emulator.js +303 -468
- package/site/firmware.js +17 -2
- package/site/index.html +520 -156
- package/site/loaders.js +13 -7
- package/site/netsim.js +15 -17
- package/site/remote-emu.js +425 -0
- package/site/stm32f4.js +276 -0
- package/site/vendor/stm32_periph_wasm.d.ts +223 -16
- package/site/vendor/stm32_periph_wasm.js +632 -94
- package/site/vendor/stm32_periph_wasm_bg.wasm +0 -0
- package/site/vendor/stm32_periph_wasm_bg.wasm.d.ts +61 -16
- package/site/ws-bridge.mjs +521 -0
- 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
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
# STM32F4 Emulator
|
|
1
|
+
# STM32F4 Emulator (`stm32f4-emu`)
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/stm32f4-emu)
|
|
4
|
+
[](https://www.npmjs.com/package/stm32f4-emu)
|
|
5
|
+
[](LICENSE)
|
|
3
6
|
[](https://github.com/danish9661/stm32F4-emulator/actions/workflows/ci.yml)
|
|
4
7
|
[](https://github.com/danish9661/stm32F4-emulator/actions/workflows/pages.yml)
|
|
5
8
|
|
|
6
9
|
An STM32F407 microcontroller emulator that runs real Cortex-M4 firmware. It
|
|
7
|
-
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
|
|
8
12
|
**Rust peripheral model** (RCC, USART, GPIO, DMA, ETH, TIM, NVIC, ...) also
|
|
9
13
|
compiled to WASM — so the whole machine runs headless in **Node.js or a
|
|
10
14
|
browser tab**, with no SDL, no native deps, no hardware.
|
|
@@ -29,18 +33,18 @@ scripted network (netsim) as fallback, a live UART terminal (with **UART
|
|
|
29
33
|
RX input** — type into the console and the firmware reads it; newline
|
|
30
34
|
characters excluded per HTML spec, see AGENTS.md §11), GPIO pin readout
|
|
31
35
|
for banks A–E, and key peripheral registers. Interrupt-driven firmware
|
|
32
|
-
(`rx_interrupt_test`, `rx_crypto_test`)
|
|
33
|
-
|
|
36
|
+
(`rx_interrupt_test`, `rx_crypto_test`) gets inline guest-IRQ delivery;
|
|
37
|
+
polling firmware (the ETH demos) never uses it. For automation, a
|
|
34
38
|
preset can auto-boot via the URL: `?fw=eth_http`, `?fw=blinky`, `?fw=crypto_test`, …
|
|
35
39
|
|
|
36
40
|
## DOOM (in the browser)
|
|
37
41
|
|
|
38
42
|
**[`site/doom.html`](site/doom.html)** runs DOOM 1 shareware
|
|
39
|
-
(doomgeneric, ported to the emulated F407) at
|
|
40
|
-
headless-Chrome-verified browser page — playable
|
|
41
|
-
35 fps
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
44
48
|
per rendered frame, sound below 35 fps plays slightly slow and pitched-down
|
|
45
49
|
rather than breaking up — the worklet rate-matches instead of inserting
|
|
46
50
|
gaps, and the stats line reports it (`audio 0.72x`). The page: 320×200
|
|
@@ -72,6 +76,10 @@ npm run serve # then open http://127.0.0.1:8123
|
|
|
72
76
|
# node end-to-end flow test (boot -> DHCP -> TCP -> HTTP, 2 rounds)
|
|
73
77
|
npm test # == node site/test_flow.mjs
|
|
74
78
|
|
|
79
|
+
# websocket bridge: headless Node serves the emulator, browser is a thin UI
|
|
80
|
+
npm run bridge -- blinky/blinky.bin --port 8234
|
|
81
|
+
# then open http://127.0.0.1:8123?bridge=ws://127.0.0.1:8234
|
|
82
|
+
|
|
75
83
|
# gateway-backed run: firmware talks to a REAL network stack (gVisor)
|
|
76
84
|
cd stm32-periph-wasm/pkg
|
|
77
85
|
node cli.mjs ../../eth_http/eth_http.bin 10000000 \
|
|
@@ -126,6 +134,29 @@ as a library stays dependency-free; only MCP users need
|
|
|
126
134
|
`npm i @modelcontextprotocol/sdk zod`. See [docs/mcp.md](docs/mcp.md) for
|
|
127
135
|
the tool reference and client config.
|
|
128
136
|
|
|
137
|
+
## WebSocket bridge (headless Node ↔ browser UI)
|
|
138
|
+
|
|
139
|
+
A binary WebSocket protocol so the browser console can drive the emulator
|
|
140
|
+
running headlessly in Node — all WASM execution stays in Node, the browser
|
|
141
|
+
is a thin UI. Zero impact on the existing local WASM path:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# 1. Start the bridge in Node (serves the emulator over WS on port 8234)
|
|
145
|
+
node site/ws-bridge.mjs eth_http/eth_http.bin --port 8234
|
|
146
|
+
|
|
147
|
+
# 2. Open the browser console with the bridge URL param
|
|
148
|
+
open "http://127.0.0.1:8123/?bridge=ws://127.0.0.1:8234"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The `RemoteEmu` adapter (`site/remote-emu.js`) is a drop-in replacement
|
|
152
|
+
for the local `emu` object — same `step()`/`drainUart()`/`read32()` API,
|
|
153
|
+
all proxied over binary WebSocket. Device stubs (OLED/TFT/etc.) run in
|
|
154
|
+
Node and are not visible to browser JS. Without `?fw=`, the page boots
|
|
155
|
+
whatever firmware the bridge was started with; with `?fw=blinky&bridge=ws://…`,
|
|
156
|
+
the browser sends the firmware image over the bridge.
|
|
157
|
+
|
|
158
|
+
See AGENTS.md §20 for the full binary protocol reference.
|
|
159
|
+
|
|
129
160
|
## Firmwares
|
|
130
161
|
|
|
131
162
|
| Firmware | What it does | Success marker |
|
|
@@ -152,9 +183,9 @@ watchdog timeouts close to spec) will diverge — see docs/progress-and-future.m
|
|
|
152
183
|
## Architecture
|
|
153
184
|
|
|
154
185
|
```
|
|
155
|
-
firmware .bin ──►
|
|
186
|
+
firmware .bin ──► Rust WASM CPU (Thumb-2 + NVIC/exceptions)
|
|
156
187
|
│
|
|
157
|
-
|
|
188
|
+
peripheral MMIO ──► Rust peripheral model (WASM)
|
|
158
189
|
RCC USART GPIO DMA ETH TIM NVIC
|
|
159
190
|
│
|
|
160
191
|
UART out / ETH TX frames ──► driver (cli.mjs / site/emulator.js)
|
|
@@ -162,9 +193,10 @@ firmware .bin ──► Unicorn WASM CPU ──► memory hooks
|
|
|
162
193
|
RX frames injected (netsim, or real gVisor gateway)
|
|
163
194
|
```
|
|
164
195
|
|
|
165
|
-
- **CPU**:
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
168
200
|
- **Peripherals**: a `wasm-bindgen` crate (`stm32-periph-wasm/`); registers
|
|
169
201
|
and bit fields come from the vendor SVD (`monox/stm32f407.svd`).
|
|
170
202
|
- **Ethernet**: TX is captured from the DMA descriptors; RX frames are
|
|
@@ -193,8 +225,8 @@ firmware .bin ──► Unicorn WASM CPU ──► memory hooks
|
|
|
193
225
|
│ ├── test_blinky.mjs Node blinky GPIO test (npm test)
|
|
194
226
|
│ ├── test_rx_interrupt.mjs Node UART-interrupt test (npm test)
|
|
195
227
|
│ ├── test_component_*.mjs Component-API tests, one firmware each (npm test)
|
|
196
|
-
│ ├──
|
|
197
|
-
│ └── 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
|
|
198
230
|
├── index.mjs, package.json npm package entry (stm32f4-emu)
|
|
199
231
|
├── mcp/ MCP server (drive the emulator from an AI agent)
|
|
200
232
|
├── .github/workflows/ CI (Linux/Windows/macOS test matrix) + Pages deploy
|
|
@@ -246,6 +278,7 @@ rebuild — delete it so the vendor assets stay tracked/committed.
|
|
|
246
278
|
|
|
247
279
|
## Documentation
|
|
248
280
|
|
|
281
|
+
- [site/about.html](site/about.html) — in-repo About page: what it is, architecture, featured firmwares, and how to use it (CLI / browser / Node API / MCP).
|
|
249
282
|
- [docs/architecture.md](docs/architecture.md) — how the emulator is put
|
|
250
283
|
together (CPU, peripheral model, drivers, ETH flow, interrupts).
|
|
251
284
|
- [docs/peripherals.md](docs/peripherals.md) — all 33 peripherals and the
|
|
@@ -262,12 +295,8 @@ rebuild — delete it so the vendor assets stay tracked/committed.
|
|
|
262
295
|
- [docs/progress-and-future.md](docs/progress-and-future.md) — status,
|
|
263
296
|
known limitations, roadmap.
|
|
264
297
|
|
|
265
|
-
## License
|
|
266
|
-
|
|
267
|
-
GPL-3.0-only. See [LICENSE](LICENSE).
|
|
298
|
+
## License & Credits
|
|
268
299
|
|
|
269
|
-
|
|
270
|
-
[nviennot/stm32-emulator](https://github.com/nviennot/stm32-emulator),
|
|
271
|
-
|
|
272
|
-
SDL app. The WASM headless emulator, network firmwares, browser demo, and npm
|
|
273
|
-
package are new work built on that base.*
|
|
300
|
+
- **License**: GPL-3.0-only. See [LICENSE](LICENSE).
|
|
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.
|
|
302
|
+
- **DOOM**: Ported using [doomgeneric](https://github.com/ozkl/doomgeneric) by Ozkan Sezgin.
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// stm32f4-emu — headless CLI to load and run an STM32F4 firmware image.
|
|
3
|
+
//
|
|
4
|
+
// stm32f4-emu <firmware> [--inst N] [--format auto|bin|hex|elf] [--verbose]
|
|
5
|
+
// [--lowpower]
|
|
6
|
+
//
|
|
7
|
+
// Loads a .bin/.elf/.hex firmware, boots it in the emulator, and streams the
|
|
8
|
+
// guest UART to stdout. Peripheral register accesses are traced to stderr when
|
|
9
|
+
// --verbose is given. With --lowpower, the core halts on WFI/WFE and the
|
|
10
|
+
// emulator advances the virtual RTC until a wakeup source (e.g. an RTC alarm)
|
|
11
|
+
// fires — so firmware that enters STOP (e.g. deep_sleep_demo.bin) runs.
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { dirname, resolve } from 'node:path';
|
|
15
|
+
import * as bindings from './site/vendor/stm32_periph_wasm.js';
|
|
16
|
+
import { createEmulator } from './site/emulator.js';
|
|
17
|
+
import { parseIntelHex, parseElf } from './site/loaders.js';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const svdXml = readFileSync(resolve(__dirname, 'site/vendor/stm32f407.svd'), 'utf8');
|
|
21
|
+
const wasmBytes = new Uint8Array(readFileSync(resolve(__dirname, 'site/vendor/stm32_periph_wasm_bg.wasm')));
|
|
22
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8'));
|
|
23
|
+
|
|
24
|
+
const USAGE = `stm32f4-emu — run an STM32F4 firmware image in the emulator
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
stm32f4-emu <firmware> [options]
|
|
28
|
+
|
|
29
|
+
Arguments:
|
|
30
|
+
<firmware> path to a firmware image (.bin, .elf, or .hex)
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
-n, --inst <N> instruction budget to run (default 20000000)
|
|
34
|
+
-f, --format <fmt> firmware format: auto|bin|hex|elf (default auto)
|
|
35
|
+
-v, --verbose trace guest PCs to stderr (capped per step)
|
|
36
|
+
-l, --lowpower halt on WFI/WFE and advance the virtual RTC until wakeup
|
|
37
|
+
-h, --help show this help
|
|
38
|
+
-V, --version show version
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
stm32f4-emu build/firmware.bin
|
|
42
|
+
stm32f4-emu app.elf --inst 5000000 --verbose
|
|
43
|
+
stm32f4-emu deep_sleep_demo.bin --lowpower
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
function fail(msg) {
|
|
47
|
+
process.stderr.write(msg + '\n\n' + USAGE);
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function detectFormat(buf, format) {
|
|
52
|
+
if (format !== 'auto') return format;
|
|
53
|
+
if (buf[0] === 0x7F && buf[1] === 0x45 && buf[2] === 0x4C && buf[3] === 0x46) return 'elf';
|
|
54
|
+
let i = 0;
|
|
55
|
+
while (i < buf.length && (buf[i] === 0x20 || buf[i] === 0x09 || buf[i] === 0x0D || buf[i] === 0x0A)) i++;
|
|
56
|
+
if (buf[i] === 0x3A) return 'hex'; // ':'
|
|
57
|
+
return 'bin';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const args = process.argv.slice(2);
|
|
62
|
+
let firmwarePath = null;
|
|
63
|
+
let inst = 20000000;
|
|
64
|
+
let format = 'auto';
|
|
65
|
+
let verbose = false;
|
|
66
|
+
let lowpower = false;
|
|
67
|
+
for (let i = 0; i < args.length; i++) {
|
|
68
|
+
const a = args[i];
|
|
69
|
+
if (a === '-h' || a === '--help') { process.stdout.write(USAGE); process.exit(0); }
|
|
70
|
+
if (a === '-V' || a === '--version') { process.stdout.write(`stm32f4-emu ${pkg.version}\n`); process.exit(0); }
|
|
71
|
+
if (a === '-v' || a === '--verbose') { verbose = true; continue; }
|
|
72
|
+
if (a === '-l' || a === '--lowpower') { lowpower = true; continue; }
|
|
73
|
+
if (a === '-n' || a === '--inst') {
|
|
74
|
+
const v = args[++i];
|
|
75
|
+
inst = Number(v);
|
|
76
|
+
if (!Number.isFinite(inst) || inst <= 0) fail(`invalid --inst value: ${v}`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (a === '-f' || a === '--format') {
|
|
80
|
+
format = args[++i];
|
|
81
|
+
if (!['auto', 'bin', 'hex', 'elf'].includes(format)) fail(`invalid --format: ${format}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (a.startsWith('-')) fail(`unknown option: ${a}`);
|
|
85
|
+
if (firmwarePath) fail('multiple firmware paths given');
|
|
86
|
+
firmwarePath = a;
|
|
87
|
+
}
|
|
88
|
+
if (!firmwarePath) fail('no firmware path given');
|
|
89
|
+
|
|
90
|
+
let raw;
|
|
91
|
+
try {
|
|
92
|
+
raw = new Uint8Array(readFileSync(firmwarePath));
|
|
93
|
+
} catch (e) {
|
|
94
|
+
fail(`cannot read firmware '${firmwarePath}': ${e.message}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fmt = detectFormat(raw, format);
|
|
98
|
+
let firmware, extra_mem = [];
|
|
99
|
+
try {
|
|
100
|
+
if (fmt === 'elf') {
|
|
101
|
+
const elf = parseElf(raw);
|
|
102
|
+
if (!elf.flash) fail('ELF contains no FLASH segment (expected a segment at 0x08000000)');
|
|
103
|
+
firmware = elf.flash;
|
|
104
|
+
extra_mem = elf.extraMem || [];
|
|
105
|
+
} else if (fmt === 'hex') {
|
|
106
|
+
const hex = parseIntelHex(Buffer.from(raw).toString('latin1'));
|
|
107
|
+
if (!hex.flash) fail('Intel HEX contains no FLASH records (expected data in the 0x08000000 range)');
|
|
108
|
+
firmware = hex.flash;
|
|
109
|
+
} else {
|
|
110
|
+
firmware = raw;
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
fail(`failed to parse ${fmt} firmware: ${e.message}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let emu;
|
|
117
|
+
try {
|
|
118
|
+
emu = await createEmulator({
|
|
119
|
+
firmware, bindings, svdXml, wasmInit: wasmBytes,
|
|
120
|
+
extra_mem, lowpower,
|
|
121
|
+
});
|
|
122
|
+
} catch (e) {
|
|
123
|
+
fail(`emulator failed to load firmware: ${e.message}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const STEP = 100000;
|
|
127
|
+
let remaining = inst;
|
|
128
|
+
let lastInst = 0;
|
|
129
|
+
try {
|
|
130
|
+
while (remaining > 0) {
|
|
131
|
+
const take = Math.min(STEP, remaining);
|
|
132
|
+
if (verbose) { try { emu.traceStart(); } catch {} }
|
|
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
|
+
}
|
|
141
|
+
remaining -= (r.instCount - lastInst);
|
|
142
|
+
lastInst = r.instCount;
|
|
143
|
+
const u = emu.drainUart();
|
|
144
|
+
if (u && u.length) process.stdout.write(u.toString());
|
|
145
|
+
if (r.stopped) break;
|
|
146
|
+
}
|
|
147
|
+
const tail = emu.drainUart();
|
|
148
|
+
if (tail && tail.length) process.stdout.write(tail.toString());
|
|
149
|
+
} finally {
|
|
150
|
+
try { emu.close(); } catch {}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
main().catch((e) => {
|
|
155
|
+
process.stderr.write(`stm32f4-emu: ${e.message}\n`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
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,28 +1,31 @@
|
|
|
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';
|
|
9
8
|
import { FIRMWARES } from './site/firmware.js';
|
|
10
9
|
import { LED, Button, Pwm, I2cRegisterDevice, Potentiometer } from './site/components.js';
|
|
10
|
+
import { STM32F4, GPIOPin, USART, DMAStream } from './site/stm32f4.js';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Node consumers get a one-call setup that injects the bundled assets. The
|
|
13
|
+
// `firmware` option is optional (defer to loadBin/loadHex/loadELF after
|
|
14
|
+
// create), matching the rp2040js / avr8js ergonomics.
|
|
15
|
+
STM32F4.create = (opts = {}) => STM32F4._create({
|
|
16
|
+
bindings, svdXml, wasmInit: wasmBytes, ...opts,
|
|
17
|
+
});
|
|
14
18
|
|
|
15
19
|
const svdXml = readFileSync(new URL('./site/vendor/stm32f407.svd', import.meta.url), 'utf8');
|
|
16
20
|
const wasmBytes = new Uint8Array(readFileSync(new URL('./site/vendor/stm32_periph_wasm_bg.wasm', import.meta.url)));
|
|
17
21
|
|
|
18
22
|
// Decode a base64-encoded firmware from FIRMWARES.
|
|
23
|
+
// Uses single-pass Uint8Array.from for fewer allocations than manual loop.
|
|
19
24
|
export function decodeFirmware(key) {
|
|
20
25
|
const fw = FIRMWARES[key];
|
|
21
26
|
if (!fw) throw new Error(`unknown firmware '${key}' (have: ${Object.keys(FIRMWARES).join(', ')})`);
|
|
22
27
|
const bin = atob(fw.bytes);
|
|
23
|
-
|
|
24
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
25
|
-
return out;
|
|
28
|
+
return Uint8Array.from(bin, c => c.charCodeAt(0));
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
// Convenience: create an emulator with the bundled STM32F407 assets.
|
|
@@ -33,7 +36,7 @@ export async function createSTM32F407(opts = {}) {
|
|
|
33
36
|
const { firmware } = opts;
|
|
34
37
|
const bin = typeof firmware === 'string' ? decodeFirmware(firmware) : firmware;
|
|
35
38
|
if (!bin) throw new Error('createSTM32F407 requires `firmware` (Uint8Array or a FIRMWARES key)');
|
|
36
|
-
return createEmulator({ ...opts, firmware: bin, bindings,
|
|
39
|
+
return createEmulator({ ...opts, firmware: bin, bindings, svdXml, wasmInit: wasmBytes });
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
export { createEmulator, createNetSim, FIRMWARES, bindings,
|
|
42
|
+
export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, USART, DMAStream };
|
package/mcp/server.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
// MCP server exposing the STM32F407 emulator as tools an MCP client
|
|
3
4
|
// (Claude Code / Claude Desktop / any MCP host) can drive: load firmware,
|
|
4
5
|
// step execution, read/write UART, poke and watch GPIO pins, inject ADC
|
|
@@ -31,6 +32,25 @@ try {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
let session = null; // { emu, firmware, components: Map }
|
|
35
|
+
|
|
36
|
+
// ── CLI surface for the bin: stm32f4-mcp --help / --version ──
|
|
37
|
+
// Printed before the stdio MCP transport starts (stdout is the JSON-RPC
|
|
38
|
+
// channel once running, so we must exit before connecting).
|
|
39
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
'stm32f4-mcp — MCP server exposing the STM32F407 emulator.\n\n' +
|
|
42
|
+
'Usage: stm32f4-mcp [--help] [--version]\n\n' +
|
|
43
|
+
'Speaks MCP over stdio (JSON-RPC). Connect it from an MCP client such\n' +
|
|
44
|
+
'as Claude Code or Claude Desktop. Optional peer deps:\n' +
|
|
45
|
+
' npm install @modelcontextprotocol/sdk zod\n'
|
|
46
|
+
);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
if (process.argv.includes('--version') || process.argv.includes('-V')) {
|
|
50
|
+
const ver = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
51
|
+
process.stdout.write(`stm32f4-mcp ${ver}\n`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
34
54
|
let componentSeq = 0;
|
|
35
55
|
|
|
36
56
|
const text = (s) => ({ content: [{ type: 'text', text: typeof s === 'string' ? s : JSON.stringify(s, null, 2) }] });
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stm32f4-emu",
|
|
3
|
-
"version": "
|
|
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": {
|
|
8
|
-
"stm32f4-
|
|
8
|
+
"stm32f4-emu": "./cli.mjs",
|
|
9
|
+
"stm32f4-mcp": "./mcp/server.mjs",
|
|
10
|
+
"stm32f4-bridge": "./site/ws-bridge.mjs"
|
|
9
11
|
},
|
|
12
|
+
"types": "index.d.ts",
|
|
10
13
|
"exports": {
|
|
11
14
|
".": "./index.mjs",
|
|
12
15
|
"./components": "./site/components.js",
|
|
@@ -14,22 +17,31 @@
|
|
|
14
17
|
"./netsim": "./site/netsim.js",
|
|
15
18
|
"./firmwares": "./site/firmware.js",
|
|
16
19
|
"./loaders": "./site/loaders.js",
|
|
20
|
+
"./stm32f4": "./site/stm32f4.js",
|
|
17
21
|
"./vendor": "./site/vendor/stm32_periph_wasm.js",
|
|
18
|
-
"./
|
|
19
|
-
"./
|
|
22
|
+
"./remote": "./site/remote-emu.js",
|
|
23
|
+
"./ws-bridge": "./site/ws-bridge.mjs"
|
|
20
24
|
},
|
|
21
25
|
"files": [
|
|
22
26
|
"index.mjs",
|
|
27
|
+
"index.d.ts",
|
|
28
|
+
"cli.mjs",
|
|
23
29
|
"README.md",
|
|
30
|
+
"LICENSE",
|
|
24
31
|
"site/emulator.js",
|
|
32
|
+
"site/emulator.d.ts",
|
|
33
|
+
"site/stm32f4.js",
|
|
25
34
|
"site/components.js",
|
|
26
|
-
"mcp/server.mjs",
|
|
27
35
|
"site/netsim.js",
|
|
28
36
|
"site/firmware.js",
|
|
29
37
|
"site/loaders.js",
|
|
30
|
-
"site/
|
|
38
|
+
"site/remote-emu.js",
|
|
39
|
+
"site/ws-bridge.mjs",
|
|
31
40
|
"site/app.js",
|
|
32
|
-
"site/
|
|
41
|
+
"site/index.html",
|
|
42
|
+
"site/vendor/",
|
|
43
|
+
"mcp/server.mjs",
|
|
44
|
+
"tools/make_firmware.mjs"
|
|
33
45
|
],
|
|
34
46
|
"keywords": [
|
|
35
47
|
"stm32",
|
|
@@ -37,7 +49,6 @@
|
|
|
37
49
|
"emulator",
|
|
38
50
|
"arm",
|
|
39
51
|
"cortex-m4",
|
|
40
|
-
"unicorn",
|
|
41
52
|
"wasm",
|
|
42
53
|
"embedded",
|
|
43
54
|
"firmware"
|
|
@@ -55,29 +66,66 @@
|
|
|
55
66
|
"node": ">=20"
|
|
56
67
|
},
|
|
57
68
|
"scripts": {
|
|
58
|
-
"test": "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",
|
|
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",
|
|
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",
|
|
71
|
+
"test:bridge": "node site/test_ws_bridge.mjs",
|
|
72
|
+
"test:bridge:browser": "node site/test_bridge_cdp.mjs",
|
|
59
73
|
"test:flow": "node site/test_flow.mjs",
|
|
60
74
|
"test:blinky": "node site/test_blinky.mjs",
|
|
61
75
|
"test:rx": "node site/test_rx_interrupt.mjs",
|
|
62
76
|
"test:multi": "node site/test_multi_instance.mjs",
|
|
77
|
+
"test:can": "node site/test_candemo.mjs",
|
|
78
|
+
"test:caninject": "node site/test_can_inject.mjs",
|
|
79
|
+
"test:watchdog": "node site/test_watchdog.mjs",
|
|
80
|
+
"test:wwdg": "node site/test_wwdg.mjs",
|
|
81
|
+
"test:wwdgwin": "node site/test_wwdg_window.mjs",
|
|
82
|
+
"test:timcap": "node site/test_tim_capture.mjs",
|
|
83
|
+
"test:lowpower": "node site/test_lowpower.mjs",
|
|
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",
|
|
88
|
+
"test:qspi:browser": "node site/test_qspi_cdp.mjs",
|
|
89
|
+
"test:browser": "node site/test_browser.mjs",
|
|
90
|
+
"test:edge": "node site/test_edge_cases.mjs",
|
|
63
91
|
"test:fsmc": "node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/test_fsmc_dcmi.mjs",
|
|
64
92
|
"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",
|
|
65
93
|
"test:mcp": "node mcp/test_mcp.mjs",
|
|
66
94
|
"mcp": "node mcp/server.mjs",
|
|
95
|
+
"bridge": "node site/ws-bridge.mjs",
|
|
67
96
|
"firmwares": "node tools/make_firmware.mjs",
|
|
68
97
|
"serve": "python3 -m http.server 8123 --directory site",
|
|
69
|
-
"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"
|
|
107
|
+
},
|
|
108
|
+
"dependencies": {
|
|
109
|
+
"ws": "^8.21.3"
|
|
70
110
|
},
|
|
71
111
|
"peerDependencies": {
|
|
72
112
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
73
113
|
"zod": "^3.25 || ^4.0"
|
|
74
114
|
},
|
|
75
115
|
"peerDependenciesMeta": {
|
|
76
|
-
"@modelcontextprotocol/sdk": {
|
|
77
|
-
|
|
116
|
+
"@modelcontextprotocol/sdk": {
|
|
117
|
+
"optional": true
|
|
118
|
+
},
|
|
119
|
+
"zod": {
|
|
120
|
+
"optional": true
|
|
121
|
+
}
|
|
78
122
|
},
|
|
79
123
|
"devDependencies": {
|
|
80
124
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
81
|
-
"
|
|
125
|
+
"playwright": "^1.62.1",
|
|
126
|
+
"zod": "^3.25 || ^4.0",
|
|
127
|
+
"typescript": "^5.6.0",
|
|
128
|
+
"eslint": "^9.0.0",
|
|
129
|
+
"prettier": "^3.0.0"
|
|
82
130
|
}
|
|
83
131
|
}
|