more
This commit is contained in:
+23
-17
@@ -16,15 +16,16 @@ touching QEMU internals. Pairs with `ROADMAP.md` (the plan) — this is the
|
||||
`0002-qemu-wasm.patch` ports qemu-wasm's host backend onto the same pinned
|
||||
Espressif QEMU commit `40edccac415693c5130f91c01d84176ae6008566`.
|
||||
- `make qemu-wasm` produces a smoke-tested ES module, WASM binary, and pthread
|
||||
worker under `dist/wasm/`. Next: static browser UI. Full X4 rendering waits.
|
||||
worker under `dist/wasm/`; the static browser UI boots firmware, renders X3,
|
||||
drives buttons, and exposes SD files/debug metrics. Full X4 rendering waits.
|
||||
|
||||
---
|
||||
|
||||
## 1. Environment / hardware realities (READ FIRST)
|
||||
|
||||
- **The dev box is ~15 yr old, 4 GB RAM.** A full QEMU build is ~1660 objects
|
||||
and takes a long time. **Always `-j2`** (more OOMs the linker) and use
|
||||
**very long timeouts (I used 7200 s)** for any `ninja`/`pio`/`nix build`.
|
||||
- A full QEMU build is large and can take several minutes. Build parallelism
|
||||
defaults to `nproc`; set `JOBS` lower if memory pressure requires it and use
|
||||
long timeouts for any `ninja`/`pio`/`nix build`.
|
||||
Do not treat a long-running build as hung.
|
||||
- **First full QEMU build ≈ many minutes; incremental after a patch edit is
|
||||
seconds** (only the touched `.c` + relink). Iterate incrementally; never
|
||||
@@ -174,8 +175,8 @@ Base addresses from ESP-IDF `soc/esp32c3` headers; all confirmed live.
|
||||
- creates the selected panel on `spi2.bus`, wires GPIO out 21→CS, 4→DC, 5→RST,
|
||||
and panel BUSY→GPIO in 6 (X3 UC8253 renders; X4 SSD1677 is a blank stub);
|
||||
- creates `ssi-sd` at CS index 1 on `spi2.bus`, wires GPIO out 12→its CS, and a
|
||||
`sd-card-spi` backed by `drive_get(IF_SD, 0, 0)` (QEMU block layer — the SD
|
||||
"interface" you asked for; local `-drive` now, browser blockdev later);
|
||||
`sd-card-spi` backed by `drive_get(IF_SD, 0, 0)`. Native uses a raw image;
|
||||
WASM uses QEMU `vvfat` over the JS-created MEMFS `/sdcard` directory;
|
||||
- **defaults both CS lines high** at init (`qemu_set_irq(cs, 1)`) so nothing is
|
||||
selected before the firmware drives GPIO.
|
||||
|
||||
@@ -283,21 +284,26 @@ ADC ladders on GPIO1 & GPIO2; power button GPIO3; UC8253 @ 16 MHz.
|
||||
- The narrow browser bridge exports panel surface metadata/pixels and atomic
|
||||
ADC/GPIO setters. JS polls a frame generation counter and forces alpha to 255;
|
||||
X3's native surface stores RGB with a zero alpha byte.
|
||||
- Firmware and ROM are written to MEMFS before QEMU starts. A second boot needs
|
||||
a page reload because QEMU machine selection and global state are process-wide.
|
||||
- Firmware and ROM are written to MEMFS before QEMU starts. Browser SD uses
|
||||
QEMU `vvfat` to expose `/sdcard` as a writable 32 GiB FAT32 volume; memory
|
||||
scales with actual directory files plus about 5 MiB of FAT/write bookkeeping,
|
||||
not logical capacity. The initial JS provider creates `Test/example.txt`.
|
||||
- The inspector has Logs, Files, and Debug tabs. Debug reports committed WASM
|
||||
heap (176.3 MiB after a 90-second validation run), visible MEMFS bytes, SD file
|
||||
bytes, and browser JS heap when supported.
|
||||
- A second boot needs a page reload because QEMU machine selection and global
|
||||
state are process-wide.
|
||||
|
||||
Next:
|
||||
|
||||
0. **Boot stall**: firmware freezes after X3 panel init (display generation stays
|
||||
at 2). Compare against native `make run` (which reaches the home screen);
|
||||
suspect timers/interrupts or just slow TCG. `_scratch/dump-fb3.mjs` reproduces.
|
||||
1. **Browser SD storage**: add an uploaded or OPFS-backed SD block device.
|
||||
1. **Browser SD editing/persistence**: add file controls and optionally OPFS.
|
||||
Live host-side changes to a mounted FAT volume are intentionally accepted as
|
||||
unsafe; editing is not implemented yet.
|
||||
|
||||
> Patch workflow: `scripts/build-qemu-wasm.sh` only re-applies
|
||||
> `qemu/patches/0002-qemu-wasm.patch` when the WASM source tree lacks the xteink
|
||||
> files. After editing the patch, `rm -rf _scratch/qemu-wasm-src` so `make
|
||||
> qemu-wasm` re-clones and re-applies it. The patch is regenerated from the
|
||||
> `_scratch/qemu-fix-src` worktree via `git diff --binary 610f8c69..HEAD`.
|
||||
> Patch workflow: `scripts/build-qemu-wasm.sh` resets/reapplies patches when
|
||||
> either patch is newer than `.xteink-patch-stamp`. Regenerate patch 0002 from
|
||||
> `_scratch/qemu-fix-src` with `git diff --binary 610f8c69 >
|
||||
> qemu/patches/0002-qemu-wasm.patch`.
|
||||
2. **Full X4 rendering later**: SSD1677 is 800×480 controller / 480×800 portrait,
|
||||
BUSY active-high. Implement its RAM/window/update commands when needed.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ QEMU_WASM_SRC ?= _scratch/qemu-wasm-src
|
||||
WASM_OUT ?= dist/wasm
|
||||
VARIANT ?= x3
|
||||
|
||||
.PHONY: firmware sdimage blank-sd-asset qemu qemu-wasm web web-test run run-upstream chip clean
|
||||
.PHONY: firmware sdimage qemu qemu-wasm web web-test run run-upstream chip clean
|
||||
|
||||
firmware:
|
||||
esptool --chip esp32c3 merge-bin -o flash.bin \
|
||||
@@ -23,12 +23,6 @@ firmware:
|
||||
sdimage:
|
||||
scripts/mksd.sh $(SD_SRC) sd.img
|
||||
|
||||
blank-sd-asset:
|
||||
@tmpdir=$$(mktemp -d); trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
mkdir "$$tmpdir/empty"; \
|
||||
SIZE_MB=64 scripts/mksd.sh "$$tmpdir/empty" "$$tmpdir/sd.img"; \
|
||||
gzip -n -9 -c "$$tmpdir/sd.img" > web/assets/blank-sd.img.gz
|
||||
|
||||
qemu:
|
||||
QEMU_SRC=$(QEMU_SRC) scripts/build-qemu.sh
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ buttons. See [ROADMAP.md](./ROADMAP.md) for architecture and plan.
|
||||
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader ../crosspoint-reader
|
||||
# Build ../crosspoint-reader's PlatformIO `default` environment first.
|
||||
nix develop
|
||||
make qemu # clone espressif/qemu @ pinned commit, apply patch, build (JOBS=2)
|
||||
make qemu # clone espressif/qemu @ pinned commit, apply patch, build
|
||||
make firmware sdimage # 16 MB flash.bin + FAT32 sd.img from the PlatformIO build
|
||||
make run # X3 (default)
|
||||
make run VARIANT=x4 # X4 detection + blank SSD1677 stub
|
||||
@@ -54,11 +54,12 @@ map to the device's ADC button ladders and active-low Power GPIO. `make web`
|
||||
serves the required COOP/COEP headers for Emscripten pthreads; a generic static
|
||||
server without those headers will not work.
|
||||
|
||||
Real firmware now boots in WASM: it runs past the earlier allocator and
|
||||
function-pointer signature crashes and initializes the X3 panel (528×792). It
|
||||
still stalls before painting a full home screen (the display generation counter
|
||||
freezes after early init), so end-to-end boot is not complete. X4 remains a
|
||||
white display stub, and browser SD storage is not implemented yet.
|
||||
Real firmware boots in WASM and drives the X3 panel (528×792). The browser SD
|
||||
uses QEMU's directory-backed FAT provider: the guest sees a writable, ephemeral
|
||||
32 GiB FAT32 card while memory use scales with actual files instead of logical
|
||||
capacity. It starts with `Test/example.txt`; inspect it in the Files tab. The
|
||||
Debug tab reports committed WASM heap, visible MEMFS data, SD file bytes, and
|
||||
browser JS heap where supported. X4 remains a white display stub.
|
||||
|
||||
## Contents
|
||||
|
||||
|
||||
+5
-6
@@ -38,9 +38,8 @@ X4 SPI traffic confirmed the firmware selected its SSD1677 driver.
|
||||
`make qemu-wasm` produces the Emscripten module, WASM binary, and pthread worker
|
||||
under `dist/wasm/`; Node smoke checks confirm `xteink` is registered and its
|
||||
browser framebuffer/input bridge initializes. `make web` serves a dependency-free
|
||||
UI for firmware, canvas, and buttons. Real firmware now boots past the allocator
|
||||
and function-pointer crashes and initializes the X3 panel, but still stalls
|
||||
before painting a full home screen.
|
||||
UI for firmware, canvas, buttons, logs, SD files, and debug metrics. Real
|
||||
firmware boots in WASM and drives the X3 panel.
|
||||
|
||||
### Device selection (X3 vs X4)
|
||||
|
||||
@@ -59,7 +58,7 @@ reuses unchanged — no bespoke abstraction layer:
|
||||
| Peripheral | Native seam | Browser binding |
|
||||
|------------|-------------|---------------------------|
|
||||
| e-ink panel | X3 UC8253 (528×792) or blank X4 SSD1677 stub (480×800) → QEMU graphical console | `<canvas>` from the same display surface |
|
||||
| SD card | `ssi-sd` + `sd-card-spi` backed by QEMU block layer (`-drive if=sd`) | browser-supplied blockdev (File/OPFS) |
|
||||
| SD card | `ssi-sd` + `sd-card-spi` backed by QEMU block layer (`-drive if=sd`) | QEMU `vvfat` over a JS-created MEMFS directory; future persistence can use OPFS |
|
||||
| Buttons + battery | ADC QOM props `adci[1]`/`adci[2]`; GPIO QOM `input-level[3]` for Power | DOM buttons → `qom-set` equivalent |
|
||||
| Firmware image | flash via `-drive if=mtd` | uploaded `.bin` written to emulated flash |
|
||||
|
||||
@@ -86,8 +85,8 @@ reuses unchanged — no bespoke abstraction layer:
|
||||
|
||||
## Remaining phases
|
||||
|
||||
1. **Browser SD storage** — add an uploaded or OPFS-backed SD block device. The
|
||||
basic static frontend already covers firmware, canvas, buttons, and X3/X4.
|
||||
1. **Browser SD editing/persistence** — add file controls and optionally OPFS;
|
||||
the current 32 GiB logical card is writable and ephemeral.
|
||||
2. **Full X4 panel** — implement SSD1677 RAM/window/update commands when visible
|
||||
X4 rendering is needed; the selection, dimensions, wiring, and stub exist.
|
||||
|
||||
|
||||
@@ -224,6 +224,30 @@ index 49814ec4af..0715610d37 100644
|
||||
assert(tcg_enabled());
|
||||
g_assert(!icount_enabled());
|
||||
|
||||
diff --git c/accel/tcg/tcg-accel-ops-rr.c w/accel/tcg/tcg-accel-ops-rr.c
|
||||
index 8ebadf8e9e..1e38df86e8 100644
|
||||
--- c/accel/tcg/tcg-accel-ops-rr.c
|
||||
+++ w/accel/tcg/tcg-accel-ops-rr.c
|
||||
@@ -177,11 +177,19 @@ static int rr_cpu_count(void)
|
||||
* elsewhere.
|
||||
*/
|
||||
|
||||
+#if defined(EMSCRIPTEN) && !defined(CONFIG_TCG_INTERPRETER)
|
||||
+#include "../../tcg/wasm32.h"
|
||||
+#endif
|
||||
+
|
||||
static void *rr_cpu_thread_fn(void *arg)
|
||||
{
|
||||
Notifier force_rcu;
|
||||
CPUState *cpu = arg;
|
||||
|
||||
+#if defined(EMSCRIPTEN) && !defined(CONFIG_TCG_INTERPRETER)
|
||||
+ init_wasm32();
|
||||
+#endif
|
||||
+
|
||||
assert(tcg_enabled());
|
||||
rcu_register_thread();
|
||||
force_rcu.notify = rr_force_rcu;
|
||||
diff --git c/backends/meson.build w/backends/meson.build
|
||||
index da714b93d1..9b88d22685 100644
|
||||
--- c/backends/meson.build
|
||||
@@ -278,6 +302,223 @@ index 90fa54352c..0fa9cc31b6 100644
|
||||
|
||||
/*
|
||||
* parse_zone - Fill a zone descriptor
|
||||
diff --git c/block/vvfat.c w/block/vvfat.c
|
||||
index 8ffe8b3b9b..4cf02d9a55 100644
|
||||
--- c/block/vvfat.c
|
||||
+++ w/block/vvfat.c
|
||||
@@ -749,6 +749,7 @@ static int read_directory(BDRVVVFATState* s, int mapping_index)
|
||||
const char* dirname = mapping->path;
|
||||
int first_cluster = mapping->begin;
|
||||
int parent_index = mapping->info.dir.parent_mapping_index;
|
||||
+ bool is_root = parent_index < 0;
|
||||
mapping_t* parent_mapping = (mapping_t*)
|
||||
(parent_index >= 0 ? array_get(&(s->mapping), parent_index) : NULL);
|
||||
int first_cluster_of_parent = parent_mapping ? parent_mapping->begin : -1;
|
||||
@@ -765,9 +766,9 @@ static int read_directory(BDRVVVFATState* s, int mapping_index)
|
||||
}
|
||||
|
||||
i = mapping->info.dir.first_dir_index =
|
||||
- first_cluster == 0 ? 0 : s->directory.next;
|
||||
+ is_root ? 0 : s->directory.next;
|
||||
|
||||
- if (first_cluster != 0) {
|
||||
+ if (!is_root) {
|
||||
/* create the top entries of a subdirectory */
|
||||
(void)create_short_and_long_name(s, i, ".", 1);
|
||||
(void)create_short_and_long_name(s, i, "..", 1);
|
||||
@@ -781,13 +782,14 @@ static int read_directory(BDRVVVFATState* s, int mapping_index)
|
||||
int is_dot=!strcmp(entry->d_name,".");
|
||||
int is_dotdot=!strcmp(entry->d_name,"..");
|
||||
|
||||
- if (first_cluster == 0 && s->directory.next >= s->root_entries - 1) {
|
||||
+ if (is_root && s->fat_type != 32 &&
|
||||
+ s->directory.next >= s->root_entries - 1) {
|
||||
fprintf(stderr, "Too many entries in root directory\n");
|
||||
closedir(dir);
|
||||
return -2;
|
||||
}
|
||||
|
||||
- if(first_cluster == 0 && (is_dotdot || is_dot))
|
||||
+ if(is_root && (is_dotdot || is_dot))
|
||||
continue;
|
||||
|
||||
buffer = g_malloc(length);
|
||||
@@ -860,8 +862,7 @@ static int read_directory(BDRVVVFATState* s, int mapping_index)
|
||||
memset(direntry,0,sizeof(direntry_t));
|
||||
}
|
||||
|
||||
- if (s->fat_type != 32 &&
|
||||
- mapping_index == 0 &&
|
||||
+ if (s->fat_type != 32 && is_root &&
|
||||
s->directory.next < s->root_entries) {
|
||||
/* root directory */
|
||||
int cur = s->directory.next;
|
||||
@@ -877,20 +878,24 @@ static int read_directory(BDRVVVFATState* s, int mapping_index)
|
||||
* 0x20 / s->cluster_size;
|
||||
mapping->end = first_cluster;
|
||||
|
||||
- direntry = array_get(&(s->directory), mapping->dir_index);
|
||||
- set_begin_of_direntry(direntry, mapping->begin);
|
||||
+ if (!is_root) {
|
||||
+ direntry = array_get(&(s->directory), mapping->dir_index);
|
||||
+ set_begin_of_direntry(direntry, mapping->begin);
|
||||
+ }
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int32_t sector2cluster(BDRVVVFATState* s,off_t sector_num)
|
||||
{
|
||||
- return (sector_num - s->offset_to_root_dir) / s->sectors_per_cluster;
|
||||
+ return (sector_num - s->offset_to_root_dir) / s->sectors_per_cluster
|
||||
+ + (s->fat_type == 32 ? 2 : 0);
|
||||
}
|
||||
|
||||
static inline off_t cluster2sector(BDRVVVFATState* s, uint32_t cluster_num)
|
||||
{
|
||||
- return s->offset_to_root_dir + s->sectors_per_cluster * cluster_num;
|
||||
+ return s->offset_to_root_dir + s->sectors_per_cluster
|
||||
+ * (cluster_num - (s->fat_type == 32 ? 2 : 0));
|
||||
}
|
||||
|
||||
static int init_directories(BDRVVVFATState* s,
|
||||
@@ -934,11 +939,12 @@ static int init_directories(BDRVVVFATState* s,
|
||||
init_fat(s);
|
||||
|
||||
/* TODO: if there are more entries, bootsector has to be adjusted! */
|
||||
- s->root_entries = 0x02 * 0x10 * s->sectors_per_cluster;
|
||||
+ s->root_entries = s->fat_type == 32 ? 0 :
|
||||
+ 0x02 * 0x10 * s->sectors_per_cluster;
|
||||
s->cluster_count=sector2cluster(s, s->sector_count);
|
||||
|
||||
mapping = array_get_next(&(s->mapping));
|
||||
- mapping->begin = 0;
|
||||
+ mapping->begin = s->fat_type == 32 ? 2 : 0;
|
||||
mapping->dir_index = 0;
|
||||
mapping->info.dir.parent_mapping_index = -1;
|
||||
mapping->first_mapping_index = -1;
|
||||
@@ -950,11 +956,10 @@ static int init_directories(BDRVVVFATState* s,
|
||||
mapping->read_only = 0;
|
||||
s->path = mapping->path;
|
||||
|
||||
- for (i = 0, cluster = 0; i < s->mapping.next; i++) {
|
||||
- /* MS-DOS expects the FAT to be 0 for the root directory
|
||||
- * (except for the media byte). */
|
||||
- /* LATER TODO: still true for FAT32? */
|
||||
- int fix_fat = (i != 0);
|
||||
+ for (i = 0, cluster = s->fat_type == 32 ? 2 : 0;
|
||||
+ i < s->mapping.next; i++) {
|
||||
+ /* FAT12/16 stores the root directory outside the cluster chain. */
|
||||
+ int fix_fat = (i != 0 || s->fat_type == 32);
|
||||
mapping = array_get(&(s->mapping), i);
|
||||
|
||||
if (mapping->mode & MODE_DIRECTORY) {
|
||||
@@ -1026,22 +1031,35 @@ static int init_directories(BDRVVVFATState* s,
|
||||
/* media descriptor: hard disk=0xf8, floppy=0xf0 */
|
||||
bootsector->media_type = (s->offset_to_bootsector > 0 ? 0xf8 : 0xf0);
|
||||
s->fat.pointer[0] = bootsector->media_type;
|
||||
- bootsector->sectors_per_fat=cpu_to_le16(s->sectors_per_fat);
|
||||
+ bootsector->sectors_per_fat = s->fat_type == 32 ? 0 :
|
||||
+ cpu_to_le16(s->sectors_per_fat);
|
||||
bootsector->sectors_per_track = cpu_to_le16(secs);
|
||||
bootsector->number_of_heads = cpu_to_le16(heads);
|
||||
bootsector->hidden_sectors = cpu_to_le32(s->offset_to_bootsector);
|
||||
bootsector->total_sectors=cpu_to_le32(s->sector_count>0xffff?s->sector_count:0);
|
||||
|
||||
- /* LATER TODO: if FAT32, this is wrong */
|
||||
- /* drive_number: fda=0, hda=0x80 */
|
||||
- bootsector->u.fat16.drive_number = s->offset_to_bootsector == 0 ? 0 : 0x80;
|
||||
- bootsector->u.fat16.signature=0x29;
|
||||
- bootsector->u.fat16.id=cpu_to_le32(0xfabe1afd);
|
||||
-
|
||||
- memcpy(bootsector->u.fat16.volume_label, s->volume_label,
|
||||
- sizeof(bootsector->u.fat16.volume_label));
|
||||
- memcpy(bootsector->u.fat16.fat_type,
|
||||
- s->fat_type == 12 ? "FAT12 " : "FAT16 ", 8);
|
||||
+ if (s->fat_type == 32) {
|
||||
+ bootsector->u.fat32.sectors_per_fat = cpu_to_le32(s->sectors_per_fat);
|
||||
+ bootsector->u.fat32.first_cluster_of_root_dir = cpu_to_le32(2);
|
||||
+ bootsector->u.fat32.info_sector = cpu_to_le16(0xffff);
|
||||
+ bootsector->u.fat32.backup_boot_sector = cpu_to_le16(0xffff);
|
||||
+ bootsector->u.fat32.drive_number = 0x80;
|
||||
+ bootsector->u.fat32.signature = 0x29;
|
||||
+ bootsector->u.fat32.id = cpu_to_le32(0xfabe1afd);
|
||||
+ memcpy(bootsector->u.fat32.volume_label, s->volume_label,
|
||||
+ sizeof(bootsector->u.fat32.volume_label));
|
||||
+ memcpy(bootsector->u.fat32.fat_type, "FAT32 ", 8);
|
||||
+ } else {
|
||||
+ /* drive_number: fda=0, hda=0x80 */
|
||||
+ bootsector->u.fat16.drive_number =
|
||||
+ s->offset_to_bootsector == 0 ? 0 : 0x80;
|
||||
+ bootsector->u.fat16.signature=0x29;
|
||||
+ bootsector->u.fat16.id=cpu_to_le32(0xfabe1afd);
|
||||
+ memcpy(bootsector->u.fat16.volume_label, s->volume_label,
|
||||
+ sizeof(bootsector->u.fat16.volume_label));
|
||||
+ memcpy(bootsector->u.fat16.fat_type,
|
||||
+ s->fat_type == 12 ? "FAT12 " : "FAT16 ", 8);
|
||||
+ }
|
||||
bootsector->magic[0]=0x55; bootsector->magic[1]=0xaa;
|
||||
|
||||
return 0;
|
||||
@@ -1139,6 +1157,7 @@ static int vvfat_open(BlockDriverState *bs, QDict *options, int flags,
|
||||
{
|
||||
BDRVVVFATState *s = bs->opaque;
|
||||
int cyls, heads, secs;
|
||||
+ uint64_t total_sectors;
|
||||
bool floppy;
|
||||
const char *dirname, *label;
|
||||
QemuOpts *opts;
|
||||
@@ -1205,7 +1224,9 @@ static int vvfat_open(BlockDriverState *bs, QDict *options, int flags,
|
||||
|
||||
switch (s->fat_type) {
|
||||
case 32:
|
||||
+#ifndef EMSCRIPTEN
|
||||
warn_report("FAT32 has not been tested. You are welcome to do so!");
|
||||
+#endif
|
||||
break;
|
||||
case 16:
|
||||
case 12:
|
||||
@@ -1219,8 +1240,17 @@ static int vvfat_open(BlockDriverState *bs, QDict *options, int flags,
|
||||
|
||||
s->bs = bs;
|
||||
|
||||
- /* LATER TODO: if FAT32, adjust */
|
||||
- s->sectors_per_cluster=0x10;
|
||||
+#ifdef EMSCRIPTEN
|
||||
+ if (!floppy && s->fat_type == 32) {
|
||||
+ /* Browser SD Geometry - A larger cluster keeps the synthesized 32 GiB FAT near 4 MiB. */
|
||||
+ s->sectors_per_cluster = 0x40;
|
||||
+ total_sectors = 32ULL * 1024 * 1024 * 1024 / BDRV_SECTOR_SIZE;
|
||||
+ } else
|
||||
+#endif
|
||||
+ {
|
||||
+ s->sectors_per_cluster = 0x10;
|
||||
+ total_sectors = (uint64_t)cyls * heads * secs;
|
||||
+ }
|
||||
|
||||
s->current_cluster=0xffffffff;
|
||||
|
||||
@@ -1232,8 +1262,8 @@ static int vvfat_open(BlockDriverState *bs, QDict *options, int flags,
|
||||
DLOG(fprintf(stderr, "vvfat %s chs %d,%d,%d\n",
|
||||
dirname, cyls, heads, secs));
|
||||
|
||||
- s->sector_count = cyls * heads * secs - s->offset_to_bootsector;
|
||||
- bs->total_sectors = cyls * heads * secs;
|
||||
+ s->sector_count = total_sectors - s->offset_to_bootsector;
|
||||
+ bs->total_sectors = total_sectors;
|
||||
|
||||
if (qemu_opt_get_bool(opts, "rw", false)) {
|
||||
if (!bdrv_is_read_only(bs)) {
|
||||
@@ -1523,7 +1553,8 @@ vvfat_read(BlockDriverState *bs, int64_t sector_num, uint8_t *buf, int nb_sector
|
||||
} else {
|
||||
uint32_t sector = sector_num - s->offset_to_root_dir,
|
||||
sector_offset_in_cluster=(sector%s->sectors_per_cluster),
|
||||
- cluster_num=sector/s->sectors_per_cluster;
|
||||
+ cluster_num=sector/s->sectors_per_cluster
|
||||
+ + (s->fat_type == 32 ? 2 : 0);
|
||||
if(cluster_num > s->cluster_count || read_cluster(s, cluster_num) != 0) {
|
||||
/* LATER TODO: strict: return -1; */
|
||||
memset(buf+i*0x200,0,0x200);
|
||||
diff --git c/configs/devices/riscv32-softmmu/xteink.mak w/configs/devices/riscv32-softmmu/xteink.mak
|
||||
new file mode 100644
|
||||
index 0000000000..a9179b4119
|
||||
@@ -613,6 +854,39 @@ index 1879b5c2df..e3f09cc51b 100644
|
||||
}
|
||||
|
||||
static void esp32c3_cache_init(Object *obj)
|
||||
diff --git c/hw/misc/esp32c3_jtag.c w/hw/misc/esp32c3_jtag.c
|
||||
index 82ee69717b..64a971f183 100644
|
||||
--- c/hw/misc/esp32c3_jtag.c
|
||||
+++ w/hw/misc/esp32c3_jtag.c
|
||||
@@ -16,10 +16,18 @@
|
||||
#include "hw/misc/esp32c3_jtag.h"
|
||||
|
||||
|
||||
+/* USB Serial JTAG Console Tap - The firmware routes ESP-IDF console output (incl. panic dumps) through this peripheral. EP1 is the byte FIFO; EP1_CONF bit1 (SERIAL_IN_EP_DATA_FREE) must read set so the driver believes the TX FIFO can always accept a byte. */
|
||||
+#define ESP32C3_JTAG_EP1_REG 0x00
|
||||
+#define ESP32C3_JTAG_EP1_CONF_REG 0x04
|
||||
+#define ESP32C3_JTAG_IN_EP_DATA_FREE (1u << 1)
|
||||
+
|
||||
static uint64_t esp32c3_jtag_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
ESP32C3UsbJtagState *s = ESP32C3_JTAG(opaque);
|
||||
(void) s;
|
||||
+ if (addr == ESP32C3_JTAG_EP1_CONF_REG) {
|
||||
+ return ESP32C3_JTAG_IN_EP_DATA_FREE;
|
||||
+ }
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -27,6 +35,9 @@ static void esp32c3_jtag_write(void *opaque, hwaddr addr, uint64_t value, unsign
|
||||
{
|
||||
ESP32C3UsbJtagState *s = ESP32C3_JTAG(opaque);
|
||||
(void) s;
|
||||
+ if (addr == ESP32C3_JTAG_EP1_REG) {
|
||||
+ fputc((int)(value & 0xff), stderr);
|
||||
+ }
|
||||
}
|
||||
|
||||
static const MemoryRegionOps esp32c3_jtag_ops = {
|
||||
diff --git c/hw/misc/esp_sha.c w/hw/misc/esp_sha.c
|
||||
index c9fa2e610a..cc186595eb 100644
|
||||
--- c/hw/misc/esp_sha.c
|
||||
@@ -707,7 +981,7 @@ index c9fa2e610a..cc186595eb 100644
|
||||
},
|
||||
};
|
||||
diff --git c/hw/riscv/esp32c3.c w/hw/riscv/esp32c3.c
|
||||
index 2f1ccbfbb0..febc96bfe3 100644
|
||||
index 2f1ccbfbb0..af8bb86c97 100644
|
||||
--- c/hw/riscv/esp32c3.c
|
||||
+++ w/hw/riscv/esp32c3.c
|
||||
@@ -57,6 +57,10 @@
|
||||
@@ -721,7 +995,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
#define ESP32C3_IO_WARNING 0
|
||||
|
||||
#define ESP32C3_RESET_ADDRESS 0x40000000
|
||||
@@ -103,6 +107,27 @@ struct Esp32C3MachineState {
|
||||
@@ -103,6 +107,29 @@ struct Esp32C3MachineState {
|
||||
Esp32C3TWAIState twai;
|
||||
};
|
||||
|
||||
@@ -732,6 +1006,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
+{
|
||||
+ if (xteink_wasm_machine && channel >= 0 &&
|
||||
+ channel < ESP32C3_ADC_CHANNELS) {
|
||||
+ fprintf(stderr, "[button] adc channel=%d value=%u\n", channel, value);
|
||||
+ qatomic_set(&xteink_wasm_machine->adc.input[channel],
|
||||
+ MIN(value, UINT32_C(0xfff)));
|
||||
+ }
|
||||
@@ -740,6 +1015,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
+EMSCRIPTEN_KEEPALIVE void xteink_wasm_set_gpio(int pin, int level)
|
||||
+{
|
||||
+ if (xteink_wasm_machine && pin >= 0 && pin < ESP32_GPIO_COUNT) {
|
||||
+ fprintf(stderr, "[button] gpio pin=%d level=%d\n", pin, level);
|
||||
+ esp32_gpio_set_input_level(&xteink_wasm_machine->gpio.parent,
|
||||
+ pin, level);
|
||||
+ }
|
||||
@@ -749,7 +1025,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
/* Fake register used by ESP-IDF application to determine whether the code is running on real hardware or on QEMU */
|
||||
#define A_SYSCON_ORIGIN_REG 0x3F8
|
||||
/* Temporary macro for generating a random value from register SYSCON_RND_DATA_REG */
|
||||
@@ -421,12 +446,18 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
@@ -421,12 +448,18 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
object_initialize_child(OBJECT(machine), "efuse", &ms->efuse, TYPE_ESP32C3_EFUSE);
|
||||
object_initialize_child(OBJECT(machine), "clock", &ms->clock, TYPE_ESP32C3_CLOCK);
|
||||
object_initialize_child(OBJECT(machine), "sha", &ms->sha, TYPE_ESP32C3_SHA);
|
||||
@@ -768,7 +1044,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
object_initialize_child(OBJECT(machine), "timg0", &ms->timg[0], TYPE_ESP32C3_TIMG);
|
||||
object_initialize_child(OBJECT(machine), "timg1", &ms->timg[1], TYPE_ESP32C3_TIMG);
|
||||
object_initialize_child(OBJECT(machine), "systimer", &ms->systimer, TYPE_ESP32C3_SYSTIMER);
|
||||
@@ -484,7 +515,9 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
@@ -484,7 +517,9 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
|
||||
/* SPI1 controller (SPI Flash) */
|
||||
{
|
||||
@@ -778,7 +1054,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
sysbus_realize(SYS_BUS_DEVICE(&ms->spi1), &error_fatal);
|
||||
MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->spi1), 0);
|
||||
memory_region_add_subregion_overlap(sys_mem, DR_REG_SPI1_BASE, mr, 0);
|
||||
@@ -539,7 +572,9 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
@@ -539,7 +574,9 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
if (blk) {
|
||||
ms->cache.flash_blk = blk;
|
||||
}
|
||||
@@ -788,7 +1064,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
sysbus_realize(SYS_BUS_DEVICE(&ms->cache), &error_fatal);
|
||||
MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->cache), 0);
|
||||
memory_region_add_subregion_overlap(sys_mem, DR_REG_EXTMEM_BASE, mr, 0);
|
||||
@@ -635,6 +670,7 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
@@ -635,6 +672,7 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
qdev_get_gpio_in(intmatrix_dev, ETS_SHA_INTR_SOURCE));
|
||||
}
|
||||
|
||||
@@ -796,7 +1072,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
/* AES realization */
|
||||
{
|
||||
ms->aes.parent.gdma = ESP_GDMA(&ms->gdma);
|
||||
@@ -654,6 +690,8 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
@@ -654,6 +692,8 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
qdev_get_gpio_in(intmatrix_dev, ETS_RSA_INTR_SOURCE));
|
||||
}
|
||||
|
||||
@@ -805,7 +1081,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
/* HMAC realization */
|
||||
{
|
||||
ms->hmac.parent.efuse = ESP_EFUSE(&ms->efuse);
|
||||
@@ -662,6 +700,7 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
@@ -662,6 +702,7 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
memory_region_add_subregion_overlap(sys_mem, DR_REG_HMAC_BASE, mr, 0);
|
||||
}
|
||||
|
||||
@@ -813,7 +1089,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
/* Digital Signature realization */
|
||||
{
|
||||
ms->ds.parent.hmac = ESP_HMAC(&ms->hmac);
|
||||
@@ -682,6 +721,8 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
@@ -682,6 +723,8 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
memory_region_add_subregion_overlap(sys_mem, DR_REG_AES_XTS_BASE, mr, 0);
|
||||
}
|
||||
|
||||
@@ -822,7 +1098,7 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
/* RGB display realization */
|
||||
if (!ms->xteink) {
|
||||
/* Give the internal RAM memory region to the display */
|
||||
@@ -706,6 +747,9 @@ static void xteink_machine_init(MachineState *machine)
|
||||
@@ -706,6 +749,9 @@ static void xteink_machine_init(MachineState *machine)
|
||||
Esp32C3MachineState *ms = ESP32C3_MACHINE(machine);
|
||||
ms->xteink = true;
|
||||
esp32c3_machine_init(machine);
|
||||
@@ -832,6 +1108,33 @@ index 2f1ccbfbb0..febc96bfe3 100644
|
||||
|
||||
/* X3 exposes the fingerprint chips; X4 omits them so stock firmware's
|
||||
* all-NAK probe selects X4. The X4 panel is a blank protocol stub. */
|
||||
diff --git c/hw/riscv/esp32c3_intmatrix.c w/hw/riscv/esp32c3_intmatrix.c
|
||||
index c1376b7299..1275808dc0 100644
|
||||
--- c/hw/riscv/esp32c3_intmatrix.c
|
||||
+++ w/hw/riscv/esp32c3_intmatrix.c
|
||||
@@ -25,9 +25,10 @@
|
||||
#define INTMATRIX_DEBUG 0
|
||||
#define INTMATRIX_WARNING 0
|
||||
|
||||
-#define BIT_SET(reg, bit) ((reg) & BIT(bit))
|
||||
-#define CLEAR_BIT(reg, bit) do { (reg) &= ~BIT(bit); } while(0)
|
||||
-#define SET_BIT(reg, bit) do { (reg) |= BIT(bit); } while(0)
|
||||
+/* 64-bit Shifts For irq_levels - There are up to 62 interrupt sources, so bit ops on the uint64_t level mask must shift in 64-bit. QEMU's BIT() is `1UL << nr`, which is only 32-bit on wasm32 and silently aliases source N to N-32. */
|
||||
+#define BIT_SET(reg, bit) ((reg) & (1ULL << (bit)))
|
||||
+#define CLEAR_BIT(reg, bit) do { (reg) &= ~(1ULL << (bit)); } while(0)
|
||||
+#define SET_BIT(reg, bit) do { (reg) |= (1ULL << (bit)); } while(0)
|
||||
|
||||
|
||||
static int esp32c3_get_output_line_level(ESP32C3IntMatrixState *s, int line)
|
||||
@@ -36,7 +37,7 @@ static int esp32c3_get_output_line_level(ESP32C3IntMatrixState *s, int line)
|
||||
|
||||
for (int i = 0; level_shared == 0 && i < ESP32C3_INT_MATRIX_INPUTS; i++) {
|
||||
const uint_fast8_t mapped = s->irq_map[i];
|
||||
- if (mapped == line && (s->irq_levels & BIT(i)))
|
||||
+ if (mapped == line && (s->irq_levels & (1ULL << i)))
|
||||
{
|
||||
level_shared |= 1;
|
||||
}
|
||||
diff --git c/include/exec/exec-all.h w/include/exec/exec-all.h
|
||||
index 2e4c4cc4b4..99e6da08c9 100644
|
||||
--- c/include/exec/exec-all.h
|
||||
|
||||
@@ -47,7 +47,7 @@ printf 'Building Emscripten environment (qemu-wasm %s)\n' "$QEMU_WASM_COMMIT"
|
||||
|
||||
FLAGS="-O3 -Wno-error=unused-command-line-argument -matomics -mbulk-memory -DNDEBUG -DG_DISABLE_ASSERT -D_GNU_SOURCE -sASYNCIFY=1 -pthread -sPROXY_TO_PTHREAD=1 -sFORCE_FILESYSTEM -sALLOW_TABLE_GROWTH -sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=1GB -sSTACK_SIZE=1048576 -sWASM_BIGINT -sMALLOC=mimalloc -sEXPORT_ES6=1 -sASYNCIFY_IMPORTS=ffi_call_js"
|
||||
|
||||
if [ ! -f build/build.ninja ]; then
|
||||
if [ ! -f build/build.ninja ] || ! grep -q 'block/vvfat.c' build/build.ninja; then
|
||||
"$ENGINE" run --rm -v "$SRC:/qemu" "$IMAGE" bash -lc "
|
||||
cd /qemu
|
||||
emconfigure ./configure \\
|
||||
@@ -59,6 +59,8 @@ if [ ! -f build/build.ninja ]; then
|
||||
--without-default-devices \\
|
||||
--with-devices-riscv32=xteink \\
|
||||
--enable-system \\
|
||||
--enable-qcow1 \\
|
||||
--enable-vvfat \\
|
||||
--with-coroutine=fiber \\
|
||||
--disable-werror \\
|
||||
--disable-docs \\
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { gunzipSync } from 'node:zlib';
|
||||
|
||||
const artifactRoot = process.argv[2];
|
||||
if (!artifactRoot) {
|
||||
@@ -10,22 +9,23 @@ if (!artifactRoot) {
|
||||
|
||||
const moduleUrl = pathToFileURL(`${artifactRoot}/qemu-system-riscv32.js`);
|
||||
const rom = new Uint8Array(await readFile(`${artifactRoot}/esp32c3-rom.bin`));
|
||||
const sdCard = gunzipSync(await readFile('web/assets/blank-sd.img.gz'));
|
||||
const { default: createQemu } = await import(moduleUrl);
|
||||
const options = {
|
||||
arguments: [
|
||||
'-S', '-machine', 'xteink,variant=x3', '-accel', 'tcg,tb-size=16', '-L', '/bios',
|
||||
'-display', 'none', '-serial', 'null', '-nic', 'none',
|
||||
'-drive', 'file=/flash.bin,if=mtd,format=raw',
|
||||
'-drive', 'file=/sd.img,if=sd,format=raw',
|
||||
'-drive', 'file=fat:32:rw:/sdcard,if=sd,format=raw',
|
||||
],
|
||||
mainScriptUrlOrBlob: moduleUrl.href,
|
||||
};
|
||||
options.preRun = [() => {
|
||||
options.FS.mkdir('/bios');
|
||||
options.FS.mkdirTree('/bios');
|
||||
options.FS.mkdirTree('/var/tmp');
|
||||
options.FS.mkdirTree('/sdcard/Test');
|
||||
options.FS.writeFile('/bios/esp32c3-rom.bin', rom);
|
||||
options.FS.writeFile('/flash.bin', new Uint8Array(16 * 1024 * 1024));
|
||||
options.FS.writeFile('/sd.img', sdCard);
|
||||
options.FS.writeFile('/sdcard/Test/example.txt', 'Lorem ipsum');
|
||||
}];
|
||||
|
||||
const module = await createQemu(options);
|
||||
@@ -35,6 +35,8 @@ const timer = setInterval(() => {
|
||||
clearInterval(timer);
|
||||
assert.equal(module._xteink_wasm_height(), 792);
|
||||
assert.ok(module._xteink_wasm_framebuffer());
|
||||
assert.ok(module.FS.readdir('/sdcard/Test').includes('example.txt'));
|
||||
assert.equal(module.FS.readFile('/sdcard/Test/example.txt', { encoding: 'utf8' }), 'Lorem ipsum');
|
||||
module._xteink_wasm_set_adc(1, 2694);
|
||||
module._xteink_wasm_set_gpio(3, 0);
|
||||
process.exit(0);
|
||||
|
||||
+5
-10
@@ -1,6 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { BUTTONS, setButton, mergeFirmware, loadEphemeralSdCard } from '../web/app.js';
|
||||
import { BUTTONS, DEFAULT_SD_FILES, EXAMPLE_SD_TEXT, formatBytes, setButton, mergeFirmware } from '../web/app.js';
|
||||
|
||||
// Merge - app image is placed at 0x10000 in a 0xFF-erased 16 MB flash with bootloader and partitions.
|
||||
const bootloader = new Uint8Array([1, 2, 3]);
|
||||
@@ -15,14 +14,10 @@ assert.equal(flash[0x10000], 0xe9);
|
||||
assert.equal(flash[0x20000], 0xff);
|
||||
assert.throws(() => mergeFirmware(new Uint8Array([0x00]), bootloader, partitions), /0xE9/);
|
||||
|
||||
const compressedSd = await readFile('web/assets/blank-sd.img.gz');
|
||||
const fetchSd = async () => new Response(compressedSd);
|
||||
const firstSd = await loadEphemeralSdCard(import.meta.url, fetchSd);
|
||||
const secondSd = await loadEphemeralSdCard(import.meta.url, fetchSd);
|
||||
assert.equal(firstSd.length, 64 * 1024 * 1024);
|
||||
assert.deepEqual(firstSd.slice(510, 512), new Uint8Array([0x55, 0xaa]));
|
||||
firstSd[0] ^= 0xff;
|
||||
assert.notEqual(firstSd[0], secondSd[0]);
|
||||
assert.equal(DEFAULT_SD_FILES.get('Test/example.txt'), EXAMPLE_SD_TEXT);
|
||||
assert.equal(EXAMPLE_SD_TEXT.split('Lorem ipsum').length - 1, 10);
|
||||
assert.equal(formatBytes(512), '512 B');
|
||||
assert.equal(formatBytes(64 * 1024 * 1024), '64.0 MiB');
|
||||
|
||||
const calls = [];
|
||||
const module = {
|
||||
|
||||
+161
-16
@@ -38,16 +38,18 @@ async function fetchBin(url) {
|
||||
return new Uint8Array(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
export async function loadEphemeralSdCard(assetRoot = import.meta.url, fetchFn = fetch) {
|
||||
const response = await fetchFn(new URL('assets/blank-sd.img.gz', assetRoot));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not load blank SD card (${response.status}).`);
|
||||
const LOREM_IPSUM = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
|
||||
export const EXAMPLE_SD_TEXT = Array(10).fill(LOREM_IPSUM).join('\n\n');
|
||||
export const DEFAULT_SD_FILES = new Map([['Test/example.txt', EXAMPLE_SD_TEXT]]);
|
||||
|
||||
function mountEphemeralSdCard(fs) {
|
||||
for (const [path, contents] of DEFAULT_SD_FILES) {
|
||||
const parts = path.split('/');
|
||||
const filename = parts.pop();
|
||||
const directory = `/sdcard/${parts.join('/')}`;
|
||||
fs.mkdirTree(directory);
|
||||
fs.writeFile(`${directory}/${filename}`, contents);
|
||||
}
|
||||
if (!response.body || typeof DecompressionStream === 'undefined') {
|
||||
throw new Error('This browser cannot decompress the blank SD card image.');
|
||||
}
|
||||
const stream = response.body.pipeThrough(new DecompressionStream('gzip'));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
|
||||
// A full 16 MB image is used as-is; anything else is treated as a CrossPoint app image and merged with the bundled bootloader and partition table.
|
||||
@@ -170,6 +172,145 @@ let persistLog = false;
|
||||
const logBuffer = [];
|
||||
const LOG_MAX_LINES = 1000;
|
||||
const DEBUG_LOG_KEY = 'xteink-debug-log';
|
||||
let activeModule = null;
|
||||
|
||||
export function formatBytes(bytes) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
const units = ['KiB', 'MiB', 'GiB'];
|
||||
let value = bytes;
|
||||
let unit = -1;
|
||||
do {
|
||||
value /= 1024;
|
||||
unit++;
|
||||
} while (value >= 1024 && unit < units.length - 1);
|
||||
return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[unit]}`;
|
||||
}
|
||||
|
||||
function sdEntries(module = activeModule) {
|
||||
if (!module) {
|
||||
return [
|
||||
{ path: 'Test/', directory: true },
|
||||
...[...DEFAULT_SD_FILES].map(([path, contents]) => ({
|
||||
path,
|
||||
size: new TextEncoder().encode(contents).length,
|
||||
read: () => contents,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
const entries = [];
|
||||
const visit = (directory, relative = '') => {
|
||||
for (const name of module.FS.readdir(directory).filter(name => name !== '.' && name !== '..').sort()) {
|
||||
const absolute = `${directory}/${name}`;
|
||||
const path = relative ? `${relative}/${name}` : name;
|
||||
const stat = module.FS.stat(absolute);
|
||||
if (module.FS.isDir(stat.mode)) {
|
||||
entries.push({ path: `${path}/`, directory: true });
|
||||
visit(absolute, path);
|
||||
} else {
|
||||
entries.push({
|
||||
path,
|
||||
size: stat.size,
|
||||
read: () => module.FS.readFile(absolute, { encoding: 'utf8' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
visit('/sdcard');
|
||||
return entries;
|
||||
}
|
||||
|
||||
function renderFiles() {
|
||||
const list = document.querySelector('#file-list');
|
||||
const preview = document.querySelector('#file-preview');
|
||||
const entries = sdEntries();
|
||||
list.replaceChildren();
|
||||
let firstFile = null;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.directory) {
|
||||
const directory = document.createElement('div');
|
||||
directory.className = 'file-directory';
|
||||
directory.textContent = entry.path;
|
||||
list.append(directory);
|
||||
continue;
|
||||
}
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
const path = document.createElement('span');
|
||||
path.textContent = entry.path;
|
||||
const size = document.createElement('small');
|
||||
size.textContent = formatBytes(entry.size);
|
||||
button.append(path, size);
|
||||
button.addEventListener('click', () => {
|
||||
try {
|
||||
preview.textContent = entry.read();
|
||||
} catch (error) {
|
||||
preview.textContent = `Could not read ${entry.path}: ${error.message || error}`;
|
||||
}
|
||||
});
|
||||
list.append(button);
|
||||
firstFile ||= button;
|
||||
}
|
||||
|
||||
if (firstFile) {
|
||||
firstFile.click();
|
||||
} else {
|
||||
preview.textContent = 'The SD card is empty.';
|
||||
}
|
||||
}
|
||||
|
||||
function pathBytes(fs, path) {
|
||||
try {
|
||||
const stat = fs.stat(path);
|
||||
if (!fs.isDir(stat.mode)) {
|
||||
return { bytes: stat.size, files: 1 };
|
||||
}
|
||||
return fs.readdir(path)
|
||||
.filter(name => name !== '.' && name !== '..')
|
||||
.map(name => pathBytes(fs, `${path}/${name}`))
|
||||
.reduce((total, item) => ({ bytes: total.bytes + item.bytes, files: total.files + item.files }), { bytes: 0, files: 0 });
|
||||
} catch {
|
||||
return { bytes: 0, files: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
function renderDebug() {
|
||||
const jsHeap = performance.memory?.usedJSHeapSize;
|
||||
document.querySelector('#debug-js-heap').textContent = jsHeap ? formatBytes(jsHeap) : 'Unavailable in this browser';
|
||||
if (!activeModule) {
|
||||
return;
|
||||
}
|
||||
const visible = ['/flash.bin', '/bios', '/sdcard', '/var/tmp']
|
||||
.map(path => pathBytes(activeModule.FS, path))
|
||||
.reduce((total, item) => ({ bytes: total.bytes + item.bytes, files: total.files + item.files }), { bytes: 0, files: 0 });
|
||||
const sd = pathBytes(activeModule.FS, '/sdcard');
|
||||
document.querySelector('#debug-wasm-heap').textContent = formatBytes(activeModule.HEAPU8.byteLength);
|
||||
document.querySelector('#debug-memfs').textContent = `${formatBytes(visible.bytes)} in ${visible.files} files`;
|
||||
document.querySelector('#debug-sd-files').textContent = `${formatBytes(sd.bytes)} in ${sd.files} files`;
|
||||
}
|
||||
|
||||
function initializeInspector() {
|
||||
for (const tab of document.querySelectorAll('[role="tab"]')) {
|
||||
tab.addEventListener('click', () => {
|
||||
for (const candidate of document.querySelectorAll('[role="tab"]')) {
|
||||
const selected = candidate === tab;
|
||||
candidate.setAttribute('aria-selected', selected);
|
||||
document.querySelector(`#${candidate.getAttribute('aria-controls')}`).hidden = !selected;
|
||||
}
|
||||
if (tab.dataset.tab === 'files') {
|
||||
renderFiles();
|
||||
} else if (tab.dataset.tab === 'debug') {
|
||||
renderDebug();
|
||||
}
|
||||
});
|
||||
}
|
||||
renderFiles();
|
||||
renderDebug();
|
||||
setInterval(renderDebug, 1000);
|
||||
}
|
||||
|
||||
function logLine(message) {
|
||||
logBuffer.push(message);
|
||||
@@ -188,7 +329,7 @@ function logLine(message) {
|
||||
}
|
||||
}
|
||||
|
||||
async function boot(file, variant, setStatus, sdCardProvider = loadEphemeralSdCard) {
|
||||
async function boot(file, variant, setStatus) {
|
||||
if (!crossOriginIsolated) {
|
||||
throw new Error('This emulator requires COOP/COEP headers. Start it with `make web`.');
|
||||
}
|
||||
@@ -196,12 +337,11 @@ async function boot(file, variant, setStatus, sdCardProvider = loadEphemeralSdCa
|
||||
|
||||
const artifactRoot = new URL('../dist/wasm/', import.meta.url);
|
||||
const qemuUrl = new URL('qemu-system-riscv32.js', artifactRoot);
|
||||
const [firmware, rom, sdCard] = await Promise.all([
|
||||
const [firmware, rom] = await Promise.all([
|
||||
prepareFirmware(new Uint8Array(await file.arrayBuffer()), import.meta.url),
|
||||
fetchBin(new URL('esp32c3-rom.bin', artifactRoot)),
|
||||
sdCardProvider(import.meta.url),
|
||||
]);
|
||||
const sdMessage = 'Using a blank ephemeral SD card; writes will be discarded when the emulator restarts.';
|
||||
const sdMessage = 'Using a 32 GiB ephemeral directory-backed SD card; writes will be discarded when the emulator restarts.';
|
||||
console.log(sdMessage);
|
||||
logLine(sdMessage);
|
||||
const { default: createQemu } = await import(qemuUrl);
|
||||
@@ -220,7 +360,7 @@ async function boot(file, variant, setStatus, sdCardProvider = loadEphemeralSdCa
|
||||
'-nic', 'none',
|
||||
'-d', qemuLogFlags,
|
||||
'-drive', 'file=/flash.bin,if=mtd,format=raw',
|
||||
'-drive', 'file=/sd.img,if=sd,format=raw',
|
||||
'-drive', 'file=fat:32:rw:/sdcard,if=sd,format=raw',
|
||||
],
|
||||
locateFile: path => new URL(path, artifactRoot).href,
|
||||
mainScriptUrlOrBlob: qemuUrl.href,
|
||||
@@ -229,10 +369,11 @@ async function boot(file, variant, setStatus, sdCardProvider = loadEphemeralSdCa
|
||||
stdin: () => null,
|
||||
};
|
||||
options.preRun = [() => {
|
||||
options.FS.mkdir('/bios');
|
||||
options.FS.mkdirTree('/bios');
|
||||
options.FS.mkdirTree('/var/tmp');
|
||||
options.FS.writeFile('/bios/esp32c3-rom.bin', rom);
|
||||
options.FS.writeFile('/flash.bin', firmware);
|
||||
options.FS.writeFile('/sd.img', sdCard);
|
||||
mountEphemeralSdCard(options.FS);
|
||||
}];
|
||||
|
||||
setStatus('Starting emulator…');
|
||||
@@ -258,6 +399,7 @@ function initialize() {
|
||||
sessionStorage.removeItem(DEBUG_LOG_KEY);
|
||||
logEl.textContent = '';
|
||||
});
|
||||
initializeInspector();
|
||||
|
||||
const setStatus = (message, error = false) => {
|
||||
status.textContent = message;
|
||||
@@ -270,6 +412,9 @@ function initialize() {
|
||||
variantInput.disabled = true;
|
||||
try {
|
||||
const module = await boot(file, variant, setStatus);
|
||||
activeModule = module;
|
||||
renderFiles();
|
||||
renderDebug();
|
||||
if (persistLog) {
|
||||
window.__xteinkModule = module;
|
||||
}
|
||||
|
||||
+29
-5
@@ -49,12 +49,36 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="log-panel" aria-label="Emulator logs">
|
||||
<div class="log-header">
|
||||
<span>QEMU log</span>
|
||||
<button id="log-clear" type="button">Clear</button>
|
||||
<section class="inspector" aria-label="Emulator inspector">
|
||||
<div class="tabs" role="tablist" aria-label="Emulator information">
|
||||
<button id="tab-logs" type="button" role="tab" aria-selected="true" aria-controls="panel-logs" data-tab="logs">Logs</button>
|
||||
<button id="tab-files" type="button" role="tab" aria-selected="false" aria-controls="panel-files" data-tab="files">Files</button>
|
||||
<button id="tab-debug" type="button" role="tab" aria-selected="false" aria-controls="panel-debug" data-tab="debug">Debug</button>
|
||||
</div>
|
||||
|
||||
<div id="panel-logs" class="tab-panel" role="tabpanel" aria-labelledby="tab-logs">
|
||||
<div class="log-header">
|
||||
<span>QEMU log</span>
|
||||
<button id="log-clear" type="button">Clear</button>
|
||||
</div>
|
||||
<pre id="log" class="log" role="log" aria-live="polite"></pre>
|
||||
</div>
|
||||
|
||||
<div id="panel-files" class="tab-panel" role="tabpanel" aria-labelledby="tab-files" hidden>
|
||||
<div id="file-list" class="file-list" aria-label="SD card files"></div>
|
||||
<pre id="file-preview" class="file-preview">Select a file to view it.</pre>
|
||||
</div>
|
||||
|
||||
<div id="panel-debug" class="tab-panel" role="tabpanel" aria-labelledby="tab-debug" hidden>
|
||||
<dl class="debug-stats">
|
||||
<div><dt>WASM heap committed</dt><dd id="debug-wasm-heap">Not started</dd></div>
|
||||
<div><dt>Browser JS heap</dt><dd id="debug-js-heap">Unavailable</dd></div>
|
||||
<div><dt>Visible MEMFS files</dt><dd id="debug-memfs">Not started</dd></div>
|
||||
<div><dt>SD files stored</dt><dd id="debug-sd-files">1 file</dd></div>
|
||||
<div><dt>Logical SD capacity</dt><dd>32 GiB</dd></div>
|
||||
</dl>
|
||||
<p class="debug-note">Committed heap is allocated WASM memory, not exact live malloc usage.</p>
|
||||
</div>
|
||||
<pre id="log" class="log" role="log" aria-live="polite"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+18
-3
@@ -71,7 +71,22 @@ canvas { display: block; width: 100%; height: auto; background: #fff; image-rend
|
||||
.workspace { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.log-panel { min-width: 0; }
|
||||
.log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .5rem; font-size: .85rem; color: #b9b3a4; }
|
||||
.inspector { min-width: 0; overflow: hidden; border: 1px solid #c8c2b6; border-radius: .75rem; background: #f7f4ed; }
|
||||
.tabs { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid #c8c2b6; }
|
||||
.tabs button { border: 0; border-right: 1px solid #c8c2b6; border-radius: 0; color: #56534d; background: transparent; }
|
||||
.tabs button:last-child { border-right: 0; }
|
||||
.tabs button[aria-selected="true"] { color: #fff; background: #343532; }
|
||||
.tab-panel { padding: 1rem; }
|
||||
.log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .5rem; font-size: .85rem; color: #625e55; }
|
||||
.log-header button { min-height: auto; padding: .25rem .75rem; font-size: .8rem; }
|
||||
.log { margin: 0; max-height: 20rem; overflow: auto; padding: .75rem 1rem; border-radius: .6rem; background: #1c1d1a; color: #d6d0c2; font: .78rem/1.4 ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; }
|
||||
.log { margin: 0; max-height: 38rem; overflow: auto; padding: .75rem 1rem; border-radius: .6rem; background: #1c1d1a; color: #d6d0c2; font: .78rem/1.4 ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; }
|
||||
.file-list { display: grid; gap: .4rem; margin-bottom: 1rem; }
|
||||
.file-directory { padding: .35rem .55rem; color: #625e55; font-weight: 700; }
|
||||
.file-list button { display: flex; justify-content: space-between; gap: 1rem; width: 100%; min-height: 2.4rem; text-align: left; }
|
||||
.file-list small { color: #cbc5b9; font-weight: 400; }
|
||||
.file-preview { min-height: 12rem; max-height: 28rem; overflow: auto; margin: 0; padding: .75rem 1rem; border-radius: .6rem; background: #fff; white-space: pre-wrap; }
|
||||
.debug-stats { display: grid; gap: .65rem; margin: 0; }
|
||||
.debug-stats div { display: flex; justify-content: space-between; gap: 1rem; padding-bottom: .65rem; border-bottom: 1px solid #d8d2c7; }
|
||||
.debug-stats dt { font-weight: 700; }
|
||||
.debug-stats dd { margin: 0; text-align: right; }
|
||||
.debug-note { margin: 1rem 0 0; color: #625e55; font-size: .8rem; }
|
||||
|
||||
Reference in New Issue
Block a user