# ESP32 模擬器 開發 / 部署 / 修改指南 (Developer guide) 給「要部署或改程式的人(含小型 AI 模型)」:照著下面「配方」逐步做即可。所有路徑相對於專案根目錄。 ## 0. 一分鐘總覽 瀏覽器內執行的 ESP32 (Xtensa LX6 雙核) 模擬器。主執行緒 `web/app.js` 負責 UI;Web Worker `web/js/worker.js` 跑 `web/js/soc.js`(SoC + 週邊)。 CPU 有兩套等價實作:WASM(`wasm/xtensa.c`,預設)與 JS(`web/js/xtensa.js`,參考/除錯用)。兩者對同一韌體執行的指令數必須逐位相同。 ## 1. 目錄地圖 | 路徑 | 內容 | |---|---| | `web/index.html style.css app.js` | UI:範例選單、載入二進位表、虛擬鍵盤、序列埠、GPIO 面板、畫面 canvas | | `web/js/worker.js` | 模擬迴圈(對齊真實時間、送 frame/uart/gpio/status 給 UI),處理 UI 訊息 | | `web/js/soc.js` | 記憶體對映、頁表、中斷矩陣、事件佇列、DPORT、UART、TIMG、FRC、RTC_CNTL、eFuse、ROM hook、主迴圈 `run()` | | `web/js/xtensa.js` | JS 版 CPU(參考實作,可下斷點/trace) | | `wasm/xtensa.c` → `wasm/xtensa.wasm` | WASM 版 CPU(與 xtensa.js 逐行對應) | | `web/js/cpu_wasm.js` | WASM CPU 的 JS 包裝(狀態陣列索引表 `I`,必須與 xtensa.c 的 enum 一致) | | `web/js/spi.js` | SPI flash 晶片、PSRAM 晶片、SPI 控制器 | | `web/js/gpio.js` | GPIO 矩陣、腳位電氣模型(開集極/外部拉低/上拉)、IO_MUX | | `web/js/i2s.js` | I2S DMA 描述子鏈 + VGA 螢幕模型(`VGA32_BOARD` 為電阻網路接線) | | `web/js/ps2.js` `ulp.js` | PS/2 鍵盤/滑鼠裝置、ULP 協處理器直譯器 + RTC_IO + SENS | | `web/js/sha.js` | SHA-1/256/384/512 硬體引擎 | | `web/assets.js` | **生成檔**(gzip+base64 的 ROM、wasm、範例韌體),勿手改 | | `web/rom/` | ESP32 ROM 映像(`esp32-v3-rom.bin`, `esp32-v3-rom-app.bin`) | | `firmware/*/` | 範例 Arduino 草稿碼(dosvga32=FreeDOS、vgademo、echo);`firmware/dist/` 為編好的 flash | | `tools/` | 除錯/測試腳本(見 §6) | | `scripts/` | `setup.sh build.sh deploy.sh pack_source.sh` | ## 2. 環境(第一次) 需要 Linux、python3、node>=18、curl。 ``` scripts/setup.sh # 安裝 zig(編 wasm)、arduino-cli+ESP32 core 2.0.17+FabGL、下載 ROM ``` 不改韌體/wasm 時可略過,只要有 node 與 python3(+ `pip install huggingface_hub pillow playwright`)。 ## 3. 建置 ``` scripts/build.sh wasm # 改了 wasm/xtensa.c 後 scripts/build.sh firmware # 改了 firmware/* 草稿碼後(輸出 firmware/dist/*.flash.bin) scripts/build.sh assets # 任何 ROM/wasm/韌體變動後都要重新生成 web/assets.js scripts/build.sh # 全部 ``` 本機預覽:`python3 -m http.server 8000 --directory web` → 開 http://localhost:8000 ## 4. 部署到 Hugging Face(Token 只用環境變數,絕不寫進檔案) ``` export HF_TOKEN=hf_xxxxxxxx # 需 write 權限 export HF_SPACE=使用者/空間名 # 可省略,預設 HelloSun/esp32emu scripts/deploy.sh ``` 它會:重生 assets → 壓縮整個專案成 `dist/esp32emu-source.tar.gz` → 上傳網站、原始碼、文件、腳本、壓縮包(Space 路徑 `downloads/esp32emu-source.tar.gz`)。 注意:HF 對二進位檔的下載會 302 轉址且無 CORS,所以網站用的 ROM/wasm/韌體一律以 base64 內嵌在 `assets.js`,不要改成 fetch 二進位檔。 ## 5. 修改配方(改什麼 → 改哪裡 → 怎麼驗證) 1. **新增內建範例**:把草稿碼放 `firmware//.ino` → `scripts/build.sh firmware`(需在 build.sh 的 for 清單加名字)→ 在 `tools/build_assets.py` 的 `examples` 加 `{'id','title','psram','flash'}` → `scripts/build.sh assets`。 2. **新增/修正週邊暫存器**:在 `web/js/soc.js` 找對應類別(Uart/TimerGroup/RtcCntl/Efuse/Frc…)或新建類別,於建構子 `reg(基底位址, 裝置)` 註冊;裝置要有 `read(off)`/`write(off,v)`(off 為頁內位元組偏移,32 位元存取)。未實作位址讀 0。中斷用 `soc.setIrq(來源編號, 0/1)`(編號見 ESP-IDF `soc/soc.h` 的 `ETS_*_SOURCE`)。延遲事件用 `soc.scheduleIn(奈秒, fn)`。 3. **改 CPU 指令行為**:同時改 `web/js/xtensa.js` 與 `wasm/xtensa.c`(兩邊結構相同),重建 wasm,然後跑 §6 的「JS/WASM 一致性」。 4. **ROM 函式加速/替身 (HLE)**:`soc.addRomHook(位址, function(a2..a7){return 回傳值})`,見 `installRomHooks()`(目前用於 APLL 的 `rom_i2c_*`)。 5. **新增板子接線(VGA 腳位)**:`web/js/i2s.js` 的 `VGA32_BOARD`;PS/2 腳位在 `soc.js` 建構子 `new Ps2Device(this, clk, dat, kind)`;UI 示意圖在 `app.js` 的 `buildBoard()`。 6. **UI 調整**:只動 `web/index.html style.css app.js`;與 worker 的訊息格式見 `worker.js` 的 `self.onmessage` 與 `app.js` 的 `worker.onmessage`。 7. **時序/速度**:`soc.run()` 的 `cpi`(每指令週期,預設 1,VGA 逐行 ISR 需要 ≤1)、`slice`(切片指令數,預設 256)。雙核同時進中斷時 `lockstep` 會細切片模擬真實競爭。 ## 6. 測試與除錯工具(node,皆在 tools/) | 目的 | 指令 | |---|---| | 跑韌體看序列埠 | `node tools/runw.mjs firmware/dist/echo.flash.bin 4`(加 `--js` 用 JS 核心、`--psram`) | | **JS/WASM 一致性** | 分別 `node tools/runw.mjs X 5 --psram` 與 `... --js`,最後印的 instr 數必須相同 | | 擷取 VGA 畫面成 ppm | `node tools/vgaw.mjs X 秒 間隔 [--psram] [--keys=3.5:KeyH,...]` → `/tmp/opencode/frame_N.ppm` | | 例外/崩潰定位 | `node tools/exc.mjs X 秒 --syms=`(用 `xtensa-esp32-elf-nm -n elf` 產生符號檔) | | 找函式呼叫序列 | `node tools/bp.mjs X 函式名 --syms=...` | | 取樣剖析 | `node tools/prof.mjs X 秒 --syms=... --warm=8` | | 瀏覽器整合測試 | `python3 tools/browser_test.py 秒 截圖.png`(playwright;需 `playwright install chromium`) | | 效能微基準 | `node tools/bench.mjs` | 除錯技巧:用 `--js` 核心才能 trace/斷點(`cpu.enableTrace()`、`cpu.bpAddr`);WASM 核心看不到。 ## 7. 常見陷阱(已踩過) - 改 `wasm/xtensa.c` 的狀態索引 enum 時,必須同步 `web/js/cpu_wasm.js` 的 `I` 表。 - 未對齊的 L32I/S32I 必須真的支援(硬體支援),不能遮罩位址。 - ULP 的 `MOVR` 取 sreg、`MOVI` 取立即值;ULP 觸發的 GPIO 事件時間用 `soc.nowNs`(已處理 `ulpNow`)。 - `I2S OUT_EOF_DES_ADDR` 要回傳完整 32 位元位址,否則 FabGL 的畫面幀同步錯亂。 - 雙核同時收到同一中斷時要同時進入 ISR(`deliverNow`),否則 FabGL 的 RTC 喚醒 ISR 會輸給 core0 的 `rtc_isr`。 - WASM 記憶體固定 64MB(與 wasm 匯入記憶體一致),heap 佈局常數在 `soc.js` 開頭 `H_*`。 - 效能上限約 80–110 MIPS/核心(單核直譯),FreeDOS 約真機 25–100%。 ## 8. 授權 GPL-3.0-or-later(範例韌體含 FabGL)。ROM 取自 Espressif QEMU 發行版;FreeDOS 為 GPL。