feat(emulator): add runtime X3 and X4 selection

Model the ESP32-C3 I2C fingerprint path for stock firmware detection. Add a blank X4 panel stub, live GPIO inputs, and pin the exact QEMU revision.
This commit is contained in:
2026-07-19 20:51:44 -04:00
parent f972fbe78b
commit ca07c02e02
7 changed files with 508 additions and 137 deletions
+48 -58
View File
@@ -9,13 +9,13 @@ touching QEMU internals. Pairs with `ROADMAP.md` (the plan) — this is the
## 0. TL;DR of current state
- The `xteink-x3` QEMU machine boots **unmodified canonical firmware** to the
home screen, mounts a FAT32 SD image, and takes button input. Verified with
QMP `screendump`.
- One `xteink` QEMU machine selects `variant=x3|x4` at launch. **Unmodified
canonical firmware** detects X3 through I²C fingerprints and boots the home
screen; X4 detects through all-NAK probes and reaches a blank SSD1677 stub.
- All Phase-1 device models live in **one patch**:
`qemu/patches/0001-xteink-x3-machine.patch`, applied onto espressif/qemu tag
`esp-develop-9.2.2-20260417` by `scripts/build-qemu.sh`.
- Next: X4 sibling machine → compile to WASM (qemu-wasm) → static browser UI.
`qemu/patches/0001-xteink-machine.patch`, applied onto exact espressif/qemu
commit `40edccac415693c5130f91c01d84176ae6008566`.
- Next: compile to WASM (qemu-wasm) → static browser UI. Full X4 rendering waits.
---
@@ -166,11 +166,12 @@ Base addresses from ESP-IDF `soc/esp32c3` headers; all confirmed live.
---
## 6. Machine wiring (`xteink_x3_machine_init`)
## 6. Machine wiring (`xteink_machine_init`)
`xteink-x3` is a subclass of the `esp32c3` machine that, after the base init:
- creates the panel on `spi2.bus`, wires GPIO out 21→CS, 4→DC, 5→RST, and panel
BUSY→GPIO in 6;
`xteink` is a subclass of the `esp32c3` machine. `variant=x3` is the default;
`variant=x4` selects the sibling wiring before boot. After the base init it:
- 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);
@@ -190,33 +191,28 @@ ADC ladders on GPIO1 & GPIO2; power button GPIO3; UC8253 @ 16 MHz.
BQ27220 gauge (0x55), DS3231 RTC (0x68), QMI8658 IMU (0x6B). ≥2 of 3 on two
passes ⇒ X3; 0 ⇒ X4. Result is cached in NVS (`cphw/dev_det`) and can be
overridden (`cphw/dev_ovr`: 1=X4, 2=X3).
- **QEMU models no I²C**, so detection currently returns X4 by default. The
**crosspoint-reader-lua** firmware never calls `setDisplayX3()` at all → it is
**X4-only in emulation** and drives SSD1677 commands (`0x18/0x0c/0x44/0x45…`),
which our UC8253 panel ignores (→ blank screen). Don't chase that as a bug;
it's the wrong firmware for an X3 machine.
- To test the X3 display this session I built the **canonical** firmware with a
**one-line temporary hack**: `detectDeviceTypeWithFingerprint()` in
`/tmp/crosspoint-reader-main/lib/hal/HalGPIO.cpp` returns `X3` at the top.
**THIS EDIT IS IN /tmp AND WILL BE LOST ON REBOOT.** The merged test image is
`/tmp/crosspoint-main-x3-flash.bin` (also ephemeral).
- **Production plan (no custom firmware):** the `xteink-x3` machine should expose
the three X3 I²C fingerprint chips (needs an esp32c3 I²C controller model +
trivial BQ27220/DS3231/QMI8658 stubs that return plausible WHO_AM_I/values),
OR the emulator seeds the NVS `dev_ovr` key. Either makes stock firmware
auto-select X3. The `xteink-x4` machine simply omits the fingerprint. This is
how launch-time X3/X4 selection stays firmware-agnostic.
- `esp32.i2c` from the Espressif fork now runs on C3. The register layout
matches, but C3 command opcodes differ (RESTART=6, WRITE=1, READ=3, STOP=2,
END=4); a `c3` controller property normalizes them without changing xtensa.
- `variant=x3` attaches minimal BQ27220/DS3231/QMI8658 targets returning exactly
the probe signatures. `variant=x4` leaves the bus empty, so reads NAK and the
firmware selects X4. The resulting Arduino errors are noisy but harmless.
- Validation used upstream commit `556b8ae` with the old forced-X3 line removed.
X3 rendered the home screen (~16,054 black pixels); X4 emitted SSD1677 bytes
`12 18 80 0c ...`, proving the guest selected its X4 driver.
- Detection persists `cphw/dev_det`; use a clean flash or QEMU `-snapshot` when
testing both variants so one run's NVS cache cannot affect the next.
---
## 8. Reproducibility model
- **Durable = the patch** (`qemu/patches/0001-xteink-x3-machine.patch`), verified
to `git apply` cleanly onto a pristine checkout of the pinned tag. When you
edit `/tmp/qsrc`, **regenerate it** and re-verify:
- **Durable = the patch** (`qemu/patches/0001-xteink-machine.patch`), verified
against exact QEMU commit `40edccac415693c5130f91c01d84176ae6008566` (tag
`esp-develop-9.2.2-20260417`). When editing `_scratch/qemu-src`, regenerate:
```sh
cd <qemu-src> && git add -N <new files> && git diff --binary > <repo>/qemu/patches/0001-xteink-x3-machine.patch
git apply --reverse --check <repo>/qemu/patches/0001-xteink-x3-machine.patch # must succeed
cd <qemu-src> && git add -N <new files> && git diff --binary > <repo>/qemu/patches/0001-xteink-machine.patch
git apply --reverse --check <repo>/qemu/patches/0001-xteink-machine.patch
```
(Reverse-check on the dirty tree proves patch == pristine→current diff, which
is equivalent to forward-applying to pristine.)
@@ -229,7 +225,7 @@ ADC ladders on GPIO1 & GPIO2; power button GPIO3; UC8253 @ 16 MHz.
- **Headless run + capture** (canonical loop this session):
```sh
qemu-system-riscv32 -machine xteink-x3 -L <src>/pc-bios -display none -serial null \
qemu-system-riscv32 -machine xteink,variant=x3 -L <src>/pc-bios -display none -serial null \
-qmp unix:/tmp/q.sock,server=on,wait=off \
-drive file=flash.bin,if=mtd,format=raw -drive file=sd.img,if=sd,format=raw &
sleep 200 # this CPU is slow — give boot real time
@@ -238,10 +234,14 @@ ADC ladders on GPIO1 & GPIO2; power button GPIO3; UC8253 @ 16 MHz.
- **Inspect the screen**: convert with `nix shell nixpkgs#imagemagick -c magick
out.ppm out.png`; the black-pixel count is a cheap "did the screen change"
signal (home ≈ 15914, browse ≈ 11962, boot-white = 962).
- **Inject a button** (proper edge = set then clear; hold-from-boot gives no
press edge): `scripts/qmp-qom.py /tmp/q.sock set /machine/adc "adci[1]" 2760`
then `… 4095` to release. Ladder-1 midpoints: Back 3512 / Confirm 2694 /
Left 1493 / Right ~5; ladder-2: Up 2242 / Down ~5. **>3900 = no button.**
- **Inject a button** (proper edge = set then clear):
`scripts/qmp-qom.py /tmp/q.sock set /machine/adc "adci[1]" 2760`, then `4095`
to release. Ladder-1: Back 3512 / Confirm 2694 / Left 1493 / Right ~5;
ladder-2: Up 2242 / Down ~5. **>3900 = no button.** Power is active-low:
`qmp-qom.py ... set /machine/gpio "input-level[3]" false` then `true`.
- **Cold-boot power behavior:** stock firmware expects Power held after a
POWERON reset. On this ~24× slow host, hold GPIO3 low ~90 real seconds. For X4
battery boot also set GPIO20 low; high means USB and causes intentional sleep.
- **SD protocol trace**: flip `//#define DEBUG_SSI_SD 1` in `hw/sd/ssi-sd.c`.
- **Panel trace**: add an `info_report` in `xteink_x3_eink_transfer`
(bounded with a static counter — it's hot).
@@ -255,24 +255,14 @@ ADC ladders on GPIO1 & GPIO2; power button GPIO3; UC8253 @ 16 MHz.
## 10. Next steps (in priority order)
1. **X3 fingerprint or NVS seeding** (§7) so stock firmware auto-selects X3 with
no source edit. Smallest path to a genuinely firmware-agnostic X3 machine.
Needs a minimal esp32c3 I²C controller + 3 tiny I²C target stubs, OR NVS
pre-seed. This is the last thing standing between "works with a hacked build"
and "works with any downloaded binary."
2. **`xteink-x4` machine**: SSD1677 panel (different command set — `0x18` set-RAM,
`0x44/0x45` window, `0x4e/0x4f` cursor, `0x24` write, `0x22/0x20` update;
BUSY active-HIGH), X4 battery via ADC (not I²C gauge), 792×528? confirm X4
geometry from `BoardConfig.h XTEINK_X4`. Sibling machine + a second panel
device; reuse ADC/SPI2/GPIO/SD unchanged.
3. **Compile to WASM**: combine this patched fork with `ktock/qemu-wasm`'s WASM
TCG/TCI backend + Emscripten. Expect toolchain friction; the device models
themselves are portable C and shouldn't need changes. De-risk by first
building an *unpatched* esp32c3 target under qemu-wasm, then apply our patch.
4. **Static browser front-end**: `<canvas>` fed from the display surface; DOM
buttons → the ADC/GPIO QOM injection (same calls as `qmp-qom.py`); file input
→ emulated flash; preloaded/uploaded SD image → block backend; X3/X4 chosen
1. **Compile to WASM**: combine this pinned Espressif fork with
`ktock/qemu-wasm`'s WASM TCG/TCI backend + Emscripten. De-risk by building an
unpatched esp32c3 target under qemu-wasm first, then apply our patch.
2. **Static browser front-end**: display surface → canvas; physical buttons →
ADC/GPIO controls; uploaded flash; SD block backend; `variant=x3|x4` chosen
before boot. Must run fully from static assets (airgapped).
3. **Full X4 rendering later**: SSD1677 is 800×480 controller / 480×800 portrait,
BUSY active-high. Implement its RAM/window/update commands when needed.
---
@@ -281,10 +271,10 @@ ADC ladders on GPIO1 & GPIO2; power button GPIO3; UC8253 @ 16 MHz.
| What | Where |
|------|-------|
| Emulator repo | `/home/evanreichard/Development/git/personal/xteink-web-emulator` |
| The patch | `qemu/patches/0001-xteink-x3-machine.patch` |
| QEMU source (ephemeral) | `_scratch/qemu-src` (via `make qemu`); was `/tmp/qsrc` this session |
| Canonical firmware (X3 hack in /tmp, ephemeral) | `/tmp/crosspoint-reader-main` (`lib/hal/HalGPIO.cpp` forced X3) |
| Lua firmware (X4-only in emu) | `/home/evanreichard/Development/git/personal/crosspoint-reader-lua` |
| Upstream firmware refs | `crosspoint-reader/crosspoint-reader` (canonical, cleaner) + its `freeink-sdk` submodule |
| The patch | `qemu/patches/0001-xteink-machine.patch` |
| QEMU source (ephemeral) | `_scratch/qemu-src` (via `make qemu`) |
| Canonical firmware | `https://github.com/crosspoint-reader/crosspoint-reader` |
| Tested firmware checkout | `/tmp/crosspoint-reader-main` @ `556b8ae` (unmodified) |
| espressif/qemu commit | `40edccac415693c5130f91c01d84176ae6008566` |
| espressif/qemu tag | `esp-develop-9.2.2-20260417` |
| qemu-wasm | `github.com/ktock/qemu-wasm` |