stm32f4-emu 1.0.0 → 1.2.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
@@ -6,15 +6,29 @@
6
6
  [![CI](https://github.com/danish9661/stm32F4-emulator/actions/workflows/ci.yml/badge.svg)](https://github.com/danish9661/stm32F4-emulator/actions/workflows/ci.yml)
7
7
  [![Pages](https://github.com/danish9661/stm32F4-emulator/actions/workflows/pages.yml/badge.svg)](https://github.com/danish9661/stm32F4-emulator/actions/workflows/pages.yml)
8
8
 
9
+ > **Status: emulation complete.** All 41 peripheral modules are Detailed
10
+ > (0 Partial), all five board maps verify green (37/37/72/73/59 presets),
11
+ > and the Ethernet descriptor layer (EDFE, TCH/TER + RCH/RER walks, RDES4,
12
+ > backoff probe) plus the Wireshark-validated pcap capture (`a1b2c3d4`,
13
+ > tcpdump-verified DHCP→TCP→HTTP) are in. What remains is product work,
14
+ > not emulation: npm publish, https gateway endpoint, VS Code packaging
15
+ > (see [docs/progress-and-future.md](docs/progress-and-future.md)).
16
+
9
17
  An STM32F407 microcontroller emulator that runs real Cortex-M4 firmware. It
10
- combines a **Unicorn CPU core** (QEMU-derived, compiled to WASM) with a
18
+ combines a **Rust CPU core** (a WASM-native Thumb-2 interpreter with exact
19
+ Cortex-M exception entry/return, including the VFPv4-SP FPU) with a
11
20
  **Rust peripheral model** (RCC, USART, GPIO, DMA, ETH, TIM, NVIC, ...) also
12
21
  compiled to WASM — so the whole machine runs headless in **Node.js or a
13
22
  browser tab**, with no SDL, no native deps, no hardware.
14
23
 
15
24
  It ships three real networking firmwares (`eth_http`, `eth_dhcp`, `eth_test`)
16
25
  that do DHCP + TCP + HTTP against a simulated (or a real gVisor-backed)
17
- network, a browser demo, and a publishable npm package.
26
+ network, a 66-marker Ethernet feature matrix (`eth_feat_test`: PHY/MDIO,
27
+ checksum offload, hash/perfect/SA/DA filtering, VLAN, PTP, WOL, wire
28
+ pacing, deferral/collisions, pause frames, MMC, RBUS — see
29
+ [NETWORKING.md](NETWORKING.md)) plus real LwIP 2.2.1 (`lwip_demo`) and
30
+ MII/RMII pin mirrors (`eth_pins_test`), a browser demo, and a publishable
31
+ npm package.
18
32
 
19
33
  ## Live demo
20
34
 
@@ -23,7 +37,7 @@ The browser demo deploys to GitHub Pages:
23
37
  **https://danish9661.github.io/stm32F4-emulator/**
24
38
 
25
39
  A single console page that starts **idle** — nothing runs until you pick a
26
- firmware: a preset dropdown with 31 bundled binaries (network demos, a
40
+ firmware: a preset dropdown with 223 bundled binaries (network demos, a
27
41
  bare-metal LED blinker, peripheral/crypto/UART/SPI test binaries), custom
28
42
  firmware upload (`.bin`, Intel `.hex`, `.elf` — with loadable RAM segments
29
43
  and symbols — plus `.map` for a symbol table), Run/Stop/Reset, a **gateway
@@ -32,18 +46,18 @@ scripted network (netsim) as fallback, a live UART terminal (with **UART
32
46
  RX input** — type into the console and the firmware reads it; newline
33
47
  characters excluded per HTML spec, see AGENTS.md §11), GPIO pin readout
34
48
  for banks A–E, and key peripheral registers. Interrupt-driven firmware
35
- (`rx_interrupt_test`, `rx_crypto_test`) is serviced by an opt-in guest-IRQ
36
- pump; polling firmware (the ETH demos) never uses it. For automation, a
37
- preset can auto-boot via the URL: `?fw=eth_http`, `?fw=blinky`, `?fw=crypto_test`, …
49
+ (`rx_interrupt_test`, `rx_crypto_test`) gets inline guest-IRQ delivery;
50
+ polling firmware (the ETH demos) never uses it. For automation, a
51
+ preset can auto-boot via the URL: `console.html?fw=eth_http`, `console.html?fw=blinky`, `console.html?fw=crypto_test`, …
38
52
 
39
53
  ## DOOM (in the browser)
40
54
 
41
55
  **[`site/doom.html`](site/doom.html)** runs DOOM 1 shareware
42
- (doomgeneric, ported to the emulated F407) at ~25 FPS in a
43
- headless-Chrome-verified browser page — playable, but below DOOM's native
44
- 35 fps: at ~918k guest instructions per rendered frame, 35 fps would need
45
- ~32 MIPS and the Unicorn WASM core tops out near 20-24 (details and the
46
- measurements in AGENTS.md §16). Because the guest mixes one frame of audio
56
+ (doomgeneric, ported to the emulated F407) at the full 35 fps in a
57
+ headless-Chrome-verified browser page — playable: at ~1M guest instructions
58
+ per rendered frame, 35 fps needs ~32 MIPS and the Rust core delivers ~65
59
+ (details and the measurements in AGENTS.md §16/§22). Because the guest mixes
60
+ one frame of audio
47
61
  per rendered frame, sound below 35 fps plays slightly slow and pitched-down
48
62
  rather than breaking up — the worklet rate-matches instead of inserting
49
63
  gaps, and the stats line reports it (`audio 0.72x`). The page: 320×200
@@ -59,7 +73,7 @@ reloads.
59
73
  - Controls: move W/S/A/D + arrows · strafe Shift · fire Ctrl · use
60
74
  Space · menu Enter/Esc · save F2/F6 · load F3/F9 · F1/F10/F11/F12.
61
75
  - Boot → menu → gameplay verified end-to-end by
62
- `node site/test_doom.mjs` (boot markers, menu navigation to E1M1,
76
+ `node site/test_doom_wasm.mjs` (boot markers, menu navigation to E1M1,
63
77
  palette + framebuffer, W-move + turn, audio, **save → `SAVE ok slot=0`
64
78
  → load handshake**).
65
79
  - `site/doom1.wad` is the 4.2 MB shareware WAD; the firmware never reads
@@ -77,7 +91,7 @@ npm test # == node site/test_flow.mjs
77
91
 
78
92
  # websocket bridge: headless Node serves the emulator, browser is a thin UI
79
93
  npm run bridge -- blinky/blinky.bin --port 8234
80
- # then open http://127.0.0.1:8123?bridge=ws://127.0.0.1:8234
94
+ # then open http://127.0.0.1:8123/console.html?bridge=ws://127.0.0.1:8234
81
95
 
82
96
  # gateway-backed run: firmware talks to a REAL network stack (gVisor)
83
97
  cd stm32-periph-wasm/pkg
@@ -144,14 +158,14 @@ is a thin UI. Zero impact on the existing local WASM path:
144
158
  node site/ws-bridge.mjs eth_http/eth_http.bin --port 8234
145
159
 
146
160
  # 2. Open the browser console with the bridge URL param
147
- open "http://127.0.0.1:8123/?bridge=ws://127.0.0.1:8234"
161
+ open "http://127.0.0.1:8123/console.html?bridge=ws://127.0.0.1:8234"
148
162
  ```
149
163
 
150
164
  The `RemoteEmu` adapter (`site/remote-emu.js`) is a drop-in replacement
151
165
  for the local `emu` object — same `step()`/`drainUart()`/`read32()` API,
152
166
  all proxied over binary WebSocket. Device stubs (OLED/TFT/etc.) run in
153
167
  Node and are not visible to browser JS. Without `?fw=`, the page boots
154
- whatever firmware the bridge was started with; with `?fw=blinky&bridge=ws://…`,
168
+ whatever firmware the bridge was started with; with `console.html?fw=blinky&bridge=ws://…`,
155
169
  the browser sends the firmware image over the bridge.
156
170
 
157
171
  See AGENTS.md §20 for the full binary protocol reference.
@@ -163,8 +177,11 @@ See AGENTS.md §20 for the full binary protocol reference.
163
177
  | `eth_http/` | DHCP + TCP client + HTTP GET + prints the response | `TCP connected`, `=== HTTP <len>b ===` |
164
178
  | `eth_dhcp/` | Loops DHCP Discover/Offer/Request/Ack | `DHCP SUCCESS` |
165
179
  | `eth_test/` | Raw ETH TX/RX self-test | `ETH Test: done` |
180
+ | `eth_feat_test/` | 66-marker Ethernet feature matrix (netsim, F407 + F429) | `FEAT Test: done` (all 66 markers, `FAIL`/`TIMEOUT` anti-markers) |
181
+ | `lwip_demo/` | Real LwIP 2.2.1 (DHCP→DNS→TCP echo→TCP server→UDP echo) | `LWIP DEMO DONE` |
182
+ | `eth_pins_test/` | MII/RMII pin-level mirrors (TX_EN/CRS_DV/RXD/COL/MDIO/MDC) | `PINS ALL PASS` |
166
183
  | `blinky/` | **No ethernet** — LED blinker on GPIOA PA5 + UART tick counter | `tick N LED=ON/OFF` |
167
- | `doom/` | **DOOM 1 shareware** (doomgeneric F407 port, browser page `site/doom.html`) | `node site/test_doom.mjs` (boot + menu + gameplay + save/load) |
184
+ | `doom/` | **DOOM 1 shareware** (doomgeneric F407 port, browser page `site/doom.html`) | `node site/test_doom_wasm.mjs` (boot + menu + gameplay + save/load) |
168
185
 
169
186
  Plus 17 more test binaries (`crypto_test`, `hal_test`, `timer_test`,
170
187
  `periph_test`, `echo_test`, `blink_serial`, `rx_interrupt_test`,
@@ -182,9 +199,9 @@ watchdog timeouts close to spec) will diverge — see docs/progress-and-future.m
182
199
  ## Architecture
183
200
 
184
201
  ```
185
- firmware .bin ──► Unicorn WASM CPU ──► memory hooks
202
+ firmware .bin ──► Rust WASM CPU (Thumb-2 + NVIC/exceptions)
186
203
  │
187
- periph_read/write ──► Rust peripheral model (WASM)
204
+ peripheral MMIO ──► Rust peripheral model (WASM)
188
205
  RCC USART GPIO DMA ETH TIM NVIC
189
206
  │
190
207
  UART out / ETH TX frames ──► driver (cli.mjs / site/emulator.js)
@@ -192,14 +209,18 @@ firmware .bin ──► Unicorn WASM CPU ──► memory hooks
192
209
  RX frames injected (netsim, or real gVisor gateway)
193
210
  ```
194
211
 
195
- - **CPU**: Unicorn 2.1.4 compiled to WASM executes Thumb-2 code; every
196
- read/write to a hooked MMIO range is routed into the Rust model, which
197
- answers by writing the modeled register value back into guest memory.
212
+ - **CPU**: a pure-Rust Cortex-M4 Thumb-2 interpreter compiled to WASM;
213
+ peripheral accesses call straight into the Rust model, and guest IRQs
214
+ (SysTick, ETH, USART, SVC/PendSV) are delivered inline with exact
215
+ exception stacking — no native deps, no JIT, no hooks.
198
216
  - **Peripherals**: a `wasm-bindgen` crate (`stm32-periph-wasm/`); registers
199
217
  and bit fields come from the vendor SVD (`monox/stm32f407.svd`).
200
- - **Ethernet**: TX is captured from the DMA descriptors; RX frames are
201
- injected into the RX ring and the firmware's `eth_irq_flag` (SRAM) drives
202
- polling — no interrupts required. Optionally, a Go gateway
218
+ - **Ethernet**: TX is captured from the DMA descriptors (OWN/FS/LS poll
219
+ demand); RX frames are delivered head-only with FS+LS status, IPHCE/PCE
220
+ checksum status, and PTP snapshots for event messages. The full MAC
221
+ register map is cross-checked against CMSIS `stm32f407xx.h` (DMASR/
222
+ DMAIER/MACFFR/MACPMTCTL positions — see [NETWORKING.md](NETWORKING.md)
223
+ §6 for the audit). Optionally, a Go gateway
203
224
  (`openhw-local-gateway/`) with a gVisor network stack makes the firmware
204
225
  talk to a real network: `node cli.mjs <fw.bin> <inst> --gateway`.
205
226
  - **Browser build** (`site/`): same modules as ESM; `site/emulator.js` is an
@@ -223,8 +244,8 @@ firmware .bin ──► Unicorn WASM CPU ──► memory hooks
223
244
  │ ├── test_blinky.mjs Node blinky GPIO test (npm test)
224
245
  │ ├── test_rx_interrupt.mjs Node UART-interrupt test (npm test)
225
246
  │ ├── test_component_*.mjs Component-API tests, one firmware each (npm test)
226
- │ ├── test_doom.mjs Node DOOM boot/menu/gameplay/save test
227
- │ └── vendor/ Browser WASM build, SVD, Unicorn
247
+ │ ├── test_doom_wasm.mjs Node DOOM boot/menu/gameplay/save test
248
+ │ └── vendor/ Browser WASM build (CPU + peripherals), SVD
228
249
  ├── index.mjs, package.json npm package entry (stm32f4-emu)
229
250
  ├── mcp/ MCP server (drive the emulator from an AI agent)
230
251
  ├── .github/workflows/ CI (Linux/Windows/macOS test matrix) + Pages deploy
@@ -257,7 +278,10 @@ rebuild — delete it so the vendor assets stay tracked/committed.
257
278
 
258
279
  ## Testing
259
280
 
260
- - `npm test` — flow test (`site/test_flow.mjs`) + blinky test
281
+ - `npm test` — flow test (`site/test_flow.mjs`) + Ethernet mock harnesses
282
+ (`site/test_eth_mock_model.mjs`: 18 checks, fake bindings + real driver;
283
+ `site/test_eth_mock_consumer.mjs`: 44 checks, fake firmware + real model)
284
+ + blinky test
261
285
  (`site/test_blinky.mjs`) + interrupt-UART test (`site/test_rx_interrupt.mjs`)
262
286
  + component-API tests (`site/test_component_{led,button,pwm,i2cregfile}.mjs`,
263
287
  each against real firmware — LED/blinky, Button/exti_test, Pwm/buzzer_test,
@@ -267,7 +291,7 @@ rebuild — delete it so the vendor assets stay tracked/committed.
267
291
  process (see docs/components.md). The same suite runs in CI on
268
292
  `ubuntu-latest`/`windows-latest`/`macos-latest`
269
293
  ([.github/workflows/ci.yml](.github/workflows/ci.yml)) on every push.
270
- - `node site/test_doom.mjs` — DOOM boot → menu → E1M1 gameplay + save/load
294
+ - `node site/test_doom_wasm.mjs` — DOOM boot → menu → E1M1 gameplay + save/load
271
295
  (see the DOOM section above).
272
296
  - `scripts/verify_ethernet.sh [max_inst]` — runs all three firmwares through
273
297
  the gateway, asserts the success markers and 0 `TCP fail`.
@@ -279,7 +303,7 @@ rebuild — delete it so the vendor assets stay tracked/committed.
279
303
  - [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).
280
304
  - [docs/architecture.md](docs/architecture.md) — how the emulator is put
281
305
  together (CPU, peripheral model, drivers, ETH flow, interrupts).
282
- - [docs/peripherals.md](docs/peripherals.md) — all 33 peripherals and the
306
+ - [docs/peripherals.md](docs/peripherals.md) — all 41 peripherals and the
283
307
  level each is implemented to, plus external devices and known gaps.
284
308
  - [docs/usage.md](docs/usage.md) — CLI, browser, and npm-library usage,
285
309
  config files, env vars, building.
@@ -296,6 +320,5 @@ rebuild — delete it so the vendor assets stay tracked/committed.
296
320
  ## License & Credits
297
321
 
298
322
  - **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
323
  - **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
324
  - **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 peripheral register reads/writes to stderr
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, unicorn: unicornFactory, svdXml, wasmInit: wasmBytes,
123
- extra_mem, verbose, lowpower,
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,55 @@
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
+ }
49
+
50
+ // Board LED map + host reset/boot control (see site/boards.js,
51
+ // site/emulator.js, site/stm32f4.js).
52
+ export interface BoardLed { bank: number; pin: number; label: string; }
53
+ export declare const BOARD_LED: Record<string, BoardLed>;
54
+ export declare const BOARD_LED_ALIASES: Record<string, BoardLed>;
55
+ export declare function boardLed(fwName: string, boardKey: string): BoardLed;
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, Unicorn) so Node consumers get a one-call setup.
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, unicorn: unicornFactory, svdXml, wasmInit: wasmBytes, ...opts,
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
- const out = new Uint8Array(bin.length);
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,8 @@ 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, unicorn: unicornFactory, svdXml, wasmInit: wasmBytes });
39
+ return createEmulator({ ...opts, firmware: bin, bindings, svdXml, wasmInit: wasmBytes });
45
40
  }
46
41
 
47
- export { createEmulator, createNetSim, FIRMWARES, bindings, unicornFactory, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, USART, DMAStream };
42
+ export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, USART, DMAStream };
43
+ export { boardLed, BOARD_LED, BOARD_LED_ALIASES } from './site/boards.js';
package/mcp/server.mjs CHANGED
@@ -252,4 +252,27 @@ server.registerTool('reset', {
252
252
  return text(`closed session (${was})`);
253
253
  });
254
254
 
255
+ server.registerTool('reset_cpu', {
256
+ description: 'Host reset button: CPU back to the vector-table SP/PC, peripherals keep state (like a real NRST pulse without the hold). Prefer over load_firmware when re-running the same image.',
257
+ inputSchema: {},
258
+ }, async () => {
259
+ const { emu } = requireSession();
260
+ if (typeof emu.resetCpu === 'function') emu.resetCpu();
261
+ else emu.reset();
262
+ return text('cpu reset to vector table');
263
+ });
264
+
265
+ server.registerTool('set_nrst', {
266
+ description: 'Hold (assert=true) or release the NRST line. While held, step() only advances the model clock — the CPU executes nothing, like real hardware in reset. Query with level omitted.',
267
+ inputSchema: { level: z.boolean().optional().describe('true = assert (hold in reset), false = release; omit to query') },
268
+ }, async ({ level }) => {
269
+ const { emu } = requireSession();
270
+ if (level === undefined) {
271
+ const s = typeof emu.isNrstAsserted === 'function' ? emu.isNrstAsserted() : false;
272
+ return text({ asserted: !!s });
273
+ }
274
+ const s = typeof emu.setNrst === 'function' ? emu.setNrst(level) : false;
275
+ return text({ asserted: !!s });
276
+ });
277
+
255
278
  await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stm32f4-emu",
3
- "version": "1.0.0",
4
- "description": "STM32F407 emulator: Unicorn (WASM) CPU + Rust peripheral model. Runs real Cortex-M4 firmware in Node or the browser.",
3
+ "version": "1.2.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,31 @@
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",
34
+ "site/boards.js",
31
35
  "site/components.js",
32
- "mcp/server.mjs",
33
36
  "site/netsim.js",
34
37
  "site/firmware.js",
35
38
  "site/loaders.js",
36
- "site/index.html",
37
- "site/app.js",
38
39
  "site/remote-emu.js",
39
40
  "site/ws-bridge.mjs",
40
- "site/vendor/"
41
+ "site/app.js",
42
+ "site/index.html",
43
+ "site/console.html",
44
+ "site/vendor/",
45
+ "mcp/server.mjs",
46
+ "tools/make_firmware.mjs"
41
47
  ],
42
48
  "keywords": [
43
49
  "stm32",
@@ -45,7 +51,6 @@
45
51
  "emulator",
46
52
  "arm",
47
53
  "cortex-m4",
48
- "unicorn",
49
54
  "wasm",
50
55
  "embedded",
51
56
  "firmware"
@@ -63,8 +68,8 @@
63
68
  "node": ">=20"
64
69
  },
65
70
  "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",
67
- "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": "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_eth_mock_model.mjs && node site/test_eth_mock_consumer.mjs && node site/test_periph_mock_consumer.mjs && node site/test_blinky.mjs && node site/test_blinky_f401.mjs && node site/test_blinky_f411.mjs && node site/test_blinky_f407g.mjs && node site/test_blinky_nucleo_f401.mjs && node site/test_blinky_nucleo_f411.mjs && node site/test_blinky_f429.mjs && node site/test_blinky_f407ve.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_arduino_boards.mjs && node site/test_exti.mjs && node site/test_flash.mjs && node site/test_spi_flash.mjs && node site/test_board_matrix.mjs && node site/test_qspi_cdp.mjs && node site/test_browser.mjs",
72
+ "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_blinky_f401.mjs && node site/test_blinky_f411.mjs && node site/test_blinky_f407g.mjs && node site/test_blinky_nucleo_f401.mjs && node site/test_blinky_nucleo_f411.mjs && node site/test_blinky_f429.mjs && node site/test_blinky_f407ve.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
73
  "test:bridge": "node site/test_ws_bridge.mjs",
69
74
  "test:bridge:browser": "node site/test_bridge_cdp.mjs",
70
75
  "test:flow": "node site/test_flow.mjs",
@@ -78,7 +83,12 @@
78
83
  "test:wwdgwin": "node site/test_wwdg_window.mjs",
79
84
  "test:timcap": "node site/test_tim_capture.mjs",
80
85
  "test:lowpower": "node site/test_lowpower.mjs",
86
+ "test:standby": "node site/test_standby.mjs",
81
87
  "test:qspi": "node site/test_qspi.mjs",
88
+ "test:eth:mock": "node site/test_eth_mock_model.mjs && node site/test_eth_mock_consumer.mjs && node site/test_periph_mock_consumer.mjs",
89
+ "test:fpu": "node site/test_fpu.mjs",
90
+ "test:fpuirq": "node site/test_fpu_irq.mjs",
91
+ "test:mpu": "node site/test_mpu.mjs",
82
92
  "test:qspi:browser": "node site/test_qspi_cdp.mjs",
83
93
  "test:browser": "node site/test_browser.mjs",
84
94
  "test:edge": "node site/test_edge_cases.mjs",
@@ -89,7 +99,15 @@
89
99
  "bridge": "node site/ws-bridge.mjs",
90
100
  "firmwares": "node tools/make_firmware.mjs",
91
101
  "serve": "python3 -m http.server 8123 --directory site",
92
- "prepack": "node tools/make_firmware.mjs"
102
+ "prepack": "node tools/make_firmware.mjs",
103
+ "typecheck": "tsc --noEmit",
104
+ "lint": "eslint site mcp tools index.mjs cli.mjs",
105
+ "format": "prettier --check .",
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'",
108
+ "build:wasm:node": "wasm-pack build stm32-periph-wasm --target nodejs --out-dir pkg",
109
+ "build:wasm:opt": "wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm",
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"
93
111
  },
94
112
  "dependencies": {
95
113
  "ws": "^8.21.3"
@@ -109,6 +127,9 @@
109
127
  "devDependencies": {
110
128
  "@modelcontextprotocol/sdk": "^1.30.0",
111
129
  "playwright": "^1.62.1",
112
- "zod": "^3.25 || ^4.0"
130
+ "zod": "^3.25 || ^4.0",
131
+ "typescript": "^5.6.0",
132
+ "eslint": "^9.0.0",
133
+ "prettier": "^3.0.0"
113
134
  }
114
135
  }