stm32f4-emu 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/index.d.ts +282 -1
- package/index.mjs +7 -3
- package/package.json +2 -2
- package/site/app.js +47 -21
- 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 +1223 -39
- package/site/vendor/stm32_periph_wasm_bg.wasm +0 -0
- 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/index.d.ts
CHANGED
|
@@ -18,7 +18,287 @@ export const bindings: any;
|
|
|
18
18
|
export const unicornFactory: any;
|
|
19
19
|
export const svdXml: string;
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// STM32F4 high-level facade (see site/stm32f4.js). F1-API parity surface:
|
|
22
|
+
// factories, execute/step results, GPIO/USART/SPI/I2C classes, symbol
|
|
23
|
+
// helpers, and engine pass-throughs. TX is a single shared model buffer
|
|
24
|
+
// (only usart1.onData/output fire); event callbacks beyond SPI/I2C taps
|
|
25
|
+
// are absent by design (no core event queue on F4).
|
|
26
|
+
export interface ExecuteResult { instCount: number; stopped: boolean; }
|
|
27
|
+
export interface FacadeStepResult { pc: number; instCount: number; stopped: boolean; }
|
|
28
|
+
export class GPIOPin {
|
|
29
|
+
constructor(mcu: STM32F4, port: string, pin: number);
|
|
30
|
+
on(event: 'change', cb: (high: boolean) => void): () => void;
|
|
31
|
+
addListener(cb: (high: boolean) => void): () => void;
|
|
32
|
+
read(): boolean;
|
|
33
|
+
readInput(): boolean;
|
|
34
|
+
setInput(high: boolean): void;
|
|
35
|
+
setInputValue(high: boolean): void;
|
|
36
|
+
detach(): void;
|
|
37
|
+
}
|
|
38
|
+
export class GPIO {
|
|
39
|
+
constructor(mcu: STM32F4);
|
|
40
|
+
pin(port: string | number, pin: number): GPIOPin;
|
|
41
|
+
}
|
|
42
|
+
export interface ChipRecord {
|
|
43
|
+
key: string;
|
|
44
|
+
svd: string;
|
|
45
|
+
flash_size: number;
|
|
46
|
+
ram_size: number;
|
|
47
|
+
maxClockMHz: number;
|
|
48
|
+
label: string;
|
|
49
|
+
idcode: number;
|
|
50
|
+
usarts: number[];
|
|
51
|
+
timers: number[];
|
|
52
|
+
can: number[];
|
|
53
|
+
dac: boolean;
|
|
54
|
+
ltdc: boolean;
|
|
55
|
+
gpioBanks: number;
|
|
56
|
+
spi: number[];
|
|
57
|
+
i2c: number[];
|
|
58
|
+
uart78: number[];
|
|
59
|
+
i2s: number[];
|
|
60
|
+
sai: number[];
|
|
61
|
+
sdio: boolean;
|
|
62
|
+
dcmi: boolean;
|
|
63
|
+
crc: boolean;
|
|
64
|
+
cryp: boolean;
|
|
65
|
+
hash: boolean;
|
|
66
|
+
rng: boolean;
|
|
67
|
+
fsmc: boolean;
|
|
68
|
+
dma2d: boolean;
|
|
69
|
+
qspi: boolean;
|
|
70
|
+
}
|
|
71
|
+
export declare const CHIPS: Record<string, Omit<ChipRecord, 'key'>>;
|
|
72
|
+
export declare function chipInfo(key?: string): ChipRecord;
|
|
73
|
+
export class USART {
|
|
74
|
+
readonly n: number;
|
|
75
|
+
onData: ((byte: number) => void) | null;
|
|
76
|
+
constructor(mcu: STM32F4, n: number);
|
|
77
|
+
send(data: string | Uint8Array | number[]): void;
|
|
78
|
+
sendData(data: string | Uint8Array | number[]): void;
|
|
79
|
+
get output(): string;
|
|
80
|
+
}
|
|
81
|
+
export class SPI {
|
|
82
|
+
readonly ch: number;
|
|
83
|
+
onTransfer: ((channel: string, tx: number[], rx: number[]) => void) | null;
|
|
84
|
+
constructor(mcu: STM32F4, ch: number);
|
|
85
|
+
injectMiso(bytes: Uint8Array | number[]): void;
|
|
86
|
+
}
|
|
87
|
+
export class I2C {
|
|
88
|
+
readonly ch: number;
|
|
89
|
+
onStart: ((addr: number, isRead: boolean) => void) | null;
|
|
90
|
+
onWrite: ((byte: number) => void) | null;
|
|
91
|
+
onRead: (() => void) | null;
|
|
92
|
+
onStop: (() => void) | null;
|
|
93
|
+
constructor(mcu: STM32F4, ch: number);
|
|
94
|
+
injectRx(bytes: Uint8Array | number[]): void;
|
|
95
|
+
}
|
|
96
|
+
export class DMAStream {
|
|
97
|
+
constructor(mcu: STM32F4, streamIndex: number);
|
|
98
|
+
pendingCount(): number;
|
|
99
|
+
setCompleted(success?: boolean): void;
|
|
100
|
+
tcif(): boolean;
|
|
101
|
+
htif(): boolean;
|
|
102
|
+
cr(): number;
|
|
103
|
+
ndtr(): number;
|
|
104
|
+
}
|
|
105
|
+
export class DMAController {
|
|
106
|
+
constructor(mcu: STM32F4, controller?: 1 | 2);
|
|
107
|
+
lisr(): number;
|
|
108
|
+
hisr(): number;
|
|
109
|
+
tcif(s: number): boolean;
|
|
110
|
+
htif(s: number): boolean;
|
|
111
|
+
cr(s: number): number;
|
|
112
|
+
ndtr(s: number): number;
|
|
113
|
+
par(s: number): number;
|
|
114
|
+
m0ar(s: number): number;
|
|
115
|
+
m1ar(s: number): number;
|
|
116
|
+
fcr(s: number): number;
|
|
117
|
+
stream(s: number): DMAStream;
|
|
118
|
+
}
|
|
119
|
+
export class Display {
|
|
120
|
+
constructor(mcu: STM32F4);
|
|
121
|
+
get oled(): { w: number; h: number; fb: Uint8Array; frame: number } | null;
|
|
122
|
+
get tft(): { w: number; h: number; fb: Uint8Array; frame: () => number } | null;
|
|
123
|
+
ltdc(): { w: number; h: number; pf: string; fb: Uint8Array; addr: number; pitch: number } | null;
|
|
124
|
+
}
|
|
125
|
+
export class STM32F4 {
|
|
126
|
+
readonly gpio: GPIO;
|
|
127
|
+
readonly usart: USART;
|
|
128
|
+
// Slots for absent-silicon USARTs are null (F401/F411: 3,4,5).
|
|
129
|
+
readonly usart1: USART; readonly usart2: USART; readonly usart3: USART | null;
|
|
130
|
+
readonly usart4: USART | null; readonly usart5: USART | null; readonly usart6: USART;
|
|
131
|
+
readonly usart7: USART | null; readonly usart8: USART | null;
|
|
132
|
+
readonly usarts: Record<number, USART>;
|
|
133
|
+
readonly spi1: SPI | null; readonly spi2: SPI; readonly spi3: SPI;
|
|
134
|
+
readonly spi4: SPI | null; readonly spi5: SPI | null; readonly spi6: SPI | null;
|
|
135
|
+
readonly spiBus: Record<number, SPI>;
|
|
136
|
+
readonly i2c1: I2C; readonly i2c2: I2C; readonly i2c3: I2C;
|
|
137
|
+
readonly i2cBus: Record<number, I2C>;
|
|
138
|
+
readonly spi: { specs: any[]; pushMiso(peripheral: string, bytes: Uint8Array | number[]): void };
|
|
139
|
+
readonly i2c: { specs: any[]; pushRx(peripheral: string, bytes: Uint8Array | number[]): void };
|
|
140
|
+
readonly dma: { stream(index: number): DMAStream; controller(ctl: 1 | 2): DMAController };
|
|
141
|
+
readonly display: Display;
|
|
142
|
+
readonly chip: ChipRecord;
|
|
143
|
+
readonly chipSvdXml: string | null;
|
|
144
|
+
// Wokwi/OpenHW/Velxio integration callbacks (polled dispatch, see site/stm32f4.js).
|
|
145
|
+
onExtiEdge: ((line: number) => void) | null;
|
|
146
|
+
onCanTx: ((can: number) => void) | null;
|
|
147
|
+
onCanRx: ((can: number, id: number, len: number, data: number[]) => void) | null;
|
|
148
|
+
onTimUpdate: ((tim: number) => void) | null;
|
|
149
|
+
onTimCapture: ((tim: number, ch: number, val: number) => void) | null;
|
|
150
|
+
onDmaTc: ((controller: number, stream: number) => void) | null;
|
|
151
|
+
onWdogReset: ((which: number) => void) | null;
|
|
152
|
+
onUsbIn: ((ep: number, data: number[]) => void) | null;
|
|
153
|
+
onUsbHsIn: ((ep: number, data: number[]) => void) | null;
|
|
154
|
+
onItmByte: ((port: number, byte: number) => void) | null;
|
|
155
|
+
onFsmcAccess: ((bank: number, off: number, write: boolean, size: number, val: number) => void) | null;
|
|
156
|
+
onAdcDone: ((adc: number, chan: number, value: number) => void) | null;
|
|
157
|
+
onDacWrite: ((chan: number, value: number) => void) | null;
|
|
158
|
+
onCrcResult: ((value: number) => void) | null;
|
|
159
|
+
onRtcAlarm: ((which: number) => void) | null;
|
|
160
|
+
onI2cAlert: ((peripheral: number, asserted: boolean) => void) | null;
|
|
161
|
+
onHostTx: ((...args: any[]) => void) | null;
|
|
162
|
+
onHostRx: ((...args: any[]) => void) | null;
|
|
163
|
+
constructor(emu: any, bindings?: any, chip?: ChipRecord, chipSvdXml?: string | null);
|
|
164
|
+
static create(opts?: any): Promise<STM32F4>;
|
|
165
|
+
static fromELF(buf: Uint8Array | ArrayBuffer, opts?: any): Promise<STM32F4>;
|
|
166
|
+
static fromBin(buf: Uint8Array | ArrayBuffer, opts?: any): Promise<STM32F4>;
|
|
167
|
+
static fromHex(text: string, opts?: any): Promise<STM32F4>;
|
|
168
|
+
loadBin(bytes: Uint8Array | ArrayBuffer, base?: number): void;
|
|
169
|
+
loadHex(text: string): void;
|
|
170
|
+
loadELF(bytes: Uint8Array | ArrayBuffer): void;
|
|
171
|
+
// Live peripheral injection (anytime unless noted).
|
|
172
|
+
setAdcChannel(peripheral: string, channel: number, value: number): void;
|
|
173
|
+
clearAdcChannel(peripheral: string, channel: number): void;
|
|
174
|
+
takeAdcDma(): number[];
|
|
175
|
+
canInject(id: number, dlc: number, data: Uint8Array | number[]): void;
|
|
176
|
+
canInjectFd(id: number, data: Uint8Array | number[], brs?: boolean): void;
|
|
177
|
+
timInjectCapture(timer: number, ch: number): void;
|
|
178
|
+
timPwmPulseUs(timer: number, ch: number, clockHz: number): number;
|
|
179
|
+
timOcMode(timer: number, ch: number): number;
|
|
180
|
+
timMoe(timer: number): boolean;
|
|
181
|
+
timEncoderStep(timer: number, ti: number, rising: boolean): void;
|
|
182
|
+
timBreakInput(timer: number, asserted: boolean): void;
|
|
183
|
+
dacTrigger(ch: number, src: number, dmaStaged?: boolean): void;
|
|
184
|
+
dacUnderrun(ch: number): boolean;
|
|
185
|
+
takeSpeakerSamples(): Float32Array;
|
|
186
|
+
usbInjectSetup(bytes: Uint8Array | number[]): boolean;
|
|
187
|
+
usbInjectOut(ep: number, bytes: Uint8Array | number[]): boolean;
|
|
188
|
+
usbTakeIn(ep: number): number[];
|
|
189
|
+
usbReset(): void;
|
|
190
|
+
usbEnumerated(): void;
|
|
191
|
+
usbHsInjectSetup(bytes: Uint8Array | number[]): boolean;
|
|
192
|
+
usbHsInjectOut(ep: number, bytes: Uint8Array | number[]): boolean;
|
|
193
|
+
usbHsTakeIn(ep: number): number[];
|
|
194
|
+
usbHsReset(): void;
|
|
195
|
+
usbHsEnumerated(): void;
|
|
196
|
+
ethInject(frame: Uint8Array | number[]): void;
|
|
197
|
+
// Fault / model-harness methods (mirror wasm exports 1:1).
|
|
198
|
+
uartSetCts(base: number, asserted: boolean): void;
|
|
199
|
+
uartFaultRx(base: number, fe: boolean, pe: boolean): void;
|
|
200
|
+
uartIdle(base: number): void;
|
|
201
|
+
uartLinBreak(base: number): void;
|
|
202
|
+
uartBreakPending(base: number): boolean;
|
|
203
|
+
uartTxLen(base: number): number;
|
|
204
|
+
uartMuted(base: number): boolean;
|
|
205
|
+
spiFaultCrc(base: number): void;
|
|
206
|
+
spiFaultModf(base: number): void;
|
|
207
|
+
spiSlaveSelect(base: number, asserted: boolean): void;
|
|
208
|
+
spiSlaveClock(base: number, mosi: number): number;
|
|
209
|
+
spiSlaveGate(base: number): boolean;
|
|
210
|
+
i2cArmArbLoss(base: number): void;
|
|
211
|
+
i2cArmSmbusAlert(base: number, addr: number): void;
|
|
212
|
+
i2cSlaveAddress(base: number, addr: number, isRead: boolean): boolean;
|
|
213
|
+
i2cSlaveWrite(base: number, byte: number): void;
|
|
214
|
+
i2cSlaveRead(base: number): number;
|
|
215
|
+
i2cSlaveStop(base: number): void;
|
|
216
|
+
i2cSlaveStatus(base: number): number;
|
|
217
|
+
sdioFaultDataCrc(): void;
|
|
218
|
+
rccInjectFailure(srcMask: number, dead: boolean): void;
|
|
219
|
+
flashRdpLevel(): number;
|
|
220
|
+
flashSetRdp(levelByte: number): void;
|
|
221
|
+
rngSeedEntropy(words: number[] | Uint32Array): void;
|
|
222
|
+
rngEntropyAvail(): number;
|
|
223
|
+
rtcTamperPin(level: boolean): void;
|
|
224
|
+
rtcTimestamp(): void;
|
|
225
|
+
setIntrPending(irq: number): void;
|
|
226
|
+
hasPendingInterrupt(): boolean;
|
|
227
|
+
// Scope probes (read-only model state).
|
|
228
|
+
adcDualLatched(): boolean;
|
|
229
|
+
ethPpsCount(): number;
|
|
230
|
+
ethPpsLevel(): boolean;
|
|
231
|
+
ethLinkUp(): boolean;
|
|
232
|
+
ethSetLink(up: boolean): void;
|
|
233
|
+
ltdcScanline(): number;
|
|
234
|
+
ltdcFrameCount(): number;
|
|
235
|
+
audioRemaining(): number;
|
|
236
|
+
audioClear(): void;
|
|
237
|
+
dcmiSync(vsync: boolean, hsync: boolean, pclkDiv: number): void;
|
|
238
|
+
qspiMmapLive(): boolean;
|
|
239
|
+
qspiMmapRead(offset: number): number;
|
|
240
|
+
sdioBusWidth(): number;
|
|
241
|
+
sdioCardBlocks(): number;
|
|
242
|
+
sdioReadBlock(block: number): number[];
|
|
243
|
+
takeItm(port: number): number[];
|
|
244
|
+
itmPending(port: number): number;
|
|
245
|
+
takeFsmc(bank: number): number[];
|
|
246
|
+
pushFsmc(bank: number, values: number[] | Uint32Array): void;
|
|
247
|
+
regfileGet(peripheral: string, offset: number): number;
|
|
248
|
+
regfileSet(peripheral: string, offset: number, value: number): void;
|
|
249
|
+
get oled(): any;
|
|
250
|
+
get tft(): any;
|
|
251
|
+
get rtc(): any;
|
|
252
|
+
get buzzer(): any;
|
|
253
|
+
get camera(): any;
|
|
254
|
+
execute(cycles?: number): ExecuteResult;
|
|
255
|
+
step(cycles?: number): FacadeStepResult;
|
|
256
|
+
uartRx(byte: number): boolean;
|
|
257
|
+
get uartOutput(): string;
|
|
258
|
+
setSymbols(mapText: string): number;
|
|
259
|
+
resolveSymbol(pc: number): string | null;
|
|
260
|
+
read32(addr: number): number;
|
|
261
|
+
write32(addr: number, val: number): void;
|
|
262
|
+
memRead32(addr: number): number;
|
|
263
|
+
memWriteBytes(addr: number, bytes: Uint8Array | number[]): void;
|
|
264
|
+
periphRead(addr: number, width?: number): number;
|
|
265
|
+
periphWrite(addr: number, width: number, value: number): void;
|
|
266
|
+
addJsPeripheral(base: number, size: number, read: (addr: number, size: number) => number, write: (addr: number, value: number, size: number) => void): boolean;
|
|
267
|
+
pwrMode(): number;
|
|
268
|
+
pwrEstimate(): number;
|
|
269
|
+
swdHalted(): boolean;
|
|
270
|
+
swdHalt(): void;
|
|
271
|
+
swdResume(): void;
|
|
272
|
+
swdStep(): number;
|
|
273
|
+
swdAddWatch(kind: number, addr: number, len: number): number;
|
|
274
|
+
swdRemoveWatch(slot: number): void;
|
|
275
|
+
swdTakeTrip(): number[];
|
|
276
|
+
swdDpRead(addr: number): number;
|
|
277
|
+
swdDpWrite(addr: number, value: number): void;
|
|
278
|
+
swdApRead(bank: number, reg: number): number;
|
|
279
|
+
swdApWrite(bank: number, reg: number, value: number): void;
|
|
280
|
+
swdRegRead(idx: number): number;
|
|
281
|
+
swdRegWrite(idx: number, value: number): void;
|
|
282
|
+
jtagReset(): void;
|
|
283
|
+
jtagIr(ir: number): void;
|
|
284
|
+
jtagIdcode(): number;
|
|
285
|
+
jtagDp(addr: number, rnw: boolean, wdata: number): number;
|
|
286
|
+
jtagAp(bank: number, reg: number, rnw: boolean, wdata: number): number;
|
|
287
|
+
dmaController(ctl?: 1 | 2): DMAController;
|
|
288
|
+
getRegisters(): any;
|
|
289
|
+
getPc(): number;
|
|
290
|
+
getSp(): number;
|
|
291
|
+
faultInfo(): any;
|
|
292
|
+
takeFault(): [number, number] | null;
|
|
293
|
+
stop(): void;
|
|
294
|
+
reset(): void;
|
|
295
|
+
close(): void;
|
|
296
|
+
resetCpu(): void;
|
|
297
|
+
setNrst(asserted: boolean): boolean;
|
|
298
|
+
isNrstAsserted(): boolean;
|
|
299
|
+
bootPreset(image: any): void;
|
|
300
|
+
ledStatus(fwName: string, boardKey: string): any;
|
|
301
|
+
}
|
|
22
302
|
export class LED {
|
|
23
303
|
constructor(emu: any, port: string, num: number, opts?: { activeLow?: boolean });
|
|
24
304
|
on(): void;
|
|
@@ -47,6 +327,7 @@ export class I2cRegisterDevice {
|
|
|
47
327
|
constructor(emu: any, peripheral: string, opts?: any);
|
|
48
328
|
}
|
|
49
329
|
|
|
330
|
+
// Component-attachment API (attach devices to an emulator handle).
|
|
50
331
|
// Board LED map + host reset/boot control (see site/boards.js,
|
|
51
332
|
// site/emulator.js, site/stm32f4.js).
|
|
52
333
|
export interface BoardLed { bank: number; pin: number; label: string; }
|
package/index.mjs
CHANGED
|
@@ -7,13 +7,17 @@ import { createEmulator } from './site/emulator.js';
|
|
|
7
7
|
import { createNetSim } from './site/netsim.js';
|
|
8
8
|
import { FIRMWARES } from './site/firmware.js';
|
|
9
9
|
import { LED, Button, Pwm, I2cRegisterDevice, Potentiometer } from './site/components.js';
|
|
10
|
-
import { STM32F4, GPIOPin, USART, DMAStream } from './site/stm32f4.js';
|
|
10
|
+
import { STM32F4, GPIOPin, GPIO, USART, SPI, I2C, DMAStream, DMAController, Display, CHIPS, chipInfo } from './site/stm32f4.js';
|
|
11
11
|
|
|
12
12
|
// Node consumers get a one-call setup that injects the bundled assets. The
|
|
13
13
|
// `firmware` option is optional (defer to loadBin/loadHex/loadELF after
|
|
14
14
|
// create), matching the rp2040js / avr8js ergonomics.
|
|
15
|
+
// NOTE: svdXml is deliberately NOT injected here. STM32F4._create resolves
|
|
16
|
+
// the SVD per `chip` (F401/F411/F407/F429) itself; injecting the F407 default
|
|
17
|
+
// would shadow the chip map (right sizes + IDCODE, wrong registers). A caller
|
|
18
|
+
// passing svdXml explicitly still wins (override path in _create).
|
|
15
19
|
STM32F4.create = (opts = {}) => STM32F4._create({
|
|
16
|
-
bindings,
|
|
20
|
+
bindings, wasmInit: wasmBytes, ...opts,
|
|
17
21
|
});
|
|
18
22
|
|
|
19
23
|
const svdXml = readFileSync(new URL('./site/vendor/stm32f407.svd', import.meta.url), 'utf8');
|
|
@@ -39,5 +43,5 @@ export async function createSTM32F407(opts = {}) {
|
|
|
39
43
|
return createEmulator({ ...opts, firmware: bin, bindings, svdXml, wasmInit: wasmBytes });
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, USART, DMAStream };
|
|
46
|
+
export { createEmulator, createNetSim, FIRMWARES, bindings, svdXml, LED, Button, Pwm, I2cRegisterDevice, Potentiometer, STM32F4, GPIOPin, GPIO, USART, SPI, I2C, DMAStream, DMAController, Display, CHIPS, chipInfo };
|
|
43
47
|
export { boardLed, BOARD_LED, BOARD_LED_ALIASES } from './site/boards.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stm32f4-emu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "STM32F407 emulator: Rust CPU + peripheral model (WASM). Runs real Cortex-M4 firmware in Node or the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"lint": "eslint site mcp tools index.mjs cli.mjs",
|
|
105
105
|
"format": "prettier --check .",
|
|
106
106
|
"format:write": "prettier --write .",
|
|
107
|
-
"build:wasm": "wasm-pack build stm32-periph-wasm --target web --out-dir ../site/vendor && rm -f site/vendor/.gitignore &&
|
|
107
|
+
"build:wasm": "wasm-pack build stm32-periph-wasm --target web --out-dir ../site/vendor && rm -f site/vendor/.gitignore && git checkout -- site/vendor/stm32f407.svd site/vendor/stm32f401.svd site/vendor/stm32f411.svd site/vendor/stm32f429.svd 2>/dev/null || true; command -v wasm-opt >/dev/null 2>&1 && wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm || echo 'wasm-opt not found, skipping'",
|
|
108
108
|
"build:wasm:node": "wasm-pack build stm32-periph-wasm --target nodejs --out-dir pkg",
|
|
109
109
|
"build:wasm:opt": "wasm-opt -O3 site/vendor/stm32_periph_wasm_bg.wasm -o site/vendor/stm32_periph_wasm_bg.wasm",
|
|
110
110
|
"test:wasm": "node site/test_wasm_cpu.mjs && node site/test_flow_wasm.mjs && node site/test_audio_wasm.mjs && node site/test_wasm_multi.mjs && node site/test_lowpower_wasm.mjs && node site/probe_freertos.mjs && node site/test_doom_wasm.mjs"
|
package/site/app.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Preset + custom (.bin/.hex/.elf/.map) firmware loading, Run/Stop/Reset,
|
|
3
3
|
// an optional WebSocket gateway (real network stack) with a netsim fallback,
|
|
4
4
|
// live UART terminal, GPIO/peripheral register readout, and packet viewer.
|
|
5
|
-
import * as bindings from './vendor/stm32_periph_wasm.js?v=
|
|
6
|
-
import { createEmulator } from './emulator.js?v=
|
|
5
|
+
import * as bindings from './vendor/stm32_periph_wasm.js?v=43';
|
|
6
|
+
import { createEmulator } from './emulator.js?v=3';
|
|
7
7
|
import { createNetSim } from './netsim.js';
|
|
8
8
|
import { createUsbHost } from './usbhost.js';
|
|
9
9
|
import { boardsOf, boardForSelection, BOARDS, boardLed } from './boards.js?v=7';
|
|
@@ -142,6 +142,9 @@ const DEVICE_FIRMWARES = {
|
|
|
142
142
|
buzzer_test_f401: { buzzer: { tim: 'TIM2' } },
|
|
143
143
|
buzzer_test_f411: { buzzer: { tim: 'TIM2' } },
|
|
144
144
|
buzzer_test_f429: { buzzer: { tim: 'TIM2' } },
|
|
145
|
+
audio_play_test_f401: { speaker: true },
|
|
146
|
+
audio_play_test_f411: { speaker: true },
|
|
147
|
+
audio_play_test_f429: { speaker: true },
|
|
145
148
|
rtc_test_f401: { rtc: { i2c: 'I2C1', addr: 0x68, init: RTC_INIT } },
|
|
146
149
|
rtc_test_f411: { rtc: { i2c: 'I2C1', addr: 0x68, init: RTC_INIT } },
|
|
147
150
|
rtc_test_f429: { rtc: { i2c: 'I2C1', addr: 0x68, init: RTC_INIT } },
|
|
@@ -481,7 +484,7 @@ const boot = async () => {
|
|
|
481
484
|
setStatus('booting…', 'stop');
|
|
482
485
|
if (emu) { try { emu.close(); } catch (e) {} emu = null; }
|
|
483
486
|
oledCacheKey = ''; tftCacheKey = ''; buzzerCacheKey = ''; rtcCacheKey = ''; ppsCacheKey = ''; TRACES.length = 0; { const cv = traceCanvas(); if (cv) cv._dma = []; } renderTraces();
|
|
484
|
-
if (audioCtx) { try { audioCtx.close(); } catch (e) {} audioCtx = null; audioQueued = 0; }
|
|
487
|
+
if (audioCtx) { try { audioCtx.close(); } catch (e) {} audioCtx = null; audioQueued = 0; audioDropped = 0; if (window.__audioCtx) delete window.__audioCtx; }
|
|
485
488
|
gpioDrivenHigh.clear();
|
|
486
489
|
|
|
487
490
|
const fw = image.flash;
|
|
@@ -546,19 +549,24 @@ const boot = async () => {
|
|
|
546
549
|
// ── local mode: WASM runs in the browser (default) ──
|
|
547
550
|
// Board variant per firmware preset (SVD + flash/RAM sizes), honoring
|
|
548
551
|
// the board selector when the preset supports the selected board.
|
|
549
|
-
// NOTE (VENDOR_V): vendor asset versions (?v=
|
|
552
|
+
// NOTE (VENDOR_V): vendor asset versions (?v=43) must be bumped together
|
|
550
553
|
// after every wasm-pack rebuild, or browsers keep the stale model.
|
|
551
554
|
const { key: boardKey, board } = boardForSelection(image.name, boardSelectEl ? boardSelectEl.value : 'all');
|
|
552
|
-
const svdXml = await fetch('vendor/' + board.svd + '?v=
|
|
555
|
+
const svdXml = await fetch('vendor/' + board.svd + '?v=43').then((r) => r.text());
|
|
553
556
|
if (id !== session) return;
|
|
554
557
|
|
|
555
558
|
netsim = gw.connected ? null : createNetSim();
|
|
556
559
|
// Scripted USB host when the demo preset boots locally.
|
|
557
560
|
usbhost = (!bridgeUrl && image.name.startsWith('usb_cdc_test')) ? createUsbHost(bindings) : null;
|
|
558
|
-
// Harness arms for
|
|
559
|
-
// prints
|
|
560
|
-
// mechanism as the node matrix
|
|
561
|
-
|
|
561
|
+
// Harness arms for collision tests (local mode only): the guest
|
|
562
|
+
// prints COLLIDE ARM and spins, the page arms the one-shot
|
|
563
|
+
// collision + link hooks (same mechanism as the node matrix).
|
|
564
|
+
// Covers eth_feat_test AND eth_pins_test (its COL phase needs the
|
|
565
|
+
// same arm-before-TX timing; without it PINS COL FAILs while the
|
|
566
|
+
// node harness passes).
|
|
567
|
+
featHooks = (!bridgeUrl && bindings && typeof bindings.eth_arm_collision === 'function'
|
|
568
|
+
&& (image.name.startsWith('eth_feat_test') || image.name.startsWith('eth_pins_test'))
|
|
569
|
+
)
|
|
562
570
|
? { collideDone: 0, linkDown: false, linkUp: false } : null;
|
|
563
571
|
gw.tx = 0; gw.rx = 0;
|
|
564
572
|
if (gw.connected) setGwStatus(true, gwLabel());
|
|
@@ -569,7 +577,7 @@ const boot = async () => {
|
|
|
569
577
|
svdFile: board.svd,
|
|
570
578
|
flash_size: board.flash_size,
|
|
571
579
|
ram_size: board.ram_size,
|
|
572
|
-
wasmUrl: 'vendor/stm32_periph_wasm_bg.wasm?v=
|
|
580
|
+
wasmUrl: 'vendor/stm32_periph_wasm_bg.wasm?v=43',
|
|
573
581
|
extra_mem: image.extraMem,
|
|
574
582
|
uart_addr: image.uartAddr,
|
|
575
583
|
enable_irqs: IRQ_FIRMWARES.has(image.name),
|
|
@@ -634,13 +642,14 @@ const loop = async (id) => {
|
|
|
634
642
|
while (session === id) {
|
|
635
643
|
if (!running) { await raf(); continue; }
|
|
636
644
|
try {
|
|
637
|
-
// eth_feat_test/eth_adv run at 20k-inst steps:
|
|
638
|
-
// bands and race windows are calibrated for
|
|
639
|
-
// node matrix uses 5k-20k); 100k steps would
|
|
640
|
-
// band (observed: eth_adv FRAG FAILs at 100k —
|
|
641
|
-
// consumed before the reassembly loop polls —
|
|
642
|
-
// 13/13;
|
|
643
|
-
|
|
645
|
+
// eth_feat_test/eth_adv/eth_pins_test run at 20k-inst steps:
|
|
646
|
+
// their wire-rate bands and race windows are calibrated for
|
|
647
|
+
// fine steps (the node matrix uses 5k-20k); 100k steps would
|
|
648
|
+
// overshoot every band (observed: eth_adv FRAG FAILs at 100k —
|
|
649
|
+
// both fragments consumed before the reassembly loop polls —
|
|
650
|
+
// while 20k passes 13/13; eth_pins_test COL needs the arm to
|
|
651
|
+
// land inside the TX window; the node matrix runs all at 20k).
|
|
652
|
+
const fineSteps = image && (image.name.startsWith('eth_feat_test') || image.name.startsWith('eth_adv') || image.name.startsWith('eth_pins_test'));
|
|
644
653
|
const res = await emu.step(fineSteps ? 20000 : undefined);
|
|
645
654
|
totalInst = res.instCount;
|
|
646
655
|
stepsDone++;
|
|
@@ -663,7 +672,7 @@ const loop = async (id) => {
|
|
|
663
672
|
try {
|
|
664
673
|
if (!bridgeUrl && image && image.name.startsWith('dcmi_test') && bindings.dcmi_feed_frame) driveDcmi();
|
|
665
674
|
} catch (e) {}
|
|
666
|
-
// Harness arms for
|
|
675
|
+
// Harness arms for collision tests (local mode only; see boot).
|
|
667
676
|
try {
|
|
668
677
|
if (featHooks) {
|
|
669
678
|
const arms = uartBuf.split('COLLIDE ARM').length - 1;
|
|
@@ -1194,6 +1203,13 @@ const renderBuzzer = () => {
|
|
|
1194
1203
|
|
|
1195
1204
|
const speakerInfo = $('speakerInfo');
|
|
1196
1205
|
let audioCtx = null, audioNextTime = 0, audioQueued = 0;
|
|
1206
|
+
// Cap on scheduled-ahead audio: the guest produces faster than realtime
|
|
1207
|
+
// (a tone loop emits unbounded samples per second of wall time), so an
|
|
1208
|
+
// uncapped scheduler piles up hundreds of seconds of BufferSources —
|
|
1209
|
+
// audible only after a huge delay, plus unbounded graph/memory growth.
|
|
1210
|
+
// MAX_AHEAD keeps ~2 s scheduled; excess samples are dropped (counted).
|
|
1211
|
+
const AUDIO_MAX_AHEAD_S = 2.0;
|
|
1212
|
+
let audioDropped = 0;
|
|
1197
1213
|
const renderSpeaker = () => {
|
|
1198
1214
|
if (!emu || !emu.takeSpeakerSamples) { speakerInfo.textContent = 'no audio firmware'; return; }
|
|
1199
1215
|
const samples = emu.takeSpeakerSamples();
|
|
@@ -1201,12 +1217,19 @@ const renderSpeaker = () => {
|
|
|
1201
1217
|
if (!audioCtx) {
|
|
1202
1218
|
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
|
|
1203
1219
|
if (!audioCtx) { speakerInfo.textContent = 'WebAudio unavailable'; return; }
|
|
1204
|
-
audioNextTime = audioCtx.currentTime + 0.05;
|
|
1220
|
+
audioNextTime = Math.max(audioNextTime, audioCtx.currentTime + 0.05);
|
|
1221
|
+
window.__audioCtx = audioCtx;
|
|
1205
1222
|
}
|
|
1206
1223
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
1224
|
+
// Rebase late schedulers (tab was hidden / long pause): never schedule
|
|
1225
|
+
// in the past, never more than MAX_AHEAD ahead of now.
|
|
1226
|
+
audioNextTime = Math.max(audioNextTime, audioCtx.currentTime + 0.02);
|
|
1207
1227
|
let off = 0;
|
|
1208
1228
|
while (off < samples.length) {
|
|
1209
|
-
const
|
|
1229
|
+
const headroom = AUDIO_MAX_AHEAD_S - (audioNextTime - audioCtx.currentTime);
|
|
1230
|
+
if (headroom <= 0) { audioDropped += samples.length - off; break; }
|
|
1231
|
+
const maxN = Math.floor(headroom * audioCtx.sampleRate);
|
|
1232
|
+
const n = Math.min(4096, samples.length - off, Math.max(1, maxN));
|
|
1210
1233
|
const buf = audioCtx.createBuffer(1, n, audioCtx.sampleRate);
|
|
1211
1234
|
const ch = buf.getChannelData(0);
|
|
1212
1235
|
for (let i = 0; i < n; i++) ch[i] = samples[off + i];
|
|
@@ -1218,7 +1241,10 @@ const renderSpeaker = () => {
|
|
|
1218
1241
|
off += n;
|
|
1219
1242
|
audioQueued += n;
|
|
1220
1243
|
}
|
|
1221
|
-
|
|
1244
|
+
window.__audioQueued = audioQueued;
|
|
1245
|
+
window.__audioDropped = audioDropped;
|
|
1246
|
+
const ahead = Math.max(0, audioNextTime - audioCtx.currentTime);
|
|
1247
|
+
speakerInfo.textContent = `I2S capture playing — ${(audioQueued / audioCtx.sampleRate).toFixed(1)} s queued (${ahead.toFixed(1)} s ahead${audioDropped ? `, ${audioDropped} dropped` : ''})`;
|
|
1222
1248
|
};
|
|
1223
1249
|
|
|
1224
1250
|
let rtcCacheKey = '';
|
package/site/console.html
CHANGED
|
@@ -957,7 +957,7 @@ canvas {
|
|
|
957
957
|
· <a href="about.html">About</a>
|
|
958
958
|
</footer>
|
|
959
959
|
|
|
960
|
-
<script type="module" src="app.js?v=
|
|
960
|
+
<script type="module" src="app.js?v=52"></script>
|
|
961
961
|
<script>
|
|
962
962
|
// Custom firmware dropdown (replaces native <select>)
|
|
963
963
|
(() => {
|
package/site/emulator.js
CHANGED
|
@@ -461,6 +461,10 @@ export async function createEmulator(opts) {
|
|
|
461
461
|
hint = String(opts.chipHint || opts.svdFile || '');
|
|
462
462
|
hint = hint.split('/').pop().split('.').slice(0, -1).join('.') || hint;
|
|
463
463
|
} catch {}
|
|
464
|
+
// compat[0] may carry no SVD filename (older callers pass none):
|
|
465
|
+
// default to the F407 identity rather than an empty hint that
|
|
466
|
+
// leaves IDCODE at its reset value (comprehensive_test pins 0x413).
|
|
467
|
+
if (!hint) hint = 'stm32f407';
|
|
464
468
|
init_svd_chip(svdXml, hint);
|
|
465
469
|
} else init_svd(svdXml);
|
|
466
470
|
}
|