stm32f4-emu 0.1.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/LICENSE +674 -0
- package/README.md +273 -0
- package/index.mjs +39 -0
- package/mcp/server.mjs +235 -0
- package/package.json +83 -0
- package/site/app.js +654 -0
- package/site/components.js +146 -0
- package/site/emulator.js +885 -0
- package/site/firmware.js +37 -0
- package/site/index.html +385 -0
- package/site/loaders.js +133 -0
- package/site/netsim.js +212 -0
- package/site/vendor/package.json +15 -0
- package/site/vendor/stm32_periph_wasm.d.ts +400 -0
- package/site/vendor/stm32_periph_wasm.js +944 -0
- package/site/vendor/stm32_periph_wasm_bg.wasm +0 -0
- package/site/vendor/stm32_periph_wasm_bg.wasm.d.ts +68 -0
- package/site/vendor/stm32f407.svd +41374 -0
- package/site/vendor/unicorn_arm.cjs +0 -0
- package/site/vendor/unicorn_arm.js +0 -0
package/site/emulator.js
ADDED
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
// Universal STM32F407 emulator factory — runs in Node and the browser.
|
|
2
|
+
// No imports: the caller supplies the bindings (wasm-bindgen module),
|
|
3
|
+
// the Unicorn factory (MUnicorn), the SVD XML, and the firmware bytes.
|
|
4
|
+
//
|
|
5
|
+
// Default eth_* constants match eth_http/eth_http.elf (nm-verified):
|
|
6
|
+
// irq_flag 0x20000620, rx_frame_idx 0x20000628, rx_frame_len 0x2000062c,
|
|
7
|
+
// tx_desc 0x20000610, rx_desc 0x20000630, tx_pkt 0x20000008, rx_buf 0x20000660
|
|
8
|
+
|
|
9
|
+
export async function createEmulator(opts) {
|
|
10
|
+
const {
|
|
11
|
+
firmware,
|
|
12
|
+
bindings, // wasm-bindgen module (web or nodejs build)
|
|
13
|
+
unicorn, // async factory -> Unicorn module
|
|
14
|
+
svdXml, // SVD XML string for init_svd
|
|
15
|
+
wasmInit, // optional: wasm bytes for bindings.default() (Node)
|
|
16
|
+
flash_size = 0x100000,
|
|
17
|
+
ram_size = 0x20000,
|
|
18
|
+
vector_table = 0x08000000,
|
|
19
|
+
tickEvery = 5000, // tick_n batching cadence
|
|
20
|
+
pollEvery = 1000, // DMA/TX-poll check cadence
|
|
21
|
+
// Skip the per-instruction poll/tick wasm calls entirely (no ETH/DMA/
|
|
22
|
+
// timers/watchdog needed — the doom firmware). InstCount++ alone keeps
|
|
23
|
+
// the instruction accounting; the guest's own clock paces the loop.
|
|
24
|
+
minimalPolls = false,
|
|
25
|
+
// doom fast path: HOOK_BLOCK instead of HOOK_CODE (size/2 inst per
|
|
26
|
+
// block). Only valid with minimalPolls (no tick/poll logic to run).
|
|
27
|
+
blockCounting = false,
|
|
28
|
+
// Fastest path: register NO counting hook at all. Every basic block
|
|
29
|
+
// otherwise crosses the WASM->JS boundary just to bump a counter,
|
|
30
|
+
// which is pure overhead for a firmware (like doom) that paces off
|
|
31
|
+
// its own frame counter rather than instCount. `instCount` then
|
|
32
|
+
// tracks the emu_start budget issued per step() — which is actually
|
|
33
|
+
// MORE accurate than block counting (that over-reports ~1.39x, see
|
|
34
|
+
// AGENTS §16). Requires minimalPolls; overrides blockCounting.
|
|
35
|
+
noCountHook = false,
|
|
36
|
+
// Escape hatch: force the old per-INSTRUCTION HOOK_CODE path. The
|
|
37
|
+
// default is now a per-BLOCK hook doing the same accounting (~16%
|
|
38
|
+
// faster — the crossing, not the work, is what costs). Set this if a
|
|
39
|
+
// firmware genuinely needs instruction-exact instCount or stops.
|
|
40
|
+
perInstHook = false,
|
|
41
|
+
maxBatch = 100000, // emu_start instruction budget per step()
|
|
42
|
+
onTx = null, // (frame: Uint8Array, meta) called per TX capture
|
|
43
|
+
eth = {}, // firmware-specific SRAM addresses (defaults above)
|
|
44
|
+
ext_devices = {},
|
|
45
|
+
extra_ram = [], // [{addr, size}] plain RAM regions mapped before
|
|
46
|
+
// extra_mem preload (e.g. FSMC SDRAM, WAD ROM at
|
|
47
|
+
// 0xB8000000 for the doom firmware)
|
|
48
|
+
extra_mem = [], // [{addr, data}] preloaded into mapped memory (ELF RAM segments)
|
|
49
|
+
uart_addr = 0x40011000, // USART base for uart_rx_byte injection (UART4 = 0x40004C00)
|
|
50
|
+
// Run guest IRQ handlers (USART RXNE etc.) between batches. OFF by
|
|
51
|
+
// default: the ETH firmware is driven by writing irq_flag in SRAM and
|
|
52
|
+
// is corrupted if the guest ETH_IRQHandler also runs (it re-reads
|
|
53
|
+
// DMASR and re-scans rx_desc, stomping rx_frame_idx/len). Enable only
|
|
54
|
+
// for interrupt-driven firmware (rx_interrupt_test, rx_crypto_test).
|
|
55
|
+
enable_irqs = false,
|
|
56
|
+
// Interrupt-driven ETH firmware: the guest ETH_IRQHandler (run by the
|
|
57
|
+
// pump) reads DMASR and scans rx_desc itself, so the driver must NOT
|
|
58
|
+
// write the SRAM irq_flag / rx_frame_idx / rx_frame_len globals that
|
|
59
|
+
// the polling ETH firmware (eth_http) expects. Requires enable_irqs.
|
|
60
|
+
irq_eth = false,
|
|
61
|
+
} = opts;
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
periph_read, periph_write, tick, tick_n, get_uart_output,
|
|
65
|
+
dma_get_pending_count, dma_get_pending, dma_set_completed,
|
|
66
|
+
dma_periph_read, dma_periph_write,
|
|
67
|
+
is_watchdog_reset_requested, add_spi_flash, add_i2c_eeprom, init_svd,
|
|
68
|
+
eth_is_tx_poll, eth_get_tx_desc_addr, eth_clear_tx_poll,
|
|
69
|
+
eth_is_rx_poll, eth_clear_rx_poll, eth_tx_done, eth_rx_done,
|
|
70
|
+
get_next_pending_interrupt, uart_rx_byte,
|
|
71
|
+
flash_is_programming, flash_take_erase, flash_erase_applied,
|
|
72
|
+
spi_tap, spi_take_events, spi_push_miso,
|
|
73
|
+
fsmc_tap, fsmc_take_events, fsmc_push_data,
|
|
74
|
+
dcmi_feed_frame, dcmi_clear,
|
|
75
|
+
i2c_register_slave, i2c_take_events, i2c_push_rx,
|
|
76
|
+
i2c_register_regfile, i2c_regfile_get, i2c_regfile_set,
|
|
77
|
+
audio_take_capture,
|
|
78
|
+
gpio_read_output, gpio_read_input, gpio_set_input,
|
|
79
|
+
adc_set_channel_value, adc_clear_channel_value,
|
|
80
|
+
} = bindings;
|
|
81
|
+
|
|
82
|
+
const E = {
|
|
83
|
+
txDesc: 0x20000610, rxDesc: 0x20000630, txPkt: 0x20000008,
|
|
84
|
+
rxBuf: 0x20000660, rxStride: 1536, irqFlag: 0x20000620,
|
|
85
|
+
rxFrameIdx: 0x20000628, rxFrameLen: 0x2000062c, // SRAM addrs of globals
|
|
86
|
+
rxInjectIdx: 0, // desc index to inject at
|
|
87
|
+
rxDescs: 4, // number of RX descriptors
|
|
88
|
+
...eth,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const Module = await unicorn();
|
|
92
|
+
if (typeof bindings.default === 'function') {
|
|
93
|
+
if (wasmInit) await bindings.default({ module_or_path: wasmInit });
|
|
94
|
+
else await bindings.default();
|
|
95
|
+
}
|
|
96
|
+
// Clear process-lifetime wasm globals before registering THIS instance's
|
|
97
|
+
// devices. ExtDevices is a module-level list and the peripheral
|
|
98
|
+
// constructors bind by first-match, so without this a second
|
|
99
|
+
// createEmulator() in the same process silently attaches to the FIRST
|
|
100
|
+
// instance's devices (measured: a regfile seeded 0x22 read back 0x11).
|
|
101
|
+
// Older wasm bundles may not export it, hence the guard.
|
|
102
|
+
if (typeof bindings.reset_state === 'function') bindings.reset_state();
|
|
103
|
+
for (const cfg of (ext_devices.spi_flash || [])) {
|
|
104
|
+
add_spi_flash(cfg.peripheral, cfg.jedec_id, cfg.data, cfg.cs ?? null);
|
|
105
|
+
}
|
|
106
|
+
for (const cfg of (ext_devices.i2c_eeprom || [])) {
|
|
107
|
+
add_i2c_eeprom(cfg.peripheral, cfg.address, cfg.data);
|
|
108
|
+
}
|
|
109
|
+
// Register-file I2C devices (DS3231 RTC). init is a &[u8] snapshot of the
|
|
110
|
+
// register file: BCD time regs 0x00-0x06, temp MSB/LSB 0x11/0x12. The
|
|
111
|
+
// `rtc` device config is shorthand for a regfile at 0x68 that also
|
|
112
|
+
// enables the emu.rtc time/temp decoder.
|
|
113
|
+
const regfileCfg = [
|
|
114
|
+
...(ext_devices.regfile || []),
|
|
115
|
+
...(ext_devices.rtc ? [{
|
|
116
|
+
peripheral: ext_devices.rtc.i2c || 'I2C1',
|
|
117
|
+
address: ext_devices.rtc.addr || 0x68,
|
|
118
|
+
size: ext_devices.rtc.size || 20,
|
|
119
|
+
init: ext_devices.rtc.init || [],
|
|
120
|
+
}] : []),
|
|
121
|
+
];
|
|
122
|
+
for (const cfg of regfileCfg) {
|
|
123
|
+
i2c_register_regfile(
|
|
124
|
+
cfg.peripheral || 'I2C1', cfg.address || 0x68, cfg.size || 20,
|
|
125
|
+
new Uint8Array(cfg.init || []));
|
|
126
|
+
}
|
|
127
|
+
// Virtual peripheral devices (JS hardware layer). Each is a real device
|
|
128
|
+
// protocol implemented in JS on top of the bus taps, driven by the
|
|
129
|
+
// firmware through the modeled I2C/SPI/TIM/I2S registers.
|
|
130
|
+
if (ext_devices.oled) {
|
|
131
|
+
i2c_register_slave(ext_devices.oled.i2c || 'I2C1', ext_devices.oled.addr || 0x3C);
|
|
132
|
+
}
|
|
133
|
+
if (ext_devices.tft) {
|
|
134
|
+
spi_tap(ext_devices.tft.spi || 'SPI2', ext_devices.tft.cs || null, ext_devices.tft.dc || null);
|
|
135
|
+
}
|
|
136
|
+
// Custom SPI/I2C devices (site/components.js-style embedder devices):
|
|
137
|
+
// { peripheral, cs?, dc?, handler(events, push) } / { peripheral, address, handler }.
|
|
138
|
+
// spi_tap/i2c_register_slave must run before init() below — the Spi/I2c
|
|
139
|
+
// peripheral objects snapshot their attached-device list once at
|
|
140
|
+
// construction and never rescan it (see spi.rs Spi::new).
|
|
141
|
+
const spiDevices = [];
|
|
142
|
+
for (const cfg of (ext_devices.spiDevices || [])) {
|
|
143
|
+
spi_tap(cfg.peripheral, cfg.cs ?? null, cfg.dc ?? null);
|
|
144
|
+
spiDevices.push({ peripheral: cfg.peripheral, handler: cfg.handler });
|
|
145
|
+
}
|
|
146
|
+
const i2cDevices = [];
|
|
147
|
+
for (const cfg of (ext_devices.i2cDevices || [])) {
|
|
148
|
+
i2c_register_slave(cfg.peripheral, cfg.address);
|
|
149
|
+
i2cDevices.push({ peripheral: cfg.peripheral, handler: cfg.handler });
|
|
150
|
+
}
|
|
151
|
+
// Memory-mapped FSMC devices: { bank, handler(events, push) }. `bank` is
|
|
152
|
+
// 0-based (0 = BANK1 @ 0x60000000). Events arrive as PAIRS of words —
|
|
153
|
+
// header then value — because an FSMC access carries an address as well
|
|
154
|
+
// as a value; see the fsmc_tap doc comment in lib.rs. Same before-init()
|
|
155
|
+
// rule as spi_tap: Fsmc binds its banks' devices once at construction.
|
|
156
|
+
const fsmcDevices = [];
|
|
157
|
+
for (const cfg of (ext_devices.fsmcDevices || [])) {
|
|
158
|
+
const bank = cfg.bank ?? 0;
|
|
159
|
+
fsmc_tap(bank);
|
|
160
|
+
fsmcDevices.push({ bank, handler: cfg.handler });
|
|
161
|
+
}
|
|
162
|
+
// Exactly ONE of these. They both install a fresh WasmSystem and the last
|
|
163
|
+
// one wins, so calling init() after init_svd() would replace the
|
|
164
|
+
// SVD-derived peripheral map with the hardcoded one. (It used to be a
|
|
165
|
+
// harmless no-op only because SYS was a OnceLock that ignored the second
|
|
166
|
+
// call — see the SYS comment in stm32-periph-wasm/src/lib.rs.)
|
|
167
|
+
if (svdXml) init_svd(svdXml);
|
|
168
|
+
else bindings.init();
|
|
169
|
+
|
|
170
|
+
const uc = new Module.Unicorn(
|
|
171
|
+
Module.ARCH_ARM,
|
|
172
|
+
Module.MODE_THUMB | Module.MODE_LITTLE_ENDIAN
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const mmap = (addr, size, perms, label) => {
|
|
176
|
+
try {
|
|
177
|
+
uc.mem_map(BigInt(addr), size, perms);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
let regions = '?';
|
|
180
|
+
try {
|
|
181
|
+
regions = JSON.stringify(uc.mem_regions());
|
|
182
|
+
} catch (e2) {
|
|
183
|
+
regions = 'regions-err: ' + e2;
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`mem_map ${label} 0x${addr.toString(16)}+0x${size.toString(16)}: ${e} | regions=${regions}`);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
mmap(vector_table & ~0x1FFFF, flash_size, Module.PROT_ALL, 'flash');
|
|
189
|
+
uc.mem_write(BigInt(vector_table), firmware);
|
|
190
|
+
mmap(0x20000000, ram_size, Module.PROT_ALL, 'ram');
|
|
191
|
+
// NOTE: this Unicorn WASM build's mem_write transports data via a
|
|
192
|
+
// stackAlloc copy; subsequent mem_map mallocs can reuse stack-touched
|
|
193
|
+
// heap pages, so "fresh" guest RAM can be seeded with leftovers from the
|
|
194
|
+
// firmware transfer (the 288KB doom.bin is big enough to trigger it).
|
|
195
|
+
// Zero SRAM after mapping so the guest never reads phantom pre-boot data.
|
|
196
|
+
uc.mem_write(BigInt(0x20000000), new Uint8Array(ram_size));
|
|
197
|
+
for (const r of extra_ram) {
|
|
198
|
+
mmap(r.addr, r.size, Module.PROT_ALL, 'extra_ram');
|
|
199
|
+
// same leftover-seeding hazard as SRAM: extra_ram can host .data/.bss
|
|
200
|
+
// (doom's 0xC0000000) — zero it too so bss globals start clean.
|
|
201
|
+
uc.mem_write(BigInt(r.addr), new Uint8Array(r.size));
|
|
202
|
+
}
|
|
203
|
+
for (const seg of extra_mem) {
|
|
204
|
+
uc.mem_write(BigInt(seg.addr), seg.data);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const periphRanges = [
|
|
208
|
+
[0x40000000, 0xB0000000],
|
|
209
|
+
[0xE0000000, 0xE1000000],
|
|
210
|
+
];
|
|
211
|
+
for (const [start, end] of periphRanges) {
|
|
212
|
+
mmap(start, end - start, Module.PROT_READ | Module.PROT_WRITE, 'periph');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const read32 = (addr) => {
|
|
216
|
+
const b = uc.mem_read(BigInt(addr), 4);
|
|
217
|
+
const v = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
218
|
+
return v.getUint32(0, true);
|
|
219
|
+
};
|
|
220
|
+
const write32 = (addr, val) => {
|
|
221
|
+
const b = new Uint8Array(4);
|
|
222
|
+
new DataView(b.buffer).setUint32(0, val >>> 0, true);
|
|
223
|
+
uc.mem_write(BigInt(addr), b);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Scratch buffers reused across MMIO reads. This hook fires on EVERY
|
|
227
|
+
// peripheral read, and polling firmware (ETH spinning on DMASR, USART on
|
|
228
|
+
// TXE) hits it constantly — allocating a fresh Uint8Array each time was
|
|
229
|
+
// pure GC churn. Safe to reuse: uc.mem_write copies the bytes into guest
|
|
230
|
+
// memory synchronously before returning, so nothing retains the buffer.
|
|
231
|
+
const rdScratch = [];
|
|
232
|
+
const memReadHook = (handle, type, address, size, value, user_data) => {
|
|
233
|
+
const val = periph_read(Number(address), size) >>> 0;
|
|
234
|
+
let bytes = rdScratch[size];
|
|
235
|
+
if (bytes === undefined) bytes = rdScratch[size] = new Uint8Array(size);
|
|
236
|
+
for (let i = 0; i < size; i++) bytes[i] = (val >> (i * 8)) & 0xFF;
|
|
237
|
+
uc.mem_write(address, bytes);
|
|
238
|
+
};
|
|
239
|
+
const memWriteHook = (handle, type, address, size, value, user_data) => {
|
|
240
|
+
const a = Number(address);
|
|
241
|
+
periph_write(a, size, Number(value));
|
|
242
|
+
if (a >= 0x40023C00 && a <= 0x40023C18) syncFlashProtection(); // FLASH CR writes flip pg
|
|
243
|
+
};
|
|
244
|
+
for (const [start, end] of periphRanges) {
|
|
245
|
+
uc.hook_add(Module.HOOK_MEM_READ, memReadHook, null, start, end);
|
|
246
|
+
uc.hook_add(Module.HOOK_MEM_WRITE, memWriteHook, null, start, end);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// FLASH regions: guest stores are hardware-gated by programming mode.
|
|
250
|
+
// While the model says programming is active (unlocked + PG + !BSY) the
|
|
251
|
+
// region is writable so stores land directly; otherwise it is read/exec
|
|
252
|
+
// only — the guest's useless stores fault with UC_ERR_WRITE_PROT and the
|
|
253
|
+
// driver skips the instruction (Unicorn 2.1.4 WASM fires HOOK_MEM_WRITE
|
|
254
|
+
// before the store and cannot block it; WRITE_PROT is the only gate).
|
|
255
|
+
const FLASH_GUEST_START = 0x08000000n;
|
|
256
|
+
const FLASH_GUEST_LEN = 0x100000;
|
|
257
|
+
let flashWritable = false;
|
|
258
|
+
const syncFlashProtection = () => {
|
|
259
|
+
const pg = flash_is_programming();
|
|
260
|
+
if (pg && !flashWritable) {
|
|
261
|
+
uc.mem_protect(FLASH_GUEST_START, FLASH_GUEST_LEN, Module.PROT_ALL);
|
|
262
|
+
flashWritable = true;
|
|
263
|
+
} else if (!pg && flashWritable) {
|
|
264
|
+
uc.mem_protect(FLASH_GUEST_START, FLASH_GUEST_LEN, Module.PROT_READ | Module.PROT_EXEC);
|
|
265
|
+
flashWritable = false;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
uc.mem_protect(FLASH_GUEST_START, FLASH_GUEST_LEN, Module.PROT_READ | Module.PROT_EXEC);
|
|
269
|
+
|
|
270
|
+
const stepThroughFlashFault = () => {
|
|
271
|
+
const pc = uc.reg_read_i32(Module.ARM_REG_PC) >>> 0;
|
|
272
|
+
const b = uc.mem_read(BigInt(pc & ~1), 2);
|
|
273
|
+
const h = new DataView(b.buffer, b.byteOffset, b.byteLength).getUint16(0, true);
|
|
274
|
+
const size = (h & 0xE000) === 0xE000 ? 4 : 2; // Thumb-2 wide vs halfword
|
|
275
|
+
uc.reg_write_i32(Module.ARM_REG_PC, (pc + size) | 1);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const applyFlashErase = () => {
|
|
279
|
+
const er = flash_take_erase();
|
|
280
|
+
if (er.length === 2) {
|
|
281
|
+
const [start, len] = er;
|
|
282
|
+
const ff = new Uint8Array(4096).fill(0xFF);
|
|
283
|
+
for (let off = 0; off < len; off += 4096) {
|
|
284
|
+
const chunk = Math.min(4096, len - off);
|
|
285
|
+
uc.mem_write(BigInt(start + off), ff.subarray(0, chunk));
|
|
286
|
+
}
|
|
287
|
+
flash_erase_applied();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const sp_init = read32(vector_table);
|
|
292
|
+
const pc_init = read32(vector_table + 4);
|
|
293
|
+
uc.reg_write_i32(Module.ARM_REG_SP, sp_init);
|
|
294
|
+
uc.reg_write_i32(Module.ARM_REG_PC, pc_init | 1);
|
|
295
|
+
|
|
296
|
+
let stopRequested = false;
|
|
297
|
+
let instCount = 0;
|
|
298
|
+
let tickAcc = 0;
|
|
299
|
+
let pollAcc = 0;
|
|
300
|
+
const rxQueue = [];
|
|
301
|
+
|
|
302
|
+
const codeHook = (handle, address, size, user_data) => {
|
|
303
|
+
instCount++;
|
|
304
|
+
if (rxQueue.length > 0 && eth_is_rx_poll()) {
|
|
305
|
+
uc.emu_stop();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (minimalPolls) return;
|
|
309
|
+
tickAcc++;
|
|
310
|
+
if (tickAcc >= tickEvery) {
|
|
311
|
+
tickAcc = 0;
|
|
312
|
+
tick_n(tickEvery);
|
|
313
|
+
if (is_watchdog_reset_requested()) {
|
|
314
|
+
stopRequested = true;
|
|
315
|
+
uc.emu_stop();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
pollAcc++;
|
|
320
|
+
if (pollAcc >= pollEvery) {
|
|
321
|
+
pollAcc = 0;
|
|
322
|
+
applyFlashErase(); // model holds BSY until the erase is applied
|
|
323
|
+
if (dma_get_pending_count() > 0 || eth_is_tx_poll()) {
|
|
324
|
+
uc.emu_stop();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
// Count-only block hook (doom's old fast path): no tick/poll logic, so it
|
|
330
|
+
// is ONLY valid alongside minimalPolls.
|
|
331
|
+
const blockHook = (handle, address, size, user_data) => {
|
|
332
|
+
instCount += Number(size) / 2; // Thumb-2: block size in bytes
|
|
333
|
+
};
|
|
334
|
+
// Full block hook — same work as codeHook, but once per basic block
|
|
335
|
+
// instead of once per instruction.
|
|
336
|
+
//
|
|
337
|
+
// Measured: the WASM->JS crossing itself costs ~16% of throughput, while
|
|
338
|
+
// the callback BODY costs ~0 (a hook whose body returns immediately is
|
|
339
|
+
// just as slow as one that does the full accounting). Blocks average
|
|
340
|
+
// roughly 5-15 Thumb-2 instructions, so this removes ~80-90% of the
|
|
341
|
+
// crossings and hands that ~16% to EVERY firmware, not just doom.
|
|
342
|
+
//
|
|
343
|
+
// Why this is not a behaviour change: the tick/poll thresholds are 5000
|
|
344
|
+
// and 1000 instructions, so accumulating per block still trips them
|
|
345
|
+
// within one block (~10 inst) of where per-instruction counting would —
|
|
346
|
+
// far finer than the thresholds themselves. The rx-poll stop likewise
|
|
347
|
+
// lands at most a block late, and the driver just services RX and
|
|
348
|
+
// resumes. The one real cost is that instCount becomes approximate (a
|
|
349
|
+
// block that exits early still counts its full size), which affects the
|
|
350
|
+
// MIPS readout but nothing that gates on emu_start's own budget.
|
|
351
|
+
const blockHookFull = (handle, address, size, user_data) => {
|
|
352
|
+
const n = Number(size) / 2;
|
|
353
|
+
instCount += n;
|
|
354
|
+
if (rxQueue.length > 0 && eth_is_rx_poll()) {
|
|
355
|
+
uc.emu_stop();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
tickAcc += n;
|
|
359
|
+
if (tickAcc >= tickEvery) {
|
|
360
|
+
tick_n(tickAcc);
|
|
361
|
+
tickAcc = 0;
|
|
362
|
+
if (is_watchdog_reset_requested()) {
|
|
363
|
+
stopRequested = true;
|
|
364
|
+
uc.emu_stop();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
pollAcc += n;
|
|
369
|
+
if (pollAcc >= pollEvery) {
|
|
370
|
+
pollAcc = 0;
|
|
371
|
+
applyFlashErase(); // model holds BSY until the erase is applied
|
|
372
|
+
if (dma_get_pending_count() > 0 || eth_is_tx_poll()) {
|
|
373
|
+
uc.emu_stop();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
if (noCountHook) { /* no per-block/per-inst hook: step() accounts the budget */ }
|
|
379
|
+
else if (blockCounting) uc.hook_add(Module.HOOK_BLOCK, blockHook, null);
|
|
380
|
+
else if (minimalPolls) uc.hook_add(Module.HOOK_CODE, codeHook, null);
|
|
381
|
+
else if (perInstHook) uc.hook_add(Module.HOOK_CODE, codeHook, null);
|
|
382
|
+
else uc.hook_add(Module.HOOK_BLOCK, blockHookFull, null);
|
|
383
|
+
|
|
384
|
+
// ── virtual peripheral devices (JS hardware layer) ────────────────────
|
|
385
|
+
// Device protocols implemented in JS on top of the bus taps:
|
|
386
|
+
// - oled: SSD1306 128x64 over I2C (page-addressed framebuffer)
|
|
387
|
+
// - tft: ILI9341 240x320 RGB565 over SPI with a DC line
|
|
388
|
+
// - buzzer: TIM PWM frequency/duty read from the modeled timer regs
|
|
389
|
+
// - speaker:I2S TX samples drained from the model's capture FIFO
|
|
390
|
+
// Enabled per-firmware via ext_devices.{oled,tft,buzzer,speaker}.
|
|
391
|
+
|
|
392
|
+
const oled = ext_devices.oled ? {
|
|
393
|
+
w: 128, h: 64, col: 0, page: 0, cmdArg: 0, cmdLeft: 0,
|
|
394
|
+
inData: false, needControl: false,
|
|
395
|
+
fb: new Uint8Array(128 * 64), frame: 0,
|
|
396
|
+
} : null;
|
|
397
|
+
const oledI2C = ext_devices.oled ? (ext_devices.oled.i2c || 'I2C1') : null;
|
|
398
|
+
const OLED_ARG_CMDS = { 0x20: 1, 0x21: 1, 0x22: 1, 0x81: 1, 0x8D: 1, 0xA8: 1, 0xD3: 1, 0xD5: 1, 0xD9: 1, 0xDA: 1, 0xDB: 1 };
|
|
399
|
+
const processOled = () => {
|
|
400
|
+
if (!oled) return;
|
|
401
|
+
const events = i2c_take_events(oledI2C);
|
|
402
|
+
for (const ev of events) {
|
|
403
|
+
if (ev & 0x80000000) {
|
|
404
|
+
// START: the next byte is a control byte (0x00 = command
|
|
405
|
+
// group, 0x40 = data group). STOP: group ends here.
|
|
406
|
+
oled.inData = false;
|
|
407
|
+
if (ev & 0x40000000) oled.needControl = true;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const b = ev & 0xFF;
|
|
411
|
+
if (oled.needControl) {
|
|
412
|
+
oled.needControl = false;
|
|
413
|
+
if (b === 0x40) oled.inData = true;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (oled.inData) {
|
|
417
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
418
|
+
oled.fb[(oled.page * 8 + bit) * oled.w + oled.col] = (b >> bit) & 1;
|
|
419
|
+
}
|
|
420
|
+
oled.col = (oled.col + 1) % oled.w;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (oled.cmdLeft > 0) { oled.cmdLeft--; continue; }
|
|
424
|
+
if (b >= 0xB0 && b <= 0xB7) { oled.page = b & 0x07; continue; }
|
|
425
|
+
if (b >= 0x00 && b <= 0x0F) { oled.col = (oled.col & 0xF0) | b; continue; }
|
|
426
|
+
if (b >= 0x10 && b <= 0x1F) { oled.col = ((b & 0x0F) << 4) | (oled.col & 0x0F); continue; }
|
|
427
|
+
const n = OLED_ARG_CMDS[b];
|
|
428
|
+
if (n !== undefined) { oled.cmdLeft = n; continue; }
|
|
429
|
+
// single-byte commands (0xAE/0xAF/0x40/0xA4/0xA6/...) — no action
|
|
430
|
+
}
|
|
431
|
+
if (events.length) oled.frame++;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const tft = ext_devices.tft ? {
|
|
435
|
+
w: 240, h: 320, x0: 0, y0: 0, x1: 239, y1: 319, x: 0, y: 0,
|
|
436
|
+
mode: 'idle', cmdArg: 0, cmdLeft: 0, argBuf: [],
|
|
437
|
+
fb: new Uint8Array(240 * 320 * 2), frame: 0, pixels: 0,
|
|
438
|
+
} : null;
|
|
439
|
+
const tftSpi = ext_devices.tft ? (ext_devices.tft.spi || 'SPI2') : null;
|
|
440
|
+
const TFT_ARG_CMDS = { 0x2A: 4, 0x2B: 4, 0x36: 1, 0x3A: 1, 0xC0: 2, 0xC1: 1, 0xC5: 2, 0xC7: 1, 0xE0: 15, 0xE1: 15, 0xF6: 3, 0x35: 1, 0x53: 1 };
|
|
441
|
+
const processTft = () => {
|
|
442
|
+
if (!tft) return;
|
|
443
|
+
const events = spi_take_events(tftSpi);
|
|
444
|
+
for (const ev of events) {
|
|
445
|
+
if (ev & 0x80000000) {
|
|
446
|
+
if (ev & 0x40000000) { // CS asserted: fresh transaction
|
|
447
|
+
tft.mode = 'idle';
|
|
448
|
+
tft.cmdLeft = 0;
|
|
449
|
+
}
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const dc = (ev >> 29) & 1;
|
|
453
|
+
const b = ev & 0xFF;
|
|
454
|
+
if (tft.mode === 'write') {
|
|
455
|
+
tft.argBuf.push(b);
|
|
456
|
+
if (tft.argBuf.length === 2) {
|
|
457
|
+
const px = (tft.argBuf[0] << 8) | tft.argBuf[1];
|
|
458
|
+
const off = (tft.y * tft.w + tft.x) * 2;
|
|
459
|
+
tft.fb[off] = tft.argBuf[0];
|
|
460
|
+
tft.fb[off + 1] = tft.argBuf[1];
|
|
461
|
+
tft.x++;
|
|
462
|
+
tft.pixels++;
|
|
463
|
+
if (tft.x > tft.x1) {
|
|
464
|
+
tft.x = tft.x0;
|
|
465
|
+
tft.y++;
|
|
466
|
+
tft.frame++;
|
|
467
|
+
}
|
|
468
|
+
if (tft.y > tft.y1) { tft.y = tft.y0; tft.mode = 'idle'; }
|
|
469
|
+
tft.argBuf.length = 0;
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (!dc) { // command byte
|
|
474
|
+
if (b === 0x2C) {
|
|
475
|
+
tft.mode = 'write';
|
|
476
|
+
tft.x = tft.x0;
|
|
477
|
+
tft.y = tft.y0;
|
|
478
|
+
tft.argBuf.length = 0;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const n = TFT_ARG_CMDS[b];
|
|
482
|
+
if (n !== undefined) { tft.mode = 'args'; tft.cmdArg = b; tft.cmdLeft = n; tft.argBuf.length = 0; }
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
// data byte in args mode
|
|
486
|
+
if (tft.mode === 'args') {
|
|
487
|
+
tft.argBuf.push(b);
|
|
488
|
+
tft.cmdLeft--;
|
|
489
|
+
if (tft.cmdLeft === 0) {
|
|
490
|
+
if (tft.cmdArg === 0x2A && tft.argBuf.length === 4) {
|
|
491
|
+
tft.x0 = (tft.argBuf[0] << 8) | tft.argBuf[1];
|
|
492
|
+
tft.x1 = (tft.argBuf[2] << 8) | tft.argBuf[3];
|
|
493
|
+
} else if (tft.cmdArg === 0x2B && tft.argBuf.length === 4) {
|
|
494
|
+
tft.y0 = (tft.argBuf[0] << 8) | tft.argBuf[1];
|
|
495
|
+
tft.y1 = (tft.argBuf[2] << 8) | tft.argBuf[3];
|
|
496
|
+
}
|
|
497
|
+
tft.mode = 'idle';
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const buzzer = ext_devices.buzzer ? {
|
|
504
|
+
base: ext_devices.buzzer.tim === 'TIM3' ? 0x40000400
|
|
505
|
+
: ext_devices.buzzer.tim === 'TIM4' ? 0x40000800
|
|
506
|
+
: ext_devices.buzzer.tim === 'TIM5' ? 0x40000C00
|
|
507
|
+
: 0x40000000, // TIM2 default
|
|
508
|
+
fclk: 84e6, freq: 0, duty: 0, change: 0,
|
|
509
|
+
} : null;
|
|
510
|
+
const processBuzzer = () => {
|
|
511
|
+
if (!buzzer) return;
|
|
512
|
+
const cr1 = read32(buzzer.base + 0x00);
|
|
513
|
+
const ccer = read32(buzzer.base + 0x20);
|
|
514
|
+
const psc = read32(buzzer.base + 0x28);
|
|
515
|
+
const arr = read32(buzzer.base + 0x2C);
|
|
516
|
+
const ccr = read32(buzzer.base + 0x34);
|
|
517
|
+
let freq = 0, duty = 0;
|
|
518
|
+
if ((cr1 & 1) && (ccer & 1) && ccr > 0 && arr > 0 && arr < 0xFFFFFF) {
|
|
519
|
+
const div = (psc + 1) * (arr + 1);
|
|
520
|
+
if (div > 0) { freq = buzzer.fclk / div; duty = ccr / (arr + 1); }
|
|
521
|
+
}
|
|
522
|
+
if (freq !== buzzer.freq || duty !== buzzer.duty) {
|
|
523
|
+
buzzer.freq = freq; buzzer.duty = duty; buzzer.change++;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const speaker = ext_devices.speaker ? { ring: [], total: 0, samples: 0 } : null;
|
|
528
|
+
const processSpeaker = () => {
|
|
529
|
+
if (!speaker) return;
|
|
530
|
+
const s = audio_take_capture();
|
|
531
|
+
if (!s || s.length === 0) return;
|
|
532
|
+
const f = new Float32Array(s.length);
|
|
533
|
+
for (let i = 0; i < s.length; i++) f[i] = (s[i] > 0x7FFF ? s[i] - 0x10000 : s[i]) / 32768;
|
|
534
|
+
speaker.ring.push(f);
|
|
535
|
+
speaker.total += f.length;
|
|
536
|
+
if (speaker.ring.length > 64) speaker.ring.shift();
|
|
537
|
+
};
|
|
538
|
+
const takeSpeakerSamples = () => {
|
|
539
|
+
if (!speaker) return new Float32Array(0);
|
|
540
|
+
let n = 0;
|
|
541
|
+
for (const f of speaker.ring) n += f.length;
|
|
542
|
+
const out = new Float32Array(n);
|
|
543
|
+
let off = 0;
|
|
544
|
+
for (const f of speaker.ring) { out.set(f, off); off += f.length; }
|
|
545
|
+
speaker.ring.length = 0;
|
|
546
|
+
return out;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// DS3231 RTC: register file behind the tap (BCD time regs 0x00-0x06,
|
|
550
|
+
// temp MSB/LSB 0x11/0x12). Read live from the modeled registers; the
|
|
551
|
+
// JS driver seeds the register file (init) and the guest writes it.
|
|
552
|
+
const rtc = ext_devices.rtc ? {
|
|
553
|
+
peri: ext_devices.rtc.i2c || 'I2C1', change: 0, time: null, temp: null,
|
|
554
|
+
lastKey: '',
|
|
555
|
+
} : null;
|
|
556
|
+
const bcd2n = (v) => ((v >> 4) * 10) + (v & 0x0F);
|
|
557
|
+
const processRtc = () => {
|
|
558
|
+
if (!rtc) return;
|
|
559
|
+
const r = (o) => i2c_regfile_get(rtc.peri, o);
|
|
560
|
+
const time = {
|
|
561
|
+
sec: bcd2n(r(0x00)), min: bcd2n(r(0x01)), hour: bcd2n(r(0x02)),
|
|
562
|
+
dow: bcd2n(r(0x03)), day: bcd2n(r(0x04)), mon: bcd2n(r(0x05)),
|
|
563
|
+
year: bcd2n(r(0x06)),
|
|
564
|
+
};
|
|
565
|
+
const tmsb = r(0x11), tlsb = r(0x12);
|
|
566
|
+
const temp = (tmsb & 0x80 ? tmsb - 0x100 : tmsb) + (tlsb >> 6) * 0.25;
|
|
567
|
+
rtc.time = time;
|
|
568
|
+
rtc.temp = temp;
|
|
569
|
+
const key = `${time.sec}:${time.min}:${time.hour}:${time.day}:${time.mon}:${time.year}:${temp}`;
|
|
570
|
+
if (key !== rtc.lastKey) { rtc.lastKey = key; rtc.change++; }
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// ── Public component-attachment API (LED/Button/custom SPI/I2C
|
|
574
|
+
// devices) — built on the same GPIO shims and bus taps the oled/tft
|
|
575
|
+
// blocks above use internally, but reusable by embedder code without
|
|
576
|
+
// editing this file. See site/components.js and docs/components.md.
|
|
577
|
+
const portIndex = (port) => typeof port === 'string' ? port.toUpperCase().charCodeAt(0) - 65 : port;
|
|
578
|
+
|
|
579
|
+
const pin = (port, num) => {
|
|
580
|
+
const p = portIndex(port);
|
|
581
|
+
return {
|
|
582
|
+
read: () => gpio_read_output(p, num),
|
|
583
|
+
readInput: () => gpio_read_input(p, num),
|
|
584
|
+
write: (level) => gpio_set_input(p, num, !!level),
|
|
585
|
+
};
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const i2cRegfile = (peripheral) => ({
|
|
589
|
+
get: (offset) => i2c_regfile_get(peripheral, offset),
|
|
590
|
+
set: (offset, value) => i2c_regfile_set(peripheral, offset, value & 0xFF),
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const setAdcChannel = (peripheral, channel, value) => adc_set_channel_value(peripheral, channel, value);
|
|
594
|
+
const clearAdcChannel = (peripheral, channel) => adc_clear_channel_value(peripheral, channel);
|
|
595
|
+
|
|
596
|
+
const gpioWatchers = [];
|
|
597
|
+
const watchPin = (port, num, callback) => {
|
|
598
|
+
const p = portIndex(port);
|
|
599
|
+
const w = { p, num, last: gpio_read_output(p, num), callback };
|
|
600
|
+
gpioWatchers.push(w);
|
|
601
|
+
return () => {
|
|
602
|
+
const i = gpioWatchers.indexOf(w);
|
|
603
|
+
if (i >= 0) gpioWatchers.splice(i, 1);
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
const processGpioWatchers = () => {
|
|
607
|
+
for (const w of gpioWatchers) {
|
|
608
|
+
const v = gpio_read_output(w.p, w.num);
|
|
609
|
+
if (v !== w.last) { w.last = v; w.callback(v); }
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const processSpiDevices = () => {
|
|
614
|
+
for (const d of spiDevices) {
|
|
615
|
+
const events = spi_take_events(d.peripheral);
|
|
616
|
+
if (events.length) d.handler(events, (bytes) => spi_push_miso(d.peripheral, bytes));
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const processI2cDevices = () => {
|
|
621
|
+
for (const d of i2cDevices) {
|
|
622
|
+
const events = i2c_take_events(d.peripheral);
|
|
623
|
+
if (events.length) d.handler(events, (bytes) => i2c_push_rx(d.peripheral, bytes));
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// ── DCMI camera sensor (JS pixel source) ──
|
|
628
|
+
// The DCMI model consumes one fed frame with real VSYNC/LINE/FRAME/OVR
|
|
629
|
+
// semantics and drops it when fully read, so a live camera just has to
|
|
630
|
+
// keep supplying frames. Unlike the bus taps this needs no registration
|
|
631
|
+
// before init() — the frame slot is a global the model polls.
|
|
632
|
+
const camera = ext_devices.camera || null;
|
|
633
|
+
let cameraFrames = 0;
|
|
634
|
+
let cameraRunning = true;
|
|
635
|
+
const processCamera = () => {
|
|
636
|
+
if (!camera || !cameraRunning) return;
|
|
637
|
+
// `frame(n)` returns the next frame's pixels, or null/undefined to
|
|
638
|
+
// leave the current one in place (a sensor running slower than the
|
|
639
|
+
// step loop, which is the normal case).
|
|
640
|
+
const px = camera.frame ? camera.frame(cameraFrames) : camera.pixels;
|
|
641
|
+
if (!px) return;
|
|
642
|
+
dcmi_feed_frame(camera.width, camera.height,
|
|
643
|
+
px instanceof Uint8Array ? px : Uint8Array.from(px));
|
|
644
|
+
cameraFrames++;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const processFsmcDevices = () => {
|
|
648
|
+
for (const d of fsmcDevices) {
|
|
649
|
+
const events = fsmc_take_events(d.bank);
|
|
650
|
+
if (events.length) d.handler(events, (values) => fsmc_push_data(d.bank, Uint32Array.from(values)));
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const processDevices = () => {
|
|
655
|
+
processOled();
|
|
656
|
+
processTft();
|
|
657
|
+
processBuzzer();
|
|
658
|
+
processSpeaker();
|
|
659
|
+
processRtc();
|
|
660
|
+
processGpioWatchers();
|
|
661
|
+
processSpiDevices();
|
|
662
|
+
processI2cDevices();
|
|
663
|
+
processFsmcDevices();
|
|
664
|
+
processCamera();
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// ── ETH TX capture + RX injection ──
|
|
668
|
+
const processEth = () => {
|
|
669
|
+
if (eth_is_tx_poll()) {
|
|
670
|
+
const descAddr = eth_get_tx_desc_addr();
|
|
671
|
+
if (descAddr !== 0) {
|
|
672
|
+
const desc = uc.mem_read(BigInt(descAddr), 8);
|
|
673
|
+
const dv = new DataView(desc.buffer, desc.byteOffset, desc.byteLength);
|
|
674
|
+
const tdes0 = dv.getUint32(0, true);
|
|
675
|
+
const tdes1 = dv.getUint32(4, true);
|
|
676
|
+
if (tdes0 & 0x80000000) {
|
|
677
|
+
const bufAddr = tdes1 & 0xFFFFFFFC;
|
|
678
|
+
const bufSize = tdes0 & 0x3FFF;
|
|
679
|
+
if (bufAddr !== 0 && bufSize > 0 && bufSize <= 2000) {
|
|
680
|
+
const pkt = new Uint8Array(uc.mem_read(BigInt(bufAddr), bufSize));
|
|
681
|
+
if (onTx) onTx(pkt, { bufAddr, len: bufSize });
|
|
682
|
+
}
|
|
683
|
+
const wb = new Uint8Array(4);
|
|
684
|
+
new DataView(wb.buffer).setUint32(0, (tdes0 & ~0x80000000) | 0x20000000, true);
|
|
685
|
+
uc.mem_write(BigInt(descAddr), wb);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
eth_clear_tx_poll();
|
|
689
|
+
eth_tx_done();
|
|
690
|
+
if (!irq_eth) write32(E.irqFlag, read32(E.irqFlag) | 1);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (eth_is_rx_poll() && rxQueue.length > 0) {
|
|
694
|
+
const idx = E.rxInjectIdx;
|
|
695
|
+
E.rxInjectIdx = (E.rxInjectIdx + 1) % E.rxDescs;
|
|
696
|
+
const descAddr = E.rxDesc + idx * 8;
|
|
697
|
+
const bufAddr = E.rxBuf + idx * E.rxStride;
|
|
698
|
+
const frame = rxQueue.shift();
|
|
699
|
+
const len = Math.min(frame.length, E.rxStride);
|
|
700
|
+
uc.mem_write(BigInt(bufAddr), frame.subarray(0, len));
|
|
701
|
+
const wb = new Uint8Array(4);
|
|
702
|
+
// Real F407: RDES0 high word = frame length [29:16], OWN cleared
|
|
703
|
+
// for CPU ownership; FS/LS live in the low status word (set by
|
|
704
|
+
// the DMA, not the driver). Writing FS/LS marker bits at 28/27
|
|
705
|
+
// would corrupt the length read by the guest ISR.
|
|
706
|
+
new DataView(wb.buffer).setUint32(0, len << 16, true);
|
|
707
|
+
uc.mem_write(BigInt(descAddr), wb);
|
|
708
|
+
if (!irq_eth) {
|
|
709
|
+
write32(E.rxFrameIdx, idx);
|
|
710
|
+
write32(E.rxFrameLen, len);
|
|
711
|
+
write32(E.irqFlag, read32(E.irqFlag) | 2);
|
|
712
|
+
}
|
|
713
|
+
eth_clear_rx_poll();
|
|
714
|
+
eth_rx_done();
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
const isPeriphAddr = (a) => (a >= 0x40000000 && a < 0xB0000000) || (a >= 0xE0000000 && a < 0xE1000000);
|
|
719
|
+
const processDma = () => {
|
|
720
|
+
const count = dma_get_pending_count();
|
|
721
|
+
for (let i = 0; i < count; i++) {
|
|
722
|
+
const pending = dma_get_pending(0);
|
|
723
|
+
if (pending.length < 5) continue;
|
|
724
|
+
const dir = pending[0], stream = pending[1], src = pending[2], dst = pending[3], size = pending[4];
|
|
725
|
+
const peri_addr = pending[5] || 0;
|
|
726
|
+
const peripheral = pending[6] || 0;
|
|
727
|
+
try {
|
|
728
|
+
if (dir === 2 || !peripheral || !isPeriphAddr(peri_addr)) {
|
|
729
|
+
uc.mem_write(BigInt(dst), uc.mem_read(BigInt(src), size));
|
|
730
|
+
} else if (dir === 0) { // read: peri -> RAM
|
|
731
|
+
const data = dma_periph_read(peri_addr, size, (pending[7] || 0) === 1, pending[8] || 4);
|
|
732
|
+
uc.mem_write(BigInt(dst), data);
|
|
733
|
+
} else { // dir === 1: write: RAM -> peri
|
|
734
|
+
dma_periph_write(peri_addr, uc.mem_read(BigInt(src), size));
|
|
735
|
+
}
|
|
736
|
+
} catch (e) { /* ignore */ }
|
|
737
|
+
dma_set_completed(stream, true);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// ── interrupt pump (runs guest IRQ handlers between batches) ───────────
|
|
742
|
+
// Port of cli.mjs processInterrupts: pushes an exception frame, runs the
|
|
743
|
+
// handler until it aborts on bx lr (EXC_RETURN unsupported), then restores
|
|
744
|
+
// the full context incl. XPSR (condition flags!).
|
|
745
|
+
// Enabled ONLY for interrupt-driven firmware (enable_irqs) — the ETH
|
|
746
|
+
// firmware must NOT use it: the driver signals TX/RX done via SRAM
|
|
747
|
+
// irq_flag + model DMASR, and a guest ETH_IRQHandler run on top re-scans
|
|
748
|
+
// rx_desc and corrupts rx_frame_idx/len (observed garbage response body).
|
|
749
|
+
const processInterrupts = () => {
|
|
750
|
+
while (!stopRequested) {
|
|
751
|
+
const irq = get_next_pending_interrupt();
|
|
752
|
+
if (irq <= -100) break;
|
|
753
|
+
const savedAt = uc.reg_read_i32(Module.ARM_REG_SP);
|
|
754
|
+
const frame = new Uint8Array(32);
|
|
755
|
+
const sv = new DataView(frame.buffer);
|
|
756
|
+
sv.setUint32(0, uc.reg_read_i32(Module.ARM_REG_XPSR), true);
|
|
757
|
+
sv.setUint32(4, uc.reg_read_i32(Module.ARM_REG_PC), true);
|
|
758
|
+
sv.setUint32(8, uc.reg_read_i32(Module.ARM_REG_LR), true);
|
|
759
|
+
sv.setUint32(12, uc.reg_read_i32(Module.ARM_REG_R12), true);
|
|
760
|
+
sv.setUint32(16, uc.reg_read_i32(Module.ARM_REG_R3), true);
|
|
761
|
+
sv.setUint32(20, uc.reg_read_i32(Module.ARM_REG_R2), true);
|
|
762
|
+
sv.setUint32(24, uc.reg_read_i32(Module.ARM_REG_R1), true);
|
|
763
|
+
sv.setUint32(28, uc.reg_read_i32(Module.ARM_REG_R0), true);
|
|
764
|
+
uc.mem_write(BigInt(savedAt - 32), frame);
|
|
765
|
+
uc.reg_write_i32(Module.ARM_REG_SP, savedAt - 32);
|
|
766
|
+
const handler_pc = read32(vector_table + 4 * (16 + irq));
|
|
767
|
+
uc.reg_write_i32(Module.ARM_REG_LR, 0xFFFFFFF9);
|
|
768
|
+
uc.reg_write_i32(Module.ARM_REG_PC, handler_pc);
|
|
769
|
+
try {
|
|
770
|
+
uc.emu_start(BigInt(handler_pc), 0n, 0n, 100000);
|
|
771
|
+
} catch (e) {
|
|
772
|
+
// handler aborted on bx lr (EXC_RETURN not supported) — expected
|
|
773
|
+
}
|
|
774
|
+
const savedFrame = uc.mem_read(BigInt(savedAt - 32), 32);
|
|
775
|
+
const savedSv = new DataView(savedFrame.buffer, savedFrame.byteOffset, savedFrame.byteLength);
|
|
776
|
+
uc.reg_write_i32(Module.ARM_REG_XPSR, savedSv.getUint32(0, true));
|
|
777
|
+
uc.reg_write_i32(Module.ARM_REG_R0, savedSv.getUint32(28, true));
|
|
778
|
+
uc.reg_write_i32(Module.ARM_REG_R1, savedSv.getUint32(24, true));
|
|
779
|
+
uc.reg_write_i32(Module.ARM_REG_R2, savedSv.getUint32(20, true));
|
|
780
|
+
uc.reg_write_i32(Module.ARM_REG_R3, savedSv.getUint32(16, true));
|
|
781
|
+
uc.reg_write_i32(Module.ARM_REG_R12, savedSv.getUint32(12, true));
|
|
782
|
+
uc.reg_write_i32(Module.ARM_REG_LR, savedSv.getUint32(8, true));
|
|
783
|
+
uc.reg_write_i32(Module.ARM_REG_PC, savedSv.getUint32(4, true) | 1);
|
|
784
|
+
uc.reg_write_i32(Module.ARM_REG_SP, savedAt);
|
|
785
|
+
processDma();
|
|
786
|
+
processEth();
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const step = (max_inst = maxBatch) => {
|
|
791
|
+
processDma();
|
|
792
|
+
processEth();
|
|
793
|
+
syncFlashProtection();
|
|
794
|
+
const current_pc = uc.reg_read_i32(Module.ARM_REG_PC);
|
|
795
|
+
try {
|
|
796
|
+
uc.emu_start(BigInt(current_pc | 1), 0n, 0n, max_inst);
|
|
797
|
+
} catch (e) {
|
|
798
|
+
if (String(e).includes('WRITE_PROT')) {
|
|
799
|
+
if (flash_is_programming()) {
|
|
800
|
+
// Region toggled late: pg is active, so re-enable writes
|
|
801
|
+
// and re-execute the store from the same PC.
|
|
802
|
+
uc.mem_protect(FLASH_GUEST_START, FLASH_GUEST_LEN, Module.PROT_ALL);
|
|
803
|
+
flashWritable = true;
|
|
804
|
+
uc.emu_start(BigInt(uc.reg_read_i32(Module.ARM_REG_PC) | 1), 0n, 0n, max_inst);
|
|
805
|
+
} else {
|
|
806
|
+
stepThroughFlashFault();
|
|
807
|
+
}
|
|
808
|
+
} else throw e;
|
|
809
|
+
}
|
|
810
|
+
// With no counting hook the budget issued IS the instruction account
|
|
811
|
+
// (emu_start runs to the budget unless something stops it early).
|
|
812
|
+
if (noCountHook) instCount += max_inst;
|
|
813
|
+
tick();
|
|
814
|
+
applyFlashErase();
|
|
815
|
+
syncFlashProtection();
|
|
816
|
+
processDma();
|
|
817
|
+
processEth();
|
|
818
|
+
processDevices();
|
|
819
|
+
if (enable_irqs) processInterrupts();
|
|
820
|
+
const stopped = stopRequested || is_watchdog_reset_requested();
|
|
821
|
+
return { pc: uc.reg_read_i32(Module.ARM_REG_PC), stopped, instCount };
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const run = (max_instructions = 0) => {
|
|
825
|
+
stopRequested = false;
|
|
826
|
+
let totalSteps = 0;
|
|
827
|
+
while (!stopRequested) {
|
|
828
|
+
step();
|
|
829
|
+
totalSteps++;
|
|
830
|
+
if (max_instructions > 0 && instCount >= max_instructions) break;
|
|
831
|
+
}
|
|
832
|
+
return { totalSteps, instCount };
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
return {
|
|
836
|
+
uc, Module, read32, write32,
|
|
837
|
+
step, run,
|
|
838
|
+
drainUart() { return get_uart_output(); },
|
|
839
|
+
injectFrame(frame) { rxQueue.push(frame); },
|
|
840
|
+
sendUartByte(b) { return uart_rx_byte(uart_addr, b & 0xFF); },
|
|
841
|
+
sendUart(bytes) {
|
|
842
|
+
for (const b of bytes) uart_rx_byte(uart_addr, b & 0xFF);
|
|
843
|
+
},
|
|
844
|
+
rxQueue,
|
|
845
|
+
pin, watchPin, i2cRegfile, setAdcChannel, clearAdcChannel,
|
|
846
|
+
oled: oled ? { fb: oled.fb, frame: () => oled.frame } : null,
|
|
847
|
+
tft: tft ? { fb: tft.fb, w: tft.w, h: tft.h, frame: () => tft.frame } : null,
|
|
848
|
+
buzzer: buzzer ? { get freq() { return buzzer.freq; }, get duty() { return buzzer.duty; }, get change() { return buzzer.change; } } : null,
|
|
849
|
+
takeSpeakerSamples,
|
|
850
|
+
rtc: rtc ? { get time() { return rtc.time; }, get temp() { return rtc.temp; }, get change() { return rtc.change; } } : null,
|
|
851
|
+
// DCMI pixel source. Usable with or without `ext_devices.camera`:
|
|
852
|
+
// feed() hands the model one frame directly, stop() empties the slot.
|
|
853
|
+
camera: {
|
|
854
|
+
feed(w, h, pixels) {
|
|
855
|
+
dcmi_feed_frame(w, h, pixels instanceof Uint8Array ? pixels : Uint8Array.from(pixels));
|
|
856
|
+
},
|
|
857
|
+
// Unplug the sensor: drops the pending frame AND stops any
|
|
858
|
+
// `ext_devices.camera` source from feeding more, so a later
|
|
859
|
+
// feed() is not overwritten on the next step.
|
|
860
|
+
stop() { cameraRunning = false; dcmi_clear(); },
|
|
861
|
+
start() { cameraRunning = true; },
|
|
862
|
+
get frames() { return cameraFrames; },
|
|
863
|
+
},
|
|
864
|
+
// Push values a memory-mapped FSMC device answers on bank reads,
|
|
865
|
+
// for embedders driving a bank without a `fsmcDevices` handler.
|
|
866
|
+
pushFsmcData(bank, values) { fsmc_push_data(bank, Uint32Array.from(values)); },
|
|
867
|
+
takeFsmcEvents(bank) { return fsmc_take_events(bank); },
|
|
868
|
+
stop() {
|
|
869
|
+
stopRequested = true;
|
|
870
|
+
try { uc.emu_stop(); } catch (e) {}
|
|
871
|
+
},
|
|
872
|
+
getRegisters() {
|
|
873
|
+
const out = {};
|
|
874
|
+
for (const n of ['R0','R1','R2','R3','R4','R5','R6','R7','R8','R9','R10','R11','R12']) {
|
|
875
|
+
out[n] = uc.reg_read_i32(Module['ARM_REG_' + n]);
|
|
876
|
+
}
|
|
877
|
+
out.SP = uc.reg_read_i32(Module.ARM_REG_SP);
|
|
878
|
+
out.LR = uc.reg_read_i32(Module.ARM_REG_LR);
|
|
879
|
+
out.PC = uc.reg_read_i32(Module.ARM_REG_PC);
|
|
880
|
+
out.XPSR = uc.reg_read_i32(Module.ARM_REG_XPSR);
|
|
881
|
+
return out;
|
|
882
|
+
},
|
|
883
|
+
close() { try { uc.close(); } catch (e) {} },
|
|
884
|
+
};
|
|
885
|
+
}
|