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/index.html
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="google-site-verification" content="15oUGn7tDY5qFKh0oX7q0zWcuUJ_nFIIjjZk6ixculo" />
|
|
7
|
+
<title>STM32F407 Emulator in the Browser — run real Cortex-M4 firmware (and DOOM)</title>
|
|
8
|
+
<!-- Search + social metadata. The page had none, so search results and
|
|
9
|
+
shared links showed only the bare title with no summary. -->
|
|
10
|
+
<meta name="description" content="An STM32F407 microcontroller emulator that runs unmodified Cortex-M4 firmware in your browser: Unicorn CPU core plus a Rust peripheral model (GPIO, USART, DMA, ETH, I2C, SPI, ADC, timers), all compiled to WebAssembly. Boot 31 firmware presets, do real DHCP/TCP/HTTP networking, or play DOOM.">
|
|
11
|
+
<link rel="canonical" href="https://danish9661.github.io/stm32F4-emulator/">
|
|
12
|
+
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
|
13
|
+
<script type="application/ld+json">
|
|
14
|
+
{
|
|
15
|
+
"@context": "https://schema.org",
|
|
16
|
+
"@type": "SoftwareApplication",
|
|
17
|
+
"name": "STM32F407 Emulator",
|
|
18
|
+
"applicationCategory": "DeveloperApplication",
|
|
19
|
+
"operatingSystem": "Any (runs in a web browser)",
|
|
20
|
+
"url": "https://danish9661.github.io/stm32F4-emulator/",
|
|
21
|
+
"codeRepository": "https://github.com/danish9661/stm32F4-emulator",
|
|
22
|
+
"license": "https://www.gnu.org/licenses/gpl-3.0.html",
|
|
23
|
+
"description": "An STM32F407 microcontroller emulator that runs unmodified Cortex-M4 firmware in the browser: a Unicorn CPU core plus a Rust peripheral model (GPIO, USART, DMA, Ethernet, I2C, SPI, ADC, timers) compiled to WebAssembly. Includes DHCP/TCP/HTTP networking demos and a DOOM port.",
|
|
24
|
+
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
<meta property="og:type" content="website">
|
|
28
|
+
<meta property="og:site_name" content="STM32F407 Emulator">
|
|
29
|
+
<meta property="og:title" content="STM32F407 Emulator in the Browser">
|
|
30
|
+
<meta property="og:description" content="Run real Cortex-M4 firmware in a browser tab — Unicorn CPU + a Rust peripheral model in WebAssembly. Networking demos, 31 firmware presets, and DOOM.">
|
|
31
|
+
<meta property="og:url" content="https://danish9661.github.io/stm32F4-emulator/">
|
|
32
|
+
<meta name="twitter:card" content="summary">
|
|
33
|
+
<meta name="twitter:title" content="STM32F407 Emulator in the Browser">
|
|
34
|
+
<meta name="twitter:description" content="Run real Cortex-M4 firmware in a browser tab — Unicorn CPU + a Rust peripheral model in WebAssembly. Networking demos, 31 firmware presets, and DOOM.">
|
|
35
|
+
<style>
|
|
36
|
+
:root {
|
|
37
|
+
--bg: #0d1117;
|
|
38
|
+
--panel: #161b22;
|
|
39
|
+
--border: #30363d;
|
|
40
|
+
--fg: #e6edf3;
|
|
41
|
+
--dim: #8b949e;
|
|
42
|
+
--green: #3fb950;
|
|
43
|
+
--yellow: #d29922;
|
|
44
|
+
--red: #f85149;
|
|
45
|
+
--cyan: #39c5cf;
|
|
46
|
+
}
|
|
47
|
+
* { box-sizing: border-box; }
|
|
48
|
+
body {
|
|
49
|
+
margin: 0;
|
|
50
|
+
background: var(--bg);
|
|
51
|
+
color: var(--fg);
|
|
52
|
+
font: 13px/1.45 "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
53
|
+
}
|
|
54
|
+
header {
|
|
55
|
+
display: flex; align-items: center; gap: 14px;
|
|
56
|
+
padding: 10px 16px;
|
|
57
|
+
border-bottom: 1px solid var(--border);
|
|
58
|
+
background: var(--panel);
|
|
59
|
+
position: sticky; top: 0; z-index: 5;
|
|
60
|
+
/* Deliberately NOT flex-wrap:wrap — wrapping happens before shrinking, so
|
|
61
|
+
it pushed the status onto a second row (taller header, bottom controls
|
|
62
|
+
clipped). The tagline shrinks/ellipses instead, then hides <980px. */
|
|
63
|
+
}
|
|
64
|
+
/* The title and the DOOM link must never wrap their own text — only the
|
|
65
|
+
tagline is allowed to give up space. */
|
|
66
|
+
header h1 { font-size: 14px; margin: 0; font-weight: 600; white-space: nowrap; flex: none; }
|
|
67
|
+
/* One-line answer to "what am I looking at?" — the page previously jumped
|
|
68
|
+
straight into controls with no explanation of what it is. */
|
|
69
|
+
/* Shrinks with an ellipsis rather than forcing the header onto a second
|
|
70
|
+
row (which pushed the terminal's buttons off the bottom of the page). */
|
|
71
|
+
.tagline { font-size: 12px; color: var(--dim); white-space: nowrap;
|
|
72
|
+
flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
73
|
+
.tagline b { color: var(--fg); font-weight: 600; }
|
|
74
|
+
@media (max-width: 980px) { .tagline { display: none; } }
|
|
75
|
+
a.doomLink {
|
|
76
|
+
font-size: 12px; font-weight: 600; color: #000; text-decoration: none;
|
|
77
|
+
background: var(--green); padding: 4px 10px; border-radius: 4px;
|
|
78
|
+
white-space: nowrap; flex: none;
|
|
79
|
+
}
|
|
80
|
+
a.doomLink:hover { filter: brightness(1.2); }
|
|
81
|
+
#status { font-size: 12px; color: var(--dim); margin-left: auto; white-space: nowrap; }
|
|
82
|
+
.dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--dim); margin-right: 6px; vertical-align: middle; }
|
|
83
|
+
.dot.run { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
84
|
+
.dot.stop { background: var(--yellow); }
|
|
85
|
+
.dot.err { background: var(--red); }
|
|
86
|
+
/* App-shell layout: the header stays put, the terminal fills the left, and
|
|
87
|
+
the 13-panel sidebar scrolls on its own. Previously the whole document
|
|
88
|
+
scrolled, so reaching GPIO/peripherals meant scrolling the terminal off
|
|
89
|
+
the screen too. */
|
|
90
|
+
html, body { height: 100%; }
|
|
91
|
+
body { display: flex; flex-direction: column; overflow: hidden; }
|
|
92
|
+
main { display: grid; grid-template-columns: 1fr 360px; gap: 12px; padding: 12px;
|
|
93
|
+
max-width: 1500px; margin: 0 auto; width: 100%; box-sizing: border-box;
|
|
94
|
+
flex: 1; min-height: 0; }
|
|
95
|
+
main > section { display: flex; flex-direction: column; min-height: 0; }
|
|
96
|
+
main > aside { overflow-y: auto; min-height: 0; padding-right: 2px; }
|
|
97
|
+
/* Below the breakpoint the columns stack, so let the page scroll normally
|
|
98
|
+
again — otherwise the lower panels become unreachable. */
|
|
99
|
+
@media (max-width: 960px) {
|
|
100
|
+
body { overflow: auto; }
|
|
101
|
+
main { grid-template-columns: 1fr; flex: none; min-height: 0; }
|
|
102
|
+
main > aside { overflow-y: visible; }
|
|
103
|
+
}
|
|
104
|
+
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
105
|
+
.panel h2 { margin: 0; padding: 8px 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--dim); border-bottom: 1px solid var(--border); }
|
|
106
|
+
/* Sidebar panels fold away: 13 of them stacked in a 360px column meant a
|
|
107
|
+
long scroll to reach GPIO/peripherals, and most (OLED/TFT/buzzer/RTC…)
|
|
108
|
+
are irrelevant unless you booted that specific firmware. Enhanced by JS
|
|
109
|
+
below — the markup and every app.js selector are untouched, panels are
|
|
110
|
+
only hidden with CSS, so nothing that reads them breaks. */
|
|
111
|
+
aside .panel h2 { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; }
|
|
112
|
+
aside .panel h2::before { content: "▾"; font-size: 9px; color: var(--dim); transition: transform .12s; }
|
|
113
|
+
aside .panel.collapsed h2 { border-bottom: none; }
|
|
114
|
+
aside .panel.collapsed h2::before { transform: rotate(-90deg); }
|
|
115
|
+
aside .panel.collapsed > *:not(h2) { display: none !important; }
|
|
116
|
+
aside .panel h2:hover { color: var(--fg); }
|
|
117
|
+
#uart {
|
|
118
|
+
/* Fills whatever the flex column leaves after the h2 and the controls
|
|
119
|
+
row, instead of a hardcoded 100vh-140px that had to be re-guessed
|
|
120
|
+
every time the header or button row changed height. */
|
|
121
|
+
flex: 1; min-height: 200px;
|
|
122
|
+
overflow-y: auto; padding: 10px 12px;
|
|
123
|
+
font: inherit; white-space: pre-wrap; word-break: break-all;
|
|
124
|
+
background: #010409; color: #c9d1d9;
|
|
125
|
+
}
|
|
126
|
+
.controls { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--border); flex-wrap: wrap; }
|
|
127
|
+
button {
|
|
128
|
+
background: #21262d; color: var(--fg);
|
|
129
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
130
|
+
padding: 5px 12px; font: inherit; font-size: 12px; cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
button:hover { background: #30363d; }
|
|
133
|
+
button.primary { background: #238636; border-color: #2ea043; }
|
|
134
|
+
button.primary:hover { background: #2ea043; }
|
|
135
|
+
button.danger { background: #da3633; border-color: #f85149; }
|
|
136
|
+
button:disabled { opacity: .45; cursor: default; }
|
|
137
|
+
.stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); }
|
|
138
|
+
.stat { background: var(--panel); padding: 7px 12px; }
|
|
139
|
+
.stat b { display: block; font-size: 15px; }
|
|
140
|
+
.stat span { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: .06em; }
|
|
141
|
+
.sec { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
142
|
+
.sec label { font-size: 11px; color: var(--dim); }
|
|
143
|
+
.sec input[type=text], .sec select {
|
|
144
|
+
width: 100%; background: #21262d; color: var(--fg);
|
|
145
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
146
|
+
padding: 5px; font: inherit; font-size: 12px;
|
|
147
|
+
}
|
|
148
|
+
.sec input[type=file] { width: 100%; font: inherit; font-size: 12px; color: var(--dim); }
|
|
149
|
+
.sec p { color: var(--dim); font-size: 11px; margin: 0; }
|
|
150
|
+
.row { display: flex; gap: 8px; }
|
|
151
|
+
.row > * { flex: 1; }
|
|
152
|
+
#frames { max-height: 160px; overflow-y: auto; }
|
|
153
|
+
.frame { padding: 5px 12px; border-bottom: 1px solid var(--border); font-size: 12px; }
|
|
154
|
+
.frame .tag { font-weight: 700; }
|
|
155
|
+
.frame.tx .tag { color: var(--yellow); }
|
|
156
|
+
.frame.rx .tag { color: var(--cyan); }
|
|
157
|
+
.gpio { padding: 8px 12px; font-size: 11px; }
|
|
158
|
+
.gpio .bank { margin-bottom: 7px; }
|
|
159
|
+
.gpio .bank .bname { color: var(--dim); width: 26px; display: inline-block; }
|
|
160
|
+
.gpio .pins { display: inline-flex; gap: 3px; vertical-align: middle; }
|
|
161
|
+
.pin { width: 10px; height: 10px; border-radius: 2px; background: #21262d; border: 1px solid var(--border); }
|
|
162
|
+
.pin.out-on { background: var(--green); box-shadow: 0 0 4px var(--green); }
|
|
163
|
+
.pin.out-off { background: #30363d; }
|
|
164
|
+
.pin.in-on { background: var(--cyan); }
|
|
165
|
+
.gpio .regs { color: var(--dim); font-size: 10px; margin-top: 2px; }
|
|
166
|
+
.periph { padding: 8px 12px; font-size: 11px; }
|
|
167
|
+
.periph .line { display: flex; justify-content: space-between; padding: 2px 0; border-bottom: 1px dashed #21262d; }
|
|
168
|
+
.periph .line b { color: var(--fg); font-weight: 600; }
|
|
169
|
+
.periph .line span { color: var(--dim); }
|
|
170
|
+
#symbols { max-height: 180px; overflow-y: auto; padding: 4px 0; }
|
|
171
|
+
.sym { display: flex; justify-content: space-between; padding: 2px 12px; font-size: 11px; }
|
|
172
|
+
.sym span { color: var(--dim); }
|
|
173
|
+
.gwstat { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--dim); }
|
|
174
|
+
.gwstat .dot { margin: 0; }
|
|
175
|
+
footer { padding: 10px 16px; color: var(--dim); font-size: 11px; border-top: 1px solid var(--border); }
|
|
176
|
+
</style>
|
|
177
|
+
</head>
|
|
178
|
+
<body>
|
|
179
|
+
<header>
|
|
180
|
+
<h1>STM32F407 in the Browser</h1>
|
|
181
|
+
<span class="tagline">Real Cortex-M4 firmware on an emulated MCU — <b>Unicorn CPU + Rust peripherals</b>, all WebAssembly</span>
|
|
182
|
+
<a class="doomLink" href="doom.html">▶ Play DOOM</a>
|
|
183
|
+
<span id="status"><span class="dot" id="dot"></span><span id="statusText">booting…</span></span>
|
|
184
|
+
</header>
|
|
185
|
+
<main>
|
|
186
|
+
<section class="panel">
|
|
187
|
+
<h2>UART terminal (firmware output)</h2>
|
|
188
|
+
<div id="uart"></div>
|
|
189
|
+
<div class="controls">
|
|
190
|
+
<button id="btnRun" class="primary">Run</button>
|
|
191
|
+
<button id="btnStop">Stop</button>
|
|
192
|
+
<button id="btnReset">Reset</button>
|
|
193
|
+
<button id="btnClear">Clear log</button>
|
|
194
|
+
<button id="btnSaveLog">Save log</button>
|
|
195
|
+
<label style="color:var(--dim);font-size:12px;margin-left:auto;display:flex;align-items:center;gap:6px;">
|
|
196
|
+
<input type="checkbox" id="chkAuto" checked> autoscroll
|
|
197
|
+
</label>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="controls" style="border-top:none;padding-top:0;">
|
|
200
|
+
<input type="text" id="rxInput" placeholder="UART RX — type bytes to send to the firmware (Enter=CR, Shift+Enter=LF)" style="flex:1;background:#21262d;color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px;">
|
|
201
|
+
<button id="btnSend">Send</button>
|
|
202
|
+
</div>
|
|
203
|
+
</section>
|
|
204
|
+
<aside>
|
|
205
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
206
|
+
<h2>Firmware</h2>
|
|
207
|
+
<div class="sec">
|
|
208
|
+
<select id="fwSelect">
|
|
209
|
+
<optgroup label="Network">
|
|
210
|
+
<option value="eth_http">eth_http — web client (DHCP+TCP+HTTP)</option>
|
|
211
|
+
<option value="eth_dhcp">eth_dhcp — DHCP loop</option>
|
|
212
|
+
<option value="eth_test">eth_test — raw ETH TX test</option>
|
|
213
|
+
</optgroup>
|
|
214
|
+
<optgroup label="Bare-metal demo">
|
|
215
|
+
<option value="blinky">blinky — LED blinker, no network</option>
|
|
216
|
+
<option value="blink_serial">blink_serial — UART4 + blink loop</option>
|
|
217
|
+
<option value="timer_test">timer_test — TIM tick 1s prints</option>
|
|
218
|
+
<option value="arduino_test">arduino_test — Arduino core OK + ADC</option>
|
|
219
|
+
<option value="hal_test">hal_test — HAL init + SysTick</option>
|
|
220
|
+
</optgroup>
|
|
221
|
+
<optgroup label="Peripheral tests">
|
|
222
|
+
<option value="periph_test">periph_test — RCC/GPIO checks</option>
|
|
223
|
+
<option value="new_periph_test">new_periph_test — PWR/FSMC regs</option>
|
|
224
|
+
<option value="deep_periph_test">deep_periph_test — deep reg r/w</option>
|
|
225
|
+
<option value="comprehensive_test">comprehensive_test — EXTI/DMA/USART</option>
|
|
226
|
+
<option value="eth_irq_test">eth_irq_test — interrupt-driven Ethernet</option>
|
|
227
|
+
<option value="edge_test">edge_test — DMA edge cases</option>
|
|
228
|
+
<option value="i2s_sai_test">i2s_sai_test — I2S audio test</option>
|
|
229
|
+
<option value="spi_tft_test">spi_tft_test — SPI TFT LCD test</option>
|
|
230
|
+
<option value="audio_test">audio_test — I2S DMA WAV play + capture</option>
|
|
231
|
+
<option value="ltdc_test">ltdc_test — LTDC scanout gradient (draws in the display panel)</option>
|
|
232
|
+
<option value="oled_test">oled_test — SSD1306 OLED over I2C (draws in the OLED panel)</option>
|
|
233
|
+
<option value="tft_test">tft_test — ILI9341 TFT over SPI (draws in the TFT panel)</option>
|
|
234
|
+
<option value="buzzer_test">buzzer_test — TIM2 PWM melody (live frequency in the buzzer panel)</option>
|
|
235
|
+
<option value="audio_play_test">audio_play_test — I2S sine playback (speaker panel plays the samples)</option>
|
|
236
|
+
<option value="rtc_test">rtc_test — DS3231 RTC time/temp over I2C1 (live clock in the RTC panel)</option>
|
|
237
|
+
<option value="can_test">can_test — CAN1/CAN2 bus loopback + arbitration (2-node bus, no device panel)</option>
|
|
238
|
+
<option value="test_firmware">test_firmware — TIM2/ADC dump</option>
|
|
239
|
+
</optgroup>
|
|
240
|
+
<optgroup label="Crypto">
|
|
241
|
+
<option value="crypto_test">crypto_test — AES-128 ECB/CBC</option>
|
|
242
|
+
<option value="crypto_deep_test">crypto_deep_test — AES CTR deep</option>
|
|
243
|
+
<option value="rx_crypto_test">rx_crypto_test — RX interrupt + CRC</option>
|
|
244
|
+
</optgroup>
|
|
245
|
+
<optgroup label="UART echo (needs input)">
|
|
246
|
+
<option value="echo_test">echo_test — UART echo server</option>
|
|
247
|
+
<option value="rx_interrupt_test">rx_interrupt_test — RX interrupt</option>
|
|
248
|
+
</optgroup>
|
|
249
|
+
</select>
|
|
250
|
+
<button id="btnBoot" class="primary" style="width:100%;">Boot preset</button>
|
|
251
|
+
<input type="file" id="fwFile" accept=".bin,.hex,.elf,.map">
|
|
252
|
+
<p id="fwHint">Load a custom firmware: <b>.bin</b> (raw flash), <b>.hex</b> (Intel HEX), <b>.elf</b> (loadable segments + symbols), or a <b>.map</b> file for the symbol table only. Firmware files boot immediately. Nothing runs until you press <b>Boot preset</b> or load a file.</p>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
256
|
+
<h2>Network gateway</h2>
|
|
257
|
+
<div class="sec">
|
|
258
|
+
<input type="text" id="gwUrl" value="ws://127.0.0.1:5099/api/network-gateway" spellcheck="false">
|
|
259
|
+
<div class="row">
|
|
260
|
+
<button id="btnGw" class="primary">Connect</button>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="gwstat"><span class="dot" id="gwDot"></span><span id="gwStatus">offline — using the scripted network (netsim)</span></div>
|
|
263
|
+
<p>When connected, all Ethernet frames are sent to a real network stack (openhw-gw + gVisor) instead of netsim. Run <b>openhw-gw</b> locally first. Note: GitHub Pages serves https, which blocks plain ws:// — serve this page over http:// when using a gateway.</p>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
267
|
+
<h2>Machine state</h2>
|
|
268
|
+
<div class="stats">
|
|
269
|
+
<div class="stat"><b id="stInst">0</b><span>instructions</span></div>
|
|
270
|
+
<div class="stat"><b id="stMips">0.0</b><span>MIPS</span></div>
|
|
271
|
+
<div class="stat"><b id="stSteps">0</b><span>batches</span></div>
|
|
272
|
+
<div class="stat"><b id="stRounds">0</b><span>rounds</span></div>
|
|
273
|
+
<div class="stat"><b id="stPc">—</b><span>PC</span></div>
|
|
274
|
+
<div class="stat"><b id="stSp">—</b><span>SP</span></div>
|
|
275
|
+
<div class="stat"><b id="stXpsr">—</b><span>XPSR</span></div>
|
|
276
|
+
<div class="stat"><b id="stFw">—</b><span>firmware</span></div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
280
|
+
<h2>GPIO</h2>
|
|
281
|
+
<div id="gpio" class="gpio"></div>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
284
|
+
<h2>LTDC display</h2>
|
|
285
|
+
<canvas id="ltdcCanvas" width="320" height="160"
|
|
286
|
+
style="width:100%;box-sizing:border-box;display:block;background:#000;
|
|
287
|
+
image-rendering:pixelated;border-bottom:1px solid var(--border);"></canvas>
|
|
288
|
+
<div style="padding:6px 10px;font-size:11px;color:var(--dim)">
|
|
289
|
+
<span id="ltdcInfo">scanout idle</span>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
293
|
+
<h2>OLED SSD1306 (I2C)</h2>
|
|
294
|
+
<canvas id="oledCanvas" width="256" height="128"
|
|
295
|
+
style="width:100%;box-sizing:border-box;display:block;background:#000;
|
|
296
|
+
image-rendering:pixelated;border-bottom:1px solid var(--border);"></canvas>
|
|
297
|
+
<div style="padding:6px 10px;font-size:11px;color:var(--dim)">
|
|
298
|
+
<span id="oledInfo">no OLED firmware</span>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
302
|
+
<h2>TFT ILI9341 (SPI)</h2>
|
|
303
|
+
<canvas id="tftCanvas" width="240" height="320"
|
|
304
|
+
style="width:100%;box-sizing:border-box;display:block;background:#000;
|
|
305
|
+
image-rendering:pixelated;border-bottom:1px solid var(--border);"></canvas>
|
|
306
|
+
<div style="padding:6px 10px;font-size:11px;color:var(--dim)">
|
|
307
|
+
<span id="tftInfo">no TFT firmware</span>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
311
|
+
<h2>Buzzer (TIM PWM)</h2>
|
|
312
|
+
<div style="padding:6px 10px;font-size:11px;color:var(--dim)">
|
|
313
|
+
<span id="buzzerInfo">no buzzer firmware</span>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
317
|
+
<h2>Speaker (I2S)</h2>
|
|
318
|
+
<div style="padding:6px 10px;font-size:11px;color:var(--dim)">
|
|
319
|
+
<span id="speakerInfo">no audio firmware</span>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
323
|
+
<h2>RTC DS3231 (I2C)</h2>
|
|
324
|
+
<div style="padding:6px 10px;font-size:11px;color:var(--dim)">
|
|
325
|
+
<span id="rtcInfo">no RTC firmware</span>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
329
|
+
<h2>Peripherals</h2>
|
|
330
|
+
<div id="periph" class="periph"></div>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="panel" style="margin-bottom:12px;">
|
|
333
|
+
<h2>Symbols</h2>
|
|
334
|
+
<div id="symbols"></div>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="panel">
|
|
337
|
+
<h2>Network packets</h2>
|
|
338
|
+
<div id="frames"></div>
|
|
339
|
+
</div>
|
|
340
|
+
</aside>
|
|
341
|
+
</main>
|
|
342
|
+
<footer>
|
|
343
|
+
Unicorn 2.1.4 (WASM) CPU + Rust peripheral model (WASM) + this page's JS driver. Everything runs locally in your tab — no network calls after the initial asset load (unless you connect a gateway).
|
|
344
|
+
· <a href="doom.html">Play DOOM</a> (doomgeneric F407 port)
|
|
345
|
+
</footer>
|
|
346
|
+
|
|
347
|
+
<script src="vendor/unicorn_arm.js"></script>
|
|
348
|
+
<script type="module" src="app.js"></script>
|
|
349
|
+
<script>
|
|
350
|
+
// Collapsible sidebar panels (progressive enhancement — app.js owns the
|
|
351
|
+
// contents, this only toggles a class). Device panels start collapsed
|
|
352
|
+
// because they are only meaningful for the firmware that drives them;
|
|
353
|
+
// Firmware/Network/Machine state stay open since they are the controls you
|
|
354
|
+
// actually start from. Choices persist per panel.
|
|
355
|
+
(() => {
|
|
356
|
+
const COLLAPSED_BY_DEFAULT = new Set([
|
|
357
|
+
'ltdc display', 'oled ssd1306 (i2c)', 'tft ili9341 (spi)', 'buzzer (tim pwm)',
|
|
358
|
+
'speaker (i2s)', 'rtc ds3231 (i2c)', 'symbols', 'network packets',
|
|
359
|
+
]);
|
|
360
|
+
for (const panel of document.querySelectorAll('aside .panel')) {
|
|
361
|
+
const h2 = panel.querySelector('h2');
|
|
362
|
+
if (!h2) continue;
|
|
363
|
+
const key = 'panel:' + h2.textContent.trim().toLowerCase();
|
|
364
|
+
const saved = localStorage.getItem(key);
|
|
365
|
+
const collapsed = saved === null
|
|
366
|
+
? COLLAPSED_BY_DEFAULT.has(h2.textContent.trim().toLowerCase())
|
|
367
|
+
: saved === '1';
|
|
368
|
+
panel.classList.toggle('collapsed', collapsed);
|
|
369
|
+
h2.setAttribute('role', 'button');
|
|
370
|
+
h2.setAttribute('tabindex', '0');
|
|
371
|
+
h2.setAttribute('aria-expanded', String(!collapsed));
|
|
372
|
+
const toggle = () => {
|
|
373
|
+
const now = panel.classList.toggle('collapsed');
|
|
374
|
+
h2.setAttribute('aria-expanded', String(!now));
|
|
375
|
+
localStorage.setItem(key, now ? '1' : '0');
|
|
376
|
+
};
|
|
377
|
+
h2.addEventListener('click', toggle);
|
|
378
|
+
h2.addEventListener('keydown', (e) => {
|
|
379
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
})();
|
|
383
|
+
</script>
|
|
384
|
+
</body>
|
|
385
|
+
</html>
|
package/site/loaders.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Firmware image loaders: Intel HEX, ELF32, and GCC linker map parsers.
|
|
2
|
+
// Pure JS, no imports. All functions are synchronous.
|
|
3
|
+
|
|
4
|
+
// ── Intel HEX ──────────────────────────────────────────────────────────────
|
|
5
|
+
// Returns { flash, ram, entry } — Uint8Array images for 0x08000000 and
|
|
6
|
+
// 0x20000000, plus an optional entry point from the 0x05 record.
|
|
7
|
+
export function parseIntelHex(text) {
|
|
8
|
+
const image = new Map(); // addr -> byte
|
|
9
|
+
let base = 0; // extended linear address (record 0x04)
|
|
10
|
+
let entry = null;
|
|
11
|
+
for (let line of text.split(/\r?\n/)) {
|
|
12
|
+
line = line.trim();
|
|
13
|
+
if (!line) continue;
|
|
14
|
+
if (line[0] !== ':') throw new Error('not an Intel HEX record: ' + line.slice(0, 16));
|
|
15
|
+
const bytes = [];
|
|
16
|
+
for (let i = 1; i < line.length; i += 2) bytes.push(parseInt(line.slice(i, i + 2), 16));
|
|
17
|
+
const count = bytes[0], addr = (bytes[1] << 8) | bytes[2], type = bytes[3];
|
|
18
|
+
let sum = 0;
|
|
19
|
+
for (const b of bytes) sum = (sum + b) & 0xFF;
|
|
20
|
+
if (sum !== 0) throw new Error('bad HEX checksum on line: ' + line.slice(0, 16));
|
|
21
|
+
const data = bytes.slice(4, 4 + count);
|
|
22
|
+
if (type === 0x00) {
|
|
23
|
+
for (let i = 0; i < count; i++) image.set(base + addr + i, data[i]);
|
|
24
|
+
} else if (type === 0x01) {
|
|
25
|
+
break;
|
|
26
|
+
} else if (type === 0x04) {
|
|
27
|
+
base = ((data[0] << 8) | data[1]) << 16;
|
|
28
|
+
} else if (type === 0x05) {
|
|
29
|
+
entry = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
|
|
30
|
+
}
|
|
31
|
+
// types 0x02/0x03 (segment) are rare; ignore
|
|
32
|
+
}
|
|
33
|
+
const toImage = (start, end) => {
|
|
34
|
+
const out = new Uint8Array(end - start);
|
|
35
|
+
let any = false;
|
|
36
|
+
for (const [addr, b] of image) {
|
|
37
|
+
if (addr >= start && addr < end) { out[addr - start] = b; any = true; }
|
|
38
|
+
}
|
|
39
|
+
return any ? out : null;
|
|
40
|
+
};
|
|
41
|
+
return { flash: toImage(0x08000000, 0x08100000), ram: toImage(0x20000000, 0x20020000), entry };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── ELF32 ─────────────────────────────────────────────────────────────────
|
|
45
|
+
// Extracts PT_LOAD segments (flash/RAM images + preload list) and, if the
|
|
46
|
+
// symtab is present, the symbol table for the symbols panel.
|
|
47
|
+
export function parseElf(bytes) {
|
|
48
|
+
if (bytes.length < 52) throw new Error('file too small to be an ELF');
|
|
49
|
+
const b = new Uint8Array(bytes);
|
|
50
|
+
if (b[0] !== 0x7F || b[1] !== 0x45 || b[2] !== 0x4C || b[3] !== 0x46) throw new Error('not an ELF file');
|
|
51
|
+
if (b[4] !== 1 || b[5] !== 1) throw new Error('only 32-bit little-endian ELF is supported');
|
|
52
|
+
const u32 = (o) => (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0;
|
|
53
|
+
const u16 = (o) => (b[o] | (b[o + 1] << 8)) >>> 0;
|
|
54
|
+
const entry = u32(24);
|
|
55
|
+
const phoff = u32(28), shentsize = u16(46), shnum = u16(48), shstrndx = u16(50);
|
|
56
|
+
const phentsize = u16(42), phnum = u16(44);
|
|
57
|
+
|
|
58
|
+
const segments = [];
|
|
59
|
+
for (let i = 0; i < phnum; i++) {
|
|
60
|
+
const o = phoff + i * phentsize;
|
|
61
|
+
if (u32(o) !== 1) continue; // PT_LOAD
|
|
62
|
+
const off = u32(o + 4), vaddr = u32(o + 8), filesz = u32(o + 16), memsz = u32(o + 20);
|
|
63
|
+
if (!filesz) continue;
|
|
64
|
+
segments.push({ vaddr, data: new Uint8Array(b.slice(off, off + filesz)), memsz });
|
|
65
|
+
}
|
|
66
|
+
if (segments.length === 0) throw new Error('ELF has no loadable segments');
|
|
67
|
+
|
|
68
|
+
const FLASH_START = 0x08000000, FLASH_END = 0x08100000;
|
|
69
|
+
const RAM_START = 0x20000000, RAM_END = 0x20020000;
|
|
70
|
+
const flash = new Uint8Array(FLASH_END - FLASH_START);
|
|
71
|
+
const ram = new Uint8Array(RAM_END - RAM_START);
|
|
72
|
+
const extraMem = [];
|
|
73
|
+
for (const seg of segments) {
|
|
74
|
+
if (seg.vaddr >= FLASH_START && seg.vaddr + seg.data.length <= FLASH_END) {
|
|
75
|
+
flash.set(seg.data, seg.vaddr - FLASH_START);
|
|
76
|
+
} else if (seg.vaddr >= RAM_START && seg.vaddr + seg.data.length <= RAM_END) {
|
|
77
|
+
ram.set(seg.data, seg.vaddr - RAM_START);
|
|
78
|
+
extraMem.push({ addr: seg.vaddr, data: seg.data });
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error('loadable segment at 0x' + seg.vaddr.toString(16) + ' outside FLASH/RAM');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let symbols = null;
|
|
85
|
+
if (shentsize && shnum) {
|
|
86
|
+
const shoff = u32(32);
|
|
87
|
+
let symtab = null, strtab = null;
|
|
88
|
+
for (let i = 0; i < shnum; i++) {
|
|
89
|
+
const o = shoff + i * shentsize;
|
|
90
|
+
if (o + shentsize > b.length) break;
|
|
91
|
+
const type = u32(o + 4);
|
|
92
|
+
if (type === 2) symtab = { offset: u32(o + 16), size: u32(o + 20), entsize: u32(o + 36), link: u16(o + 24) };
|
|
93
|
+
if (type === 3 && strtab === null) strtab = { offset: u32(o + 16), size: u32(o + 20) };
|
|
94
|
+
}
|
|
95
|
+
if (symtab && strtab && symtab.entsize) {
|
|
96
|
+
symbols = [];
|
|
97
|
+
const names = new Uint8Array(b.slice(strtab.offset, strtab.offset + strtab.size));
|
|
98
|
+
const count = Math.floor(symtab.size / symtab.entsize);
|
|
99
|
+
for (let i = 0; i < count; i++) {
|
|
100
|
+
const o = symtab.offset + i * symtab.entsize;
|
|
101
|
+
const nameOff = u32(o), value = u32(o + 4), size = u32(o + 8);
|
|
102
|
+
const info = b[o + 12], shndx = u16(o + 14);
|
|
103
|
+
if (!nameOff || shndx === 0) continue; // no name or UNDEF
|
|
104
|
+
const type = info & 0xF;
|
|
105
|
+
if (type !== 2 && type !== 1) continue; // funcs + objects only
|
|
106
|
+
let name = '';
|
|
107
|
+
for (let j = nameOff; j < names.length && names[j] !== 0; j++) name += String.fromCharCode(names[j]);
|
|
108
|
+
symbols.push({ name, addr: value, size });
|
|
109
|
+
}
|
|
110
|
+
symbols.sort((a, c) => a.addr - c.addr);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const any = (img) => img.some((v) => v !== 0);
|
|
114
|
+
return { flash: any(flash) ? flash : null, ram: any(ram) ? ram : null, extraMem, entry, symbols };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── GCC linker map ─────────────────────────────────────────────────────────
|
|
118
|
+
// Extracts "0x08000185 _start" style symbol lines.
|
|
119
|
+
export function parseMap(text) {
|
|
120
|
+
const symbols = [];
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
for (const line of text.split(/\r?\n/)) {
|
|
123
|
+
const m = line.match(/0x([0-9a-fA-F]{8,})\s+([_a-zA-Z][_a-zA-Z0-9$]*)/);
|
|
124
|
+
if (!m) continue;
|
|
125
|
+
const addr = parseInt(m[1], 16);
|
|
126
|
+
if (!((addr >= 0x08000000 && addr < 0x08100000) || (addr >= 0x20000000 && addr < 0x20020000))) continue;
|
|
127
|
+
if (seen.has(m[2])) continue;
|
|
128
|
+
seen.add(m[2]);
|
|
129
|
+
symbols.push({ name: m[2], addr });
|
|
130
|
+
}
|
|
131
|
+
symbols.sort((a, b) => a.addr - b.addr);
|
|
132
|
+
return symbols;
|
|
133
|
+
}
|