stm32f4-emu 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/cli.mjs +16 -2
- package/index.d.ts +387 -2
- package/index.mjs +23 -7
- package/mcp/server.mjs +6 -3
- package/package.json +3 -3
- package/site/app.js +47 -21
- package/site/boards.js +8 -0
- package/site/components.js +258 -3
- package/site/console.html +1 -1
- package/site/emulator.js +4 -0
- package/site/firmware.js +1 -1
- package/site/loaders.js +15 -10
- package/site/stm32f4.js +1383 -39
- package/site/vendor/stm32_periph_wasm_bg.wasm +0 -0
- package/site/ws-bridge.mjs +16 -3
- package/tools/make_firmware.mjs +224 -224
package/README.md
CHANGED
|
@@ -90,13 +90,13 @@ npm run serve # then open http://127.0.0.1:8123
|
|
|
90
90
|
npm test # == node site/test_flow.mjs
|
|
91
91
|
|
|
92
92
|
# websocket bridge: headless Node serves the emulator, browser is a thin UI
|
|
93
|
-
npm run bridge -- blinky/blinky.bin --port 8234
|
|
93
|
+
npm run bridge -- firmware/blinky/blinky.bin --port 8234
|
|
94
94
|
# then open http://127.0.0.1:8123/console.html?bridge=ws://127.0.0.1:8234
|
|
95
95
|
|
|
96
96
|
# gateway-backed run: firmware talks to a REAL network stack (gVisor)
|
|
97
97
|
cd stm32-periph-wasm/pkg
|
|
98
|
-
node cli.mjs ../../eth_http/eth_http.bin 10000000 \
|
|
99
|
-
--gateway --config=../../eth_http/config.yaml
|
|
98
|
+
node cli.mjs ../../firmware/eth_http/eth_http.bin 10000000 \
|
|
99
|
+
--gateway --config=../../firmware/eth_http/config.yaml
|
|
100
100
|
# (requires an HTTP server at 127.0.0.1:8092; see AGENTS.md §10)
|
|
101
101
|
```
|
|
102
102
|
|
|
@@ -132,6 +132,11 @@ custom SPI/I2C devices) to pins and buses with the component API —
|
|
|
132
132
|
`ext_devices.spiDevices`/`i2cDevices` for your own bus protocol. See
|
|
133
133
|
[docs/components.md](docs/components.md).
|
|
134
134
|
|
|
135
|
+
For platform integration (Wokwi/OpenHW/Velxio) use the `STM32F4` facade —
|
|
136
|
+
factories, GPIO/USART/SPI/I2C objects, polled event callbacks, DMA
|
|
137
|
+
controller, live displays, SWD/JTAG/power/probe helpers. See
|
|
138
|
+
[docs/facade.md](docs/facade.md).
|
|
139
|
+
|
|
135
140
|
## Drive it from an AI agent (MCP)
|
|
136
141
|
|
|
137
142
|
`mcp/server.mjs` exposes the emulator over the Model Context Protocol —
|
|
@@ -155,7 +160,7 @@ is a thin UI. Zero impact on the existing local WASM path:
|
|
|
155
160
|
|
|
156
161
|
```bash
|
|
157
162
|
# 1. Start the bridge in Node (serves the emulator over WS on port 8234)
|
|
158
|
-
node site/ws-bridge.mjs eth_http/eth_http.bin --port 8234
|
|
163
|
+
node site/ws-bridge.mjs firmware/eth_http/eth_http.bin --port 8234
|
|
159
164
|
|
|
160
165
|
# 2. Open the browser console with the bridge URL param
|
|
161
166
|
open "http://127.0.0.1:8123/console.html?bridge=ws://127.0.0.1:8234"
|
|
@@ -214,7 +219,7 @@ firmware .bin ──► Rust WASM CPU (Thumb-2 + NVIC/exceptions)
|
|
|
214
219
|
(SysTick, ETH, USART, SVC/PendSV) are delivered inline with exact
|
|
215
220
|
exception stacking — no native deps, no JIT, no hooks.
|
|
216
221
|
- **Peripherals**: a `wasm-bindgen` crate (`stm32-periph-wasm/`); registers
|
|
217
|
-
and bit fields come from the vendor SVD (`
|
|
222
|
+
and bit fields come from the vendor SVD (`site/vendor/stm32f407.svd`).
|
|
218
223
|
- **Ethernet**: TX is captured from the DMA descriptors (OWN/FS/LS poll
|
|
219
224
|
demand); RX frames are delivered head-only with FS+LS status, IPHCE/PCE
|
|
220
225
|
checksum status, and PTP snapshots for event messages. The full MAC
|
|
@@ -249,13 +254,13 @@ firmware .bin ──► Rust WASM CPU (Thumb-2 + NVIC/exceptions)
|
|
|
249
254
|
├── index.mjs, package.json npm package entry (stm32f4-emu)
|
|
250
255
|
├── mcp/ MCP server (drive the emulator from an AI agent)
|
|
251
256
|
├── .github/workflows/ CI (Linux/Windows/macOS test matrix) + Pages deploy
|
|
252
|
-
├── tools/make_firmware.mjs Regenerates site/firmware.js from
|
|
257
|
+
├── tools/make_firmware.mjs Regenerates site/firmware.js from firmware/*/.bin
|
|
253
258
|
├── stm32-periph-wasm/ Rust peripheral model (WASM build + pkg/)
|
|
254
|
-
├── eth_http/ eth_dhcp/ eth_test/ Sample network firmwares + configs
|
|
255
|
-
├── doom/
|
|
259
|
+
├── firmware/eth_http/ eth_dhcp/ eth_test/ Sample network firmwares + configs
|
|
260
|
+
├── firmware/doom/ DOOM 1 port (doomgeneric f407 target + WAD path)
|
|
256
261
|
├── openhw-local-gateway/ Go gateway (gVisor network stack)
|
|
257
262
|
├── scripts/verify_ethernet.sh Regression runner for all three firmwares
|
|
258
|
-
├──
|
|
263
|
+
├── hardware/monox/, hardware/saturn/ Upstream printer firmwares (heritage)
|
|
259
264
|
└── AGENTS.md Full architecture, build steps, runbook
|
|
260
265
|
```
|
|
261
266
|
|
|
@@ -267,10 +272,9 @@ cd stm32-periph-wasm && wasm-pack build --release --target nodejs
|
|
|
267
272
|
|
|
268
273
|
# firmware (bare-metal Makefiles; toolchain from the Arduino core)
|
|
269
274
|
TOOLCHAIN="$HOME/.arduino15/packages/STMicroelectronics/tools/xpack-arm-none-eabi-gcc/14.2.1-1.1/bin/arm-none-eabi-" \
|
|
270
|
-
make -C eth_http
|
|
275
|
+
make -C firmware/eth_http # also eth_dhcp, eth_test (all firmware lives in firmware/)
|
|
271
276
|
|
|
272
|
-
# native SDL emulator
|
|
273
|
-
cd stm32-emulator-main && cargo build --release
|
|
277
|
+
# (no native SDL emulator in this tree — the headless WASM path above is the only backend)
|
|
274
278
|
```
|
|
275
279
|
|
|
276
280
|
`wasm-pack` writes `site/vendor/.gitignore` containing `*` after a browser
|
package/cli.mjs
CHANGED
|
@@ -15,9 +15,11 @@ import { dirname, resolve } from 'node:path';
|
|
|
15
15
|
import * as bindings from './site/vendor/stm32_periph_wasm.js';
|
|
16
16
|
import { createEmulator } from './site/emulator.js';
|
|
17
17
|
import { parseIntelHex, parseElf } from './site/loaders.js';
|
|
18
|
+
import { CHIPS, chipInfo } from './site/stm32f4.js';
|
|
18
19
|
|
|
19
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const
|
|
21
|
+
const svdCache = {};
|
|
22
|
+
const svdFor = (f) => svdCache[f] || (svdCache[f] = readFileSync(resolve(__dirname, 'site/vendor/' + f), 'utf8'));
|
|
21
23
|
const wasmBytes = new Uint8Array(readFileSync(resolve(__dirname, 'site/vendor/stm32_periph_wasm_bg.wasm')));
|
|
22
24
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8'));
|
|
23
25
|
|
|
@@ -32,6 +34,7 @@ Arguments:
|
|
|
32
34
|
Options:
|
|
33
35
|
-n, --inst <N> instruction budget to run (default 20000000)
|
|
34
36
|
-f, --format <fmt> firmware format: auto|bin|hex|elf (default auto)
|
|
37
|
+
-c, --chip <chip> chip: stm32f401|stm32f411|stm32f407|stm32f407ve|stm32f429 (default stm32f407)
|
|
35
38
|
-v, --verbose trace guest PCs to stderr (capped per step)
|
|
36
39
|
-l, --lowpower halt on WFI/WFE and advance the virtual RTC until wakeup
|
|
37
40
|
-h, --help show this help
|
|
@@ -62,6 +65,7 @@ async function main() {
|
|
|
62
65
|
let firmwarePath = null;
|
|
63
66
|
let inst = 20000000;
|
|
64
67
|
let format = 'auto';
|
|
68
|
+
let chipKey = 'stm32f407';
|
|
65
69
|
let verbose = false;
|
|
66
70
|
let lowpower = false;
|
|
67
71
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -81,6 +85,11 @@ async function main() {
|
|
|
81
85
|
if (!['auto', 'bin', 'hex', 'elf'].includes(format)) fail(`invalid --format: ${format}`);
|
|
82
86
|
continue;
|
|
83
87
|
}
|
|
88
|
+
if (a === '-c' || a === '--chip') {
|
|
89
|
+
chipKey = String(args[++i] || '').toLowerCase();
|
|
90
|
+
if (!CHIPS[chipKey]) fail(`invalid --chip: ${args[i]} (have: ${Object.keys(CHIPS).join(', ')})`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
84
93
|
if (a.startsWith('-')) fail(`unknown option: ${a}`);
|
|
85
94
|
if (firmwarePath) fail('multiple firmware paths given');
|
|
86
95
|
firmwarePath = a;
|
|
@@ -115,8 +124,13 @@ async function main() {
|
|
|
115
124
|
|
|
116
125
|
let emu;
|
|
117
126
|
try {
|
|
127
|
+
const chip = chipInfo(chipKey);
|
|
118
128
|
emu = await createEmulator({
|
|
119
|
-
firmware, bindings,
|
|
129
|
+
firmware, bindings,
|
|
130
|
+
svdXml: svdFor(chip.svd), svdFile: chip.svd,
|
|
131
|
+
wasmInit: wasmBytes,
|
|
132
|
+
flash_size: chip.flash_size, ram_size: chip.ram_size,
|
|
133
|
+
chipHint: chip.svd.replace(/\.svd$/, ''),
|
|
120
134
|
extra_mem, lowpower,
|
|
121
135
|
});
|
|
122
136
|
} catch (e) {
|
package/index.d.ts
CHANGED
|
@@ -15,10 +15,343 @@ export function createNetSim(opts?: any): any;
|
|
|
15
15
|
|
|
16
16
|
export const FIRMWARES: Record<string, { bytes: string; [key: string]: unknown }>;
|
|
17
17
|
export const bindings: any;
|
|
18
|
-
export const unicornFactory: any;
|
|
19
18
|
export const svdXml: string;
|
|
19
|
+
// Removed: unicornFactory (Unicorn backend deleted — Rust CPU is sole backend).
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// STM32F4 high-level facade (see site/stm32f4.js). F1-API parity surface:
|
|
22
|
+
// factories, execute/step results, GPIO/USART/SPI/I2C classes, symbol
|
|
23
|
+
// helpers, and engine pass-throughs. TX is a single shared model buffer
|
|
24
|
+
// (only usart1.onData/output fire); event callbacks beyond SPI/I2C taps
|
|
25
|
+
// are absent by design (no core event queue on F4).
|
|
26
|
+
export interface ExecuteResult { instCount: number; stopped: boolean; }
|
|
27
|
+
export interface FacadeStepResult { pc: number; instCount: number; stopped: boolean; }
|
|
28
|
+
export class GPIOPin {
|
|
29
|
+
constructor(mcu: STM32F4, port: string, pin: number);
|
|
30
|
+
on(event: 'change', cb: (high: boolean) => void): () => void;
|
|
31
|
+
addListener(cb: (high: boolean) => void): () => void;
|
|
32
|
+
read(): boolean;
|
|
33
|
+
readInput(): boolean;
|
|
34
|
+
setInput(high: boolean): void;
|
|
35
|
+
setInputValue(high: boolean): void;
|
|
36
|
+
detach(): void;
|
|
37
|
+
}
|
|
38
|
+
export class GPIO {
|
|
39
|
+
constructor(mcu: STM32F4);
|
|
40
|
+
pin(port: string | number, pin: number): GPIOPin;
|
|
41
|
+
}
|
|
42
|
+
export interface ChipRecord {
|
|
43
|
+
key: string;
|
|
44
|
+
svd: string;
|
|
45
|
+
flash_size: number;
|
|
46
|
+
ram_size: number;
|
|
47
|
+
maxClockMHz: number;
|
|
48
|
+
label: string;
|
|
49
|
+
idcode: number;
|
|
50
|
+
usarts: number[];
|
|
51
|
+
timers: number[];
|
|
52
|
+
can: number[];
|
|
53
|
+
dac: boolean;
|
|
54
|
+
ltdc: boolean;
|
|
55
|
+
gpioBanks: number;
|
|
56
|
+
spi: number[];
|
|
57
|
+
i2c: number[];
|
|
58
|
+
uart78: number[];
|
|
59
|
+
i2s: number[];
|
|
60
|
+
sai: number[];
|
|
61
|
+
sdio: boolean;
|
|
62
|
+
dcmi: boolean;
|
|
63
|
+
crc: boolean;
|
|
64
|
+
cryp: boolean;
|
|
65
|
+
hash: boolean;
|
|
66
|
+
rng: boolean;
|
|
67
|
+
fsmc: boolean;
|
|
68
|
+
dma2d: boolean;
|
|
69
|
+
qspi: boolean;
|
|
70
|
+
}
|
|
71
|
+
export declare const CHIPS: Record<string, Omit<ChipRecord, 'key'>>;
|
|
72
|
+
export declare function chipInfo(key?: string): ChipRecord;
|
|
73
|
+
export class USART {
|
|
74
|
+
readonly n: number;
|
|
75
|
+
onData: ((byte: number) => void) | null;
|
|
76
|
+
constructor(mcu: STM32F4, n: number);
|
|
77
|
+
send(data: string | Uint8Array | number[]): void;
|
|
78
|
+
sendData(data: string | Uint8Array | number[]): void;
|
|
79
|
+
get output(): string;
|
|
80
|
+
}
|
|
81
|
+
export class SPI {
|
|
82
|
+
readonly ch: number;
|
|
83
|
+
onTransfer: ((channel: string, tx: number[], rx: number[]) => void) | null;
|
|
84
|
+
constructor(mcu: STM32F4, ch: number);
|
|
85
|
+
injectMiso(bytes: Uint8Array | number[]): void;
|
|
86
|
+
}
|
|
87
|
+
export class I2C {
|
|
88
|
+
readonly ch: number;
|
|
89
|
+
onStart: ((addr: number, isRead: boolean) => void) | null;
|
|
90
|
+
onWrite: ((byte: number) => void) | null;
|
|
91
|
+
onRead: (() => void) | null;
|
|
92
|
+
onStop: (() => void) | null;
|
|
93
|
+
constructor(mcu: STM32F4, ch: number);
|
|
94
|
+
injectRx(bytes: Uint8Array | number[]): void;
|
|
95
|
+
}
|
|
96
|
+
export class DMAStream {
|
|
97
|
+
constructor(mcu: STM32F4, streamIndex: number);
|
|
98
|
+
pendingCount(): number;
|
|
99
|
+
setCompleted(success?: boolean): void;
|
|
100
|
+
tcif(): boolean;
|
|
101
|
+
htif(): boolean;
|
|
102
|
+
cr(): number;
|
|
103
|
+
ndtr(): number;
|
|
104
|
+
}
|
|
105
|
+
export class DMAController {
|
|
106
|
+
constructor(mcu: STM32F4, controller?: 1 | 2);
|
|
107
|
+
lisr(): number;
|
|
108
|
+
hisr(): number;
|
|
109
|
+
tcif(s: number): boolean;
|
|
110
|
+
htif(s: number): boolean;
|
|
111
|
+
cr(s: number): number;
|
|
112
|
+
ndtr(s: number): number;
|
|
113
|
+
par(s: number): number;
|
|
114
|
+
m0ar(s: number): number;
|
|
115
|
+
m1ar(s: number): number;
|
|
116
|
+
fcr(s: number): number;
|
|
117
|
+
stream(s: number): DMAStream;
|
|
118
|
+
}
|
|
119
|
+
export class Display {
|
|
120
|
+
constructor(mcu: STM32F4);
|
|
121
|
+
get oled(): { w: number; h: number; fb: Uint8Array; frame: number } | null;
|
|
122
|
+
get tft(): { w: number; h: number; fb: Uint8Array; frame: () => number } | null;
|
|
123
|
+
ltdc(): { w: number; h: number; pf: string; fb: Uint8Array; addr: number; pitch: number } | null;
|
|
124
|
+
}
|
|
125
|
+
export class STM32F4 {
|
|
126
|
+
readonly gpio: GPIO;
|
|
127
|
+
readonly usart: USART;
|
|
128
|
+
// Slots for absent-silicon USARTs are null (F401/F411: 3,4,5).
|
|
129
|
+
readonly usart1: USART; readonly usart2: USART; readonly usart3: USART | null;
|
|
130
|
+
readonly usart4: USART | null; readonly usart5: USART | null; readonly usart6: USART;
|
|
131
|
+
readonly usart7: USART | null; readonly usart8: USART | null;
|
|
132
|
+
readonly usarts: Record<number, USART>;
|
|
133
|
+
readonly spi1: SPI | null; readonly spi2: SPI; readonly spi3: SPI;
|
|
134
|
+
readonly spi4: SPI | null; readonly spi5: SPI | null; readonly spi6: SPI | null;
|
|
135
|
+
readonly spiBus: Record<number, SPI>;
|
|
136
|
+
readonly i2c1: I2C; readonly i2c2: I2C; readonly i2c3: I2C;
|
|
137
|
+
readonly i2cBus: Record<number, I2C>;
|
|
138
|
+
readonly spi: { specs: any[]; pushMiso(peripheral: string, bytes: Uint8Array | number[]): void };
|
|
139
|
+
readonly i2c: { specs: any[]; pushRx(peripheral: string, bytes: Uint8Array | number[]): void };
|
|
140
|
+
readonly dma: { stream(index: number): DMAStream; controller(ctl: 1 | 2): DMAController };
|
|
141
|
+
readonly display: Display;
|
|
142
|
+
readonly chip: ChipRecord;
|
|
143
|
+
readonly chipSvdXml: string | null;
|
|
144
|
+
// Wokwi/OpenHW/Velxio integration callbacks (polled dispatch, see site/stm32f4.js).
|
|
145
|
+
onExtiEdge: ((line: number) => void) | null;
|
|
146
|
+
onCanTx: ((can: number) => void) | null;
|
|
147
|
+
onCanRx: ((can: number, id: number, len: number, data: number[]) => void) | null;
|
|
148
|
+
onTimUpdate: ((tim: number) => void) | null;
|
|
149
|
+
onTimCapture: ((tim: number, ch: number, val: number) => void) | null;
|
|
150
|
+
onDmaTc: ((controller: number, stream: number) => void) | null;
|
|
151
|
+
onWdogReset: ((which: number) => void) | null;
|
|
152
|
+
onUsbIn: ((ep: number, data: number[]) => void) | null;
|
|
153
|
+
onUsbHsIn: ((ep: number, data: number[]) => void) | null;
|
|
154
|
+
onItmByte: ((port: number, byte: number) => void) | null;
|
|
155
|
+
onFsmcAccess: ((bank: number, off: number, write: boolean, size: number, val: number) => void) | null;
|
|
156
|
+
onAdcDone: ((adc: number, chan: number, value: number) => void) | null;
|
|
157
|
+
onDacWrite: ((chan: number, value: number) => void) | null;
|
|
158
|
+
onCrcResult: ((value: number) => void) | null;
|
|
159
|
+
onRtcAlarm: ((which: number) => void) | null;
|
|
160
|
+
onI2cAlert: ((peripheral: number, asserted: boolean) => void) | null;
|
|
161
|
+
onHostTx: ((...args: any[]) => void) | null;
|
|
162
|
+
onHostRx: ((...args: any[]) => void) | null;
|
|
163
|
+
constructor(emu: any, bindings?: any, chip?: ChipRecord, chipSvdXml?: string | null);
|
|
164
|
+
static create(opts?: any): Promise<STM32F4>;
|
|
165
|
+
static fromELF(buf: Uint8Array | ArrayBuffer, opts?: any): Promise<STM32F4>;
|
|
166
|
+
static fromBin(buf: Uint8Array | ArrayBuffer, opts?: any): Promise<STM32F4>;
|
|
167
|
+
static fromHex(text: string, opts?: any): Promise<STM32F4>;
|
|
168
|
+
loadBin(bytes: Uint8Array | ArrayBuffer, base?: number): void;
|
|
169
|
+
loadHex(text: string): void;
|
|
170
|
+
loadELF(bytes: Uint8Array | ArrayBuffer): void;
|
|
171
|
+
// Live peripheral injection (anytime unless noted).
|
|
172
|
+
setAdcChannel(peripheral: string, channel: number, value: number): void;
|
|
173
|
+
clearAdcChannel(peripheral: string, channel: number): void;
|
|
174
|
+
takeAdcDma(): number[];
|
|
175
|
+
canInject(id: number, dlc: number, data: Uint8Array | number[]): void;
|
|
176
|
+
canInjectFd(id: number, data: Uint8Array | number[], brs?: boolean): void;
|
|
177
|
+
timInjectCapture(timer: number, ch: number): void;
|
|
178
|
+
timPwmPulseUs(timer: number, ch: number, clockHz: number): number;
|
|
179
|
+
timOcMode(timer: number, ch: number): number;
|
|
180
|
+
timMoe(timer: number): boolean;
|
|
181
|
+
timEncoderStep(timer: number, ti: number, rising: boolean): void;
|
|
182
|
+
timBreakInput(timer: number, asserted: boolean): void;
|
|
183
|
+
dacTrigger(ch: number, src: number, dmaStaged?: boolean): void;
|
|
184
|
+
dacUnderrun(ch: number): boolean;
|
|
185
|
+
takeSpeakerSamples(): Float32Array;
|
|
186
|
+
usbInjectSetup(bytes: Uint8Array | number[]): boolean;
|
|
187
|
+
usbInjectOut(ep: number, bytes: Uint8Array | number[]): boolean;
|
|
188
|
+
usbTakeIn(ep: number): number[];
|
|
189
|
+
usbReset(): void;
|
|
190
|
+
usbEnumerated(): void;
|
|
191
|
+
usbHsInjectSetup(bytes: Uint8Array | number[]): boolean;
|
|
192
|
+
usbHsInjectOut(ep: number, bytes: Uint8Array | number[]): boolean;
|
|
193
|
+
usbHsTakeIn(ep: number): number[];
|
|
194
|
+
usbHsReset(): void;
|
|
195
|
+
usbHsEnumerated(): void;
|
|
196
|
+
ethInject(frame: Uint8Array | number[]): void;
|
|
197
|
+
// Peripheral-domain helpers (ETH status, DMA2D jobs, NAND, trace/FPU).
|
|
198
|
+
ethMacAddr(): bigint;
|
|
199
|
+
ethTxDeferred(): boolean;
|
|
200
|
+
ethCollisionArmed(): boolean;
|
|
201
|
+
ethArmCollision(): void;
|
|
202
|
+
ethPauseTx(): number;
|
|
203
|
+
ethBackoffSlots(attempt: number, seed: number): number;
|
|
204
|
+
dma2dTakeJob(): number[];
|
|
205
|
+
dma2dJobDone(): void;
|
|
206
|
+
dma2dFifoThreshold(dma: string, stream: number): number;
|
|
207
|
+
dmaStreamCt(dma: string, stream: number): boolean;
|
|
208
|
+
dmaStreamFeif(dma: string, stream: number): boolean;
|
|
209
|
+
fsmcBindNand(bank: number, size: number): void;
|
|
210
|
+
fsmcNandErase(bank: number, offset: number, len: number): void;
|
|
211
|
+
traceStart(): void;
|
|
212
|
+
traceStop(): void;
|
|
213
|
+
takeTrace(): number[];
|
|
214
|
+
getFpuState(): any;
|
|
215
|
+
setSreg(i: number, v: number): void;
|
|
216
|
+
setFpscr(v: number): void;
|
|
217
|
+
wdogRequested(): boolean;
|
|
218
|
+
wdogClearFlags(): void;
|
|
219
|
+
wdogIwdgFlag(): boolean;
|
|
220
|
+
wdogWwdgFlag(): boolean;
|
|
221
|
+
flashProgramming(): boolean;
|
|
222
|
+
// Protocol helpers (USB status/VBUS, UART LIN/SMARTCARD/IrDA, CAN error/FD).
|
|
223
|
+
usbInStatus(ep: number): number;
|
|
224
|
+
usbOutStatus(ep: number): number;
|
|
225
|
+
usbHsInStatus(ep: number): number;
|
|
226
|
+
usbHsOutStatus(ep: number): number;
|
|
227
|
+
usbSetVbus(present: boolean): void;
|
|
228
|
+
usbHsSetVbus(present: boolean): void;
|
|
229
|
+
usbFrame(): number;
|
|
230
|
+
usbHsFrame(): number;
|
|
231
|
+
usbUlpiRate(): number;
|
|
232
|
+
usbHsUlpiRate(): number;
|
|
233
|
+
usbDmaProgress(ep: number): any;
|
|
234
|
+
uartBreakTx(base: number): void;
|
|
235
|
+
uartIrdaRx(base: number, byte: number, lowPower?: boolean): void;
|
|
236
|
+
uartIrdaTxClass(base: number): number;
|
|
237
|
+
uartSmartcardNack(base: number): void;
|
|
238
|
+
uartSmartcardRetries(base: number): number;
|
|
239
|
+
canNoteError(base: number, lec: number, recover?: boolean): void;
|
|
240
|
+
canFdLen(base: number, fifo: number, slot: number): number;
|
|
241
|
+
canFdByte(base: number, fifo: number, slot: number, idx: number): number;
|
|
242
|
+
canFdCost(base: number, fd: boolean, brs: boolean, payloadBytes: number): bigint;
|
|
243
|
+
i2cPec(base: number): number;
|
|
244
|
+
spiFlashDebug(peripheral: string): number[];
|
|
245
|
+
sdioCardIrq(set: boolean): void;
|
|
246
|
+
rtcTamper(): void;
|
|
247
|
+
audioLoadWav(bytes: Uint8Array | number[]): void;
|
|
248
|
+
ltdcClutEntry(layer: number, idx: number): number;
|
|
249
|
+
ltdcLutPixel(layer: number, pf: number, byte: number): number;
|
|
250
|
+
// Fault / model-harness methods (mirror wasm exports 1:1).
|
|
251
|
+
uartSetCts(base: number, asserted: boolean): void;
|
|
252
|
+
uartFaultRx(base: number, fe: boolean, pe: boolean): void;
|
|
253
|
+
uartIdle(base: number): void;
|
|
254
|
+
uartLinBreak(base: number): void;
|
|
255
|
+
uartBreakPending(base: number): boolean;
|
|
256
|
+
uartTxLen(base: number): number;
|
|
257
|
+
uartMuted(base: number): boolean;
|
|
258
|
+
spiFaultCrc(base: number): void;
|
|
259
|
+
spiFaultModf(base: number): void;
|
|
260
|
+
spiSlaveSelect(base: number, asserted: boolean): void;
|
|
261
|
+
spiSlaveClock(base: number, mosi: number): number;
|
|
262
|
+
spiSlaveGate(base: number): boolean;
|
|
263
|
+
i2cArmArbLoss(base: number): void;
|
|
264
|
+
i2cArmSmbusAlert(base: number, addr: number): void;
|
|
265
|
+
i2cSlaveAddress(base: number, addr: number, isRead: boolean): boolean;
|
|
266
|
+
i2cSlaveWrite(base: number, byte: number): void;
|
|
267
|
+
i2cSlaveRead(base: number): number;
|
|
268
|
+
i2cSlaveStop(base: number): void;
|
|
269
|
+
i2cSlaveStatus(base: number): number;
|
|
270
|
+
sdioFaultDataCrc(): void;
|
|
271
|
+
rccInjectFailure(srcMask: number, dead: boolean): void;
|
|
272
|
+
flashRdpLevel(): number;
|
|
273
|
+
flashSetRdp(levelByte: number): void;
|
|
274
|
+
rngSeedEntropy(words: number[] | Uint32Array): void;
|
|
275
|
+
rngEntropyAvail(): number;
|
|
276
|
+
rtcTamperPin(level: boolean): void;
|
|
277
|
+
rtcTimestamp(): void;
|
|
278
|
+
setIntrPending(irq: number): void;
|
|
279
|
+
hasPendingInterrupt(): boolean;
|
|
280
|
+
// Scope probes (read-only model state).
|
|
281
|
+
adcDualLatched(): boolean;
|
|
282
|
+
ethPpsCount(): number;
|
|
283
|
+
ethPpsLevel(): boolean;
|
|
284
|
+
ethLinkUp(): boolean;
|
|
285
|
+
ethSetLink(up: boolean): void;
|
|
286
|
+
ltdcScanline(): number;
|
|
287
|
+
ltdcFrameCount(): number;
|
|
288
|
+
audioRemaining(): number;
|
|
289
|
+
audioClear(): void;
|
|
290
|
+
dcmiSync(vsync: boolean, hsync: boolean, pclkDiv: number): void;
|
|
291
|
+
qspiMmapLive(): boolean;
|
|
292
|
+
qspiMmapRead(offset: number): number;
|
|
293
|
+
sdioBusWidth(): number;
|
|
294
|
+
sdioCardBlocks(): number;
|
|
295
|
+
sdioReadBlock(block: number): number[];
|
|
296
|
+
takeItm(port: number): number[];
|
|
297
|
+
itmPending(port: number): number;
|
|
298
|
+
takeFsmc(bank: number): number[];
|
|
299
|
+
pushFsmc(bank: number, values: number[] | Uint32Array): void;
|
|
300
|
+
regfileGet(peripheral: string, offset: number): number;
|
|
301
|
+
regfileSet(peripheral: string, offset: number, value: number): void;
|
|
302
|
+
get oled(): any;
|
|
303
|
+
get tft(): any;
|
|
304
|
+
get rtc(): any;
|
|
305
|
+
get buzzer(): any;
|
|
306
|
+
get camera(): any;
|
|
307
|
+
execute(cycles?: number): ExecuteResult;
|
|
308
|
+
step(cycles?: number): FacadeStepResult;
|
|
309
|
+
uartRx(byte: number): boolean;
|
|
310
|
+
get uartOutput(): string;
|
|
311
|
+
setSymbols(mapText: string): number;
|
|
312
|
+
resolveSymbol(pc: number): string | null;
|
|
313
|
+
read32(addr: number): number;
|
|
314
|
+
write32(addr: number, val: number): void;
|
|
315
|
+
memRead32(addr: number): number;
|
|
316
|
+
memWriteBytes(addr: number, bytes: Uint8Array | number[]): void;
|
|
317
|
+
periphRead(addr: number, width?: number): number;
|
|
318
|
+
periphWrite(addr: number, width: number, value: number): void;
|
|
319
|
+
addJsPeripheral(base: number, size: number, read: (addr: number, size: number) => number, write: (addr: number, value: number, size: number) => void): boolean;
|
|
320
|
+
pwrMode(): number;
|
|
321
|
+
pwrEstimate(): number;
|
|
322
|
+
swdHalted(): boolean;
|
|
323
|
+
swdHalt(): void;
|
|
324
|
+
swdResume(): void;
|
|
325
|
+
swdStep(): number;
|
|
326
|
+
swdAddWatch(kind: number, addr: number, len: number): number;
|
|
327
|
+
swdRemoveWatch(slot: number): void;
|
|
328
|
+
swdTakeTrip(): number[];
|
|
329
|
+
swdDpRead(addr: number): number;
|
|
330
|
+
swdDpWrite(addr: number, value: number): void;
|
|
331
|
+
swdApRead(bank: number, reg: number): number;
|
|
332
|
+
swdApWrite(bank: number, reg: number, value: number): void;
|
|
333
|
+
swdRegRead(idx: number): number;
|
|
334
|
+
swdRegWrite(idx: number, value: number): void;
|
|
335
|
+
jtagReset(): void;
|
|
336
|
+
jtagIr(ir: number): void;
|
|
337
|
+
jtagIdcode(): number;
|
|
338
|
+
jtagDp(addr: number, rnw: boolean, wdata: number): number;
|
|
339
|
+
jtagAp(bank: number, reg: number, rnw: boolean, wdata: number): number;
|
|
340
|
+
dmaController(ctl?: 1 | 2): DMAController;
|
|
341
|
+
getRegisters(): any;
|
|
342
|
+
getPc(): number;
|
|
343
|
+
getSp(): number;
|
|
344
|
+
faultInfo(): any;
|
|
345
|
+
takeFault(): [number, number] | null;
|
|
346
|
+
stop(): void;
|
|
347
|
+
reset(): void;
|
|
348
|
+
close(): void;
|
|
349
|
+
resetCpu(): void;
|
|
350
|
+
setNrst(asserted: boolean): boolean;
|
|
351
|
+
isNrstAsserted(): boolean;
|
|
352
|
+
bootPreset(image: any): void;
|
|
353
|
+
ledStatus(fwName: string, boardKey: string): any;
|
|
354
|
+
}
|
|
22
355
|
export class LED {
|
|
23
356
|
constructor(emu: any, port: string, num: number, opts?: { activeLow?: boolean });
|
|
24
357
|
on(): void;
|
|
@@ -46,7 +379,59 @@ export class Potentiometer {
|
|
|
46
379
|
export class I2cRegisterDevice {
|
|
47
380
|
constructor(emu: any, peripheral: string, opts?: any);
|
|
48
381
|
}
|
|
382
|
+
// Hardware-substitute components (close the documented model gaps — pure
|
|
383
|
+
// JS on existing wasm exports, see site/components.js).
|
|
384
|
+
export class CameraSensor {
|
|
385
|
+
constructor(emu: any, opts?: { width?: number; height?: number; scene?: string; noise?: number });
|
|
386
|
+
pixels(): Uint8Array;
|
|
387
|
+
capture(): boolean;
|
|
388
|
+
frame: number;
|
|
389
|
+
}
|
|
390
|
+
export class DacLoad {
|
|
391
|
+
constructor(emu: any, opts?: { channel?: number; vref?: number; bits?: number });
|
|
392
|
+
write(code: number): number;
|
|
393
|
+
get code(): number;
|
|
394
|
+
get voltage(): number;
|
|
395
|
+
get underrun(): boolean;
|
|
396
|
+
}
|
|
397
|
+
export class RngNoise {
|
|
398
|
+
constructor(emu: any, opts?: { source?: string; seed?: number });
|
|
399
|
+
seed(words?: number): number;
|
|
400
|
+
get avail(): number;
|
|
401
|
+
}
|
|
402
|
+
export class I2cPeer {
|
|
403
|
+
constructor(emu: any, opts?: { base?: number });
|
|
404
|
+
loseNextArbitration(): void;
|
|
405
|
+
alert(addr: number): void;
|
|
406
|
+
}
|
|
407
|
+
export class UsbLink {
|
|
408
|
+
constructor(emu: any, opts?: { hs?: boolean });
|
|
409
|
+
plug(): void;
|
|
410
|
+
unplug(): void;
|
|
411
|
+
get frame(): number;
|
|
412
|
+
get ulpiRate(): number;
|
|
413
|
+
}
|
|
414
|
+
export class UlpiMeter {
|
|
415
|
+
constructor(emu: any, opts?: { hs?: boolean });
|
|
416
|
+
get rateMbps(): number;
|
|
417
|
+
budgetBytes(usec: number): number;
|
|
418
|
+
}
|
|
419
|
+
export class UartBaud {
|
|
420
|
+
constructor(emu: any, opts?: { base?: number; clockHz?: number });
|
|
421
|
+
get brr(): number;
|
|
422
|
+
get over8(): number;
|
|
423
|
+
get baud(): number;
|
|
424
|
+
get txLen(): number;
|
|
425
|
+
}
|
|
426
|
+
export class NandEcc {
|
|
427
|
+
constructor();
|
|
428
|
+
enable(): void;
|
|
429
|
+
disable(): void;
|
|
430
|
+
write(word16: number): number;
|
|
431
|
+
get eccr(): number;
|
|
432
|
+
}
|
|
49
433
|
|
|
434
|
+
// Component-attachment API (attach devices to an emulator handle).
|
|
50
435
|
// Board LED map + host reset/boot control (see site/boards.js,
|
|
51
436
|
// site/emulator.js, site/stm32f4.js).
|
|
52
437
|
export interface BoardLed { bank: number; pin: number; label: string; }
|
package/index.mjs
CHANGED
|
@@ -6,14 +6,18 @@ import * as bindings from './site/vendor/stm32_periph_wasm.js';
|
|
|
6
6
|
import { createEmulator } from './site/emulator.js';
|
|
7
7
|
import { createNetSim } from './site/netsim.js';
|
|
8
8
|
import { FIRMWARES } from './site/firmware.js';
|
|
9
|
-
import { LED, Button, Pwm, I2cRegisterDevice, Potentiometer } from './site/components.js';
|
|
10
|
-
import { STM32F4, GPIOPin, USART, DMAStream } from './site/stm32f4.js';
|
|
9
|
+
import { LED, Button, Pwm, Servo, I2cRegisterDevice, Potentiometer, CameraSensor, DacLoad, RngNoise, I2cPeer, UsbLink, UlpiMeter, UartBaud, NandEcc } from './site/components.js';
|
|
10
|
+
import { STM32F4, GPIOPin, GPIO, USART, SPI, I2C, DMAStream, DMAController, Display, CHIPS, chipInfo } from './site/stm32f4.js';
|
|
11
11
|
|
|
12
12
|
// Node consumers get a one-call setup that injects the bundled assets. The
|
|
13
13
|
// `firmware` option is optional (defer to loadBin/loadHex/loadELF after
|
|
14
14
|
// create), matching the rp2040js / avr8js ergonomics.
|
|
15
|
+
// NOTE: svdXml is deliberately NOT injected here. STM32F4._create resolves
|
|
16
|
+
// the SVD per `chip` (F401/F411/F407/F429) itself; injecting the F407 default
|
|
17
|
+
// would shadow the chip map (right sizes + IDCODE, wrong registers). A caller
|
|
18
|
+
// passing svdXml explicitly still wins (override path in _create).
|
|
15
19
|
STM32F4.create = (opts = {}) => STM32F4._create({
|
|
16
|
-
bindings,
|
|
20
|
+
bindings, wasmInit: wasmBytes, ...opts,
|
|
17
21
|
});
|
|
18
22
|
|
|
19
23
|
const svdXml = readFileSync(new URL('./site/vendor/stm32f407.svd', import.meta.url), 'utf8');
|
|
@@ -28,16 +32,28 @@ export function decodeFirmware(key) {
|
|
|
28
32
|
return Uint8Array.from(bin, c => c.charCodeAt(0));
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
// Convenience: create an emulator with the bundled
|
|
35
|
+
// Convenience: create an emulator with the bundled assets.
|
|
32
36
|
// await createSTM32F407({ firmware }) // firmware: Uint8Array
|
|
33
37
|
// await createSTM32F407({ firmware: 'eth_http' }) // or a FIRMWARES key
|
|
38
|
+
// await createSTM32F407({ firmware, chip: 'stm32f401' }) // other chips
|
|
39
|
+
// `chip` resolves SVD + flash/RAM + IDCODE from the facade CHIPS table
|
|
40
|
+
// (default stm32f407); explicit svdXml/flash_size/ram_size opts override it.
|
|
34
41
|
// All extra options pass through to createEmulator().
|
|
35
42
|
export async function createSTM32F407(opts = {}) {
|
|
36
|
-
const { firmware } = opts;
|
|
43
|
+
const { firmware, chip = 'stm32f407' } = opts;
|
|
37
44
|
const bin = typeof firmware === 'string' ? decodeFirmware(firmware) : firmware;
|
|
38
45
|
if (!bin) throw new Error('createSTM32F407 requires `firmware` (Uint8Array or a FIRMWARES key)');
|
|
39
|
-
|
|
46
|
+
const info = chipInfo(chip);
|
|
47
|
+
const cSvd = (opts.svdXml !== undefined) ? opts.svdXml
|
|
48
|
+
: readFileSync(new URL(`./site/vendor/${info.svd}`, import.meta.url), 'utf8');
|
|
49
|
+
return createEmulator({
|
|
50
|
+
...opts, firmware: bin, bindings, wasmInit: wasmBytes,
|
|
51
|
+
svdXml: cSvd, svdFile: info.svd,
|
|
52
|
+
flash_size: opts.flash_size ?? info.flash_size,
|
|
53
|
+
ram_size: opts.ram_size ?? info.ram_size,
|
|
54
|
+
chipHint: info.svd.replace(/\.svd$/, ''),
|
|
55
|
+
});
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, USART, DMAStream };
|
|
58
|
+
export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, Servo, I2cRegisterDevice, Potentiometer, CameraSensor, DacLoad, RngNoise, I2cPeer, UsbLink, UlpiMeter, UartBaud, NandEcc, STM32F4, GPIOPin, GPIO, USART, SPI, I2C, DMAStream, DMAController, Display, CHIPS, chipInfo };
|
|
43
59
|
export { boardLed, BOARD_LED, BOARD_LED_ALIASES } from './site/boards.js';
|
package/mcp/server.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { readFileSync } from 'node:fs';
|
|
|
9
9
|
// Sequential instances are supported (see docs/components.md), but there is
|
|
10
10
|
// exactly one active system per process and creating a new one detaches the
|
|
11
11
|
// old, so we close the previous instance before creating the next.
|
|
12
|
-
import { createSTM32F407, FIRMWARES, LED, Button, Pwm, Potentiometer } from '../index.mjs';
|
|
12
|
+
import { createSTM32F407, FIRMWARES, LED, Button, Pwm, Potentiometer, CHIPS } from '../index.mjs';
|
|
13
13
|
|
|
14
14
|
// The MCP SDK and zod are OPTIONAL peer dependencies: the emulator library
|
|
15
15
|
// itself has no runtime dependencies, and only this server needs them, so
|
|
@@ -73,16 +73,19 @@ server.registerTool('load_firmware', {
|
|
|
73
73
|
inputSchema: {
|
|
74
74
|
firmware: z.string().describe('a firmware key from list_firmwares, e.g. "blinky"'),
|
|
75
75
|
enable_irqs: z.boolean().optional().describe('run guest IRQ handlers between batches (needed for interrupt-driven firmware; must stay off for the ETH demos)'),
|
|
76
|
+
chip: z.string().optional().describe(`chip variant: ${Object.keys(CHIPS).join('|')} (default stm32f407)`),
|
|
76
77
|
},
|
|
77
|
-
}, async ({ firmware, enable_irqs = false }) => {
|
|
78
|
+
}, async ({ firmware, enable_irqs = false, chip = 'stm32f407' }) => {
|
|
78
79
|
if (!FIRMWARES[firmware]) return err(`unknown firmware '${firmware}' (have: ${Object.keys(FIRMWARES).join(', ')})`);
|
|
80
|
+
if (!CHIPS[chip]) return err(`unknown chip '${chip}' (have: ${Object.keys(CHIPS).join(', ')})`);
|
|
79
81
|
const replaced = session !== null;
|
|
80
82
|
if (session) { try { session.emu.close(); } catch {} }
|
|
81
|
-
const emu = await createSTM32F407({ firmware, enable_irqs });
|
|
83
|
+
const emu = await createSTM32F407({ firmware, enable_irqs, chip });
|
|
82
84
|
session = { emu, firmware, components: new Map() };
|
|
83
85
|
componentSeq = 0;
|
|
84
86
|
return text({
|
|
85
87
|
loaded: firmware,
|
|
88
|
+
chip,
|
|
86
89
|
enable_irqs,
|
|
87
90
|
note: replaced
|
|
88
91
|
? 'Replaced a previous session. Peripheral state is process-global in the wasm model, so switching firmware in one process can misbehave (see docs/components.md) — restart this server for a fully clean boot.'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stm32f4-emu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "STM32F407 emulator: Rust CPU + peripheral model (WASM). Runs real Cortex-M4 firmware in Node or the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"test:browser": "node site/test_browser.mjs",
|
|
94
94
|
"test:edge": "node site/test_edge_cases.mjs",
|
|
95
95
|
"test:fsmc": "node site/test_fsmc.mjs && node site/test_dcmi.mjs && node site/test_fsmc_dcmi.mjs",
|
|
96
|
-
"test:components": "node site/test_component_led.mjs && node site/test_component_button.mjs && node site/test_component_pwm.mjs && node site/test_component_i2cregfile.mjs && node site/test_component_adc.mjs",
|
|
96
|
+
"test:components": "node site/test_component_led.mjs && node site/test_component_button.mjs && node site/test_component_pwm.mjs && node site/test_component_i2cregfile.mjs && node site/test_component_adc.mjs && node site/test_components_subs.mjs",
|
|
97
97
|
"test:mcp": "node mcp/test_mcp.mjs",
|
|
98
98
|
"mcp": "node mcp/server.mjs",
|
|
99
99
|
"bridge": "node site/ws-bridge.mjs",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"lint": "eslint site mcp tools index.mjs cli.mjs",
|
|
105
105
|
"format": "prettier --check .",
|
|
106
106
|
"format:write": "prettier --write .",
|
|
107
|
-
"build:wasm": "wasm-pack build stm32-periph-wasm --target web --out-dir ../site/vendor && rm -f site/vendor/.gitignore &&
|
|
107
|
+
"build:wasm": "wasm-pack build stm32-periph-wasm --target web --out-dir ../site/vendor && rm -f site/vendor/.gitignore && git checkout -- site/vendor/stm32f407.svd site/vendor/stm32f401.svd site/vendor/stm32f411.svd site/vendor/stm32f429.svd 2>/dev/null || true; command -v wasm-opt >/dev/null 2>&1 && wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm || echo 'wasm-opt not found, skipping'",
|
|
108
108
|
"build:wasm:node": "wasm-pack build stm32-periph-wasm --target nodejs --out-dir pkg",
|
|
109
109
|
"build:wasm:opt": "wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm",
|
|
110
110
|
"test:wasm": "node site/test_wasm_cpu.mjs && node site/test_flow_wasm.mjs && node site/test_audio_wasm.mjs && node site/test_wasm_multi.mjs && node site/test_lowpower_wasm.mjs && node site/probe_freertos.mjs && node site/test_doom_wasm.mjs"
|