diff --git a/HANDOFF.md b/HANDOFF.md index be124d4..8510793 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -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 && git add -N && git diff --binary > /qemu/patches/0001-xteink-x3-machine.patch - git apply --reverse --check /qemu/patches/0001-xteink-x3-machine.patch # must succeed + cd && git add -N && git diff --binary > /qemu/patches/0001-xteink-machine.patch + git apply --reverse --check /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 /pc-bios -display none -serial null \ + qemu-system-riscv32 -machine xteink,variant=x3 -L /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**: `` 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` | diff --git a/Makefile b/Makefile index 2ba4e11..46f008d 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,11 @@ -# PlatformIO artifacts to merge into a 16 MB ESP32-C3 flash image. -# Canonical firmware: https://github.com/crosspoint-reader/crosspoint-reader -FIRMWARE_DIR ?= ../crosspoint-reader/.pio/build/default -SD_SRC ?= ../crosspoint-reader/sdcard +# Canonical firmware: clone https://github.com/crosspoint-reader/crosspoint-reader +# next to this repo, then build its PlatformIO `default` environment. +FIRMWARE_REPO ?= ../crosspoint-reader +FIRMWARE_DIR ?= $(FIRMWARE_REPO)/.pio/build/default +SD_SRC ?= $(FIRMWARE_REPO)/sdcard QEMU_SRC ?= _scratch/qemu-src QEMU_BIN = $(QEMU_SRC)/build/qemu-system-riscv32 +VARIANT ?= x3 .PHONY: firmware sdimage qemu run run-upstream chip clean @@ -22,9 +24,9 @@ sdimage: qemu: QEMU_SRC=$(QEMU_SRC) scripts/build-qemu.sh -# Patched xteink X3 machine. Add `-display none` for a headless run. +# Select X4 with `make run VARIANT=x4`. Add `-display none` for headless. run: qemu - $(QEMU_BIN) -machine xteink-x3 -L $(QEMU_SRC)/pc-bios \ + $(QEMU_BIN) -machine xteink,variant=$(VARIANT) -L $(QEMU_SRC)/pc-bios \ -serial stdio -drive file=flash.bin,if=mtd,format=raw \ $(if $(wildcard sd.img),-drive file=sd.img,if=sd,format=raw) diff --git a/README.md b/README.md index 9fd805c..d35924b 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,22 @@ An offline, in-browser emulator for the xteink **X3/X4** (ESP32-C3 + e-ink): upload a firmware `.bin`, run the real machine code client-side, see the screen, press buttons, mount a virtual SD — no server, no proprietary engine. -**Status:** native peripheral models done for the X3. Unmodified canonical -firmware boots to the home screen, mounts a FAT32 SD image, lists its files, and -responds to button presses under a patched espressif-qemu. The browser (WASM) +**Status:** one native `xteink` machine selects X3 or X4 at launch. Unmodified +[canonical firmware](https://github.com/crosspoint-reader/crosspoint-reader) +auto-detects the selected variant: X3 boots with its UC8253 display and SD card; +X4 reaches its SSD1677 driver through a blank protocol stub. The browser (WASM) port is next. See [ROADMAP.md](./ROADMAP.md) for architecture and plan. ## Quick start (native QEMU) ```sh +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 tag, apply patch, build (JOBS=2) -make firmware sdimage # 16 MB flash.bin + FAT32 sd.img from a PlatformIO build -make run # boot the xteink-x3 machine (add `-display none` for headless) +make qemu # clone espressif/qemu @ pinned commit, apply patch, build (JOBS=2) +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 ``` Point at a firmware build with `make firmware FIRMWARE_DIR=/path/to/.pio/build/gh_release`. @@ -23,9 +27,10 @@ Point at a firmware build with `make firmware FIRMWARE_DIR=/path/to/.pio/build/g ## Contents -- `qemu/patches/0001-xteink-x3-machine.patch` — the `xteink-x3` machine and its - device models (ADC, GPIO, SPI2, UC8253 e-ink, SD-over-SPI fix), applied onto - the pinned espressif/qemu tag by `scripts/build-qemu.sh`. +- `qemu/patches/0001-xteink-machine.patch` — the `xteink,variant=x3|x4` machine + and its device models (I²C fingerprints, ADC, GPIO, SPI2, X3 UC8253, X4 panel + stub, SD-over-SPI fix), applied onto the pinned espressif/qemu commit by + `scripts/build-qemu.sh`. - `flake.nix` — the espressif-qemu *release* binary as the fast generic baseline (`nix run .#run-upstream -- flash.bin`), plus a devShell with QEMU's full build environment. `Makefile` — `qemu`, `firmware`, `sdimage`, `run`, `chip`. diff --git a/ROADMAP.md b/ROADMAP.md index f70d18d..6263fc6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,28 +19,31 @@ running in-browser from static files. The endgame merges those two. ## Status -**Phase 1 (native peripheral models) is done for the X3.** The unmodified -canonical firmware (`crosspoint-reader/crosspoint-reader`, X3-selected) boots to -the home screen, mounts a FAT32 SD image, lists its contents, and responds to -button presses — all under native espressif-qemu with our patch. Reproduce: +**Phase 1 (native foundation) is done for X3 and X4 selection.** Unmodified +[canonical firmware](https://github.com/crosspoint-reader/crosspoint-reader) +auto-detects `xteink,variant=x3` through its real I²C fingerprint and boots the +X3 home screen with SD support. `variant=x4` omits those chips, so the same +firmware selects its SSD1677 driver and sends its command stream to a blank X4 +panel stub. Reproduce: ```sh -make qemu # clone espressif/qemu @ pinned tag, apply patch, build +make qemu # clone espressif/qemu @ pinned commit, patch, build make firmware sdimage # 16 MB flash.bin + FAT32 sd.img from a pio build -make run # boot the xteink-x3 machine (add `-display none` for headless) +make run # X3 (default) +make run VARIANT=x4 # X4 detection + blank panel stub ``` -Screenshots (via QMP `screendump`) confirmed: boot → home → Browse Files. +QMP screenshots confirmed the X3 home screen and 528×792 orientation; captured +X4 SPI traffic confirmed the firmware selected its SSD1677 driver. ### Device selection (X3 vs X4) X3 and X4 share one ESP32-C3 binary and pick a profile at runtime via an I²C fingerprint (BQ27220 gauge + DS3231 RTC + QMI8658 IMU on SDA20/SCL0), with an NVS override (`cphw/dev_ovr`: 1=X4, 2=X3). **The emulator picks X3 vs X4 at -launch** by starting the matching machine (`xteink-x3`, later `xteink-x4`); the -X3 machine will expose the X3 fingerprint so stock dual firmware auto-detects. -No recompiled/custom firmware — the current test build forces X3 only because -the fingerprint I²C devices aren't modelled yet. +launch** with one machine property: `-machine xteink,variant=x3|x4`. X3 attaches +minimal BQ27220/DS3231/QMI8658 targets; X4 leaves the I²C bus empty. This drives +both firmware detection and panel wiring without modifying the firmware. ## The interfaces (native now → browser later) @@ -49,38 +52,40 @@ reuses unchanged — no bespoke abstraction layer: | Peripheral | Native seam | Browser binding (phase 3) | |------------|-------------|---------------------------| -| e-ink panel | `xteink-x3-eink` SSI device → QEMU graphical console (528×792, physical orientation) | `` from the same display surface | +| e-ink panel | X3 UC8253 (528×792) or blank X4 SSD1677 stub (480×800) → QEMU graphical console | `` 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) | -| Buttons + battery | `esp32c3.adc` QOM props `adci[1]`/`adci[2]`; power button = GPIO3 input | DOM buttons → `qom-set` equivalent | +| 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 | -## Phase 1 device models (in `qemu/patches/0001-xteink-x3-machine.patch`) +## Phase 1 device models (in `qemu/patches/0001-xteink-machine.patch`) - **`esp32c3.adc`** — SAR ADC: oneshot completes immediately; per-channel value is settable live via QOM (`adci[*]`). Feeds battery and the two resistor-ladder button groups. Replaced the original always-done register shim. - **`esp32_gpio`** — real OUT/ENABLE/IN registers with 32 named input + output - lines. Outputs read high while a pin is input-configured (matches the board's - pull-ups; without this, display CS floated low during SD probing). + lines and live `input-level[*]` QOM controls. Outputs read high while a pin is + input-configured (matches the board's pull-ups). - **`ssi.esp32c3.spi2`** — the general-purpose SPI2 controller (CPU-buffer transactions on an SSI bus). DMA path deliberately unimplemented until needed. - **`xteink-x3-eink`** — UC8253 panel: decodes DTM1/DTM2 planes (52 272 B each), drives BUSY, renders to a QEMU console. Protocol from `chip/eink-x3.chip.c`. - **`ssi-sd` fix** — track card idle state so CMD58 reports ready after ACMD41 (the 9.2 fork hardcoded idle, which SdFat rejects). -- **`xteink-x3` machine** — subclass of `esp32c3` wiring panel CS21/DC4/RST5/ - BUSY6 and SD CS12 onto SPI2, and the SD card onto QEMU's block layer. +- **`esp32.i2c` + fingerprint targets** — C3 opcode support and minimal + BQ27220/DS3231/QMI8658 register responses for stock X3 detection. +- **`xteink` machine** — `variant=x3|x4` controls fingerprint presence and panel + type; shared panel/SD wiring stays in one machine. +- **X4 panel stub** — blank 480×800 console with X4 idle-low BUSY; accepts the + SSD1677 command stream without implementing it. ## Remaining phases -1. **X4 machine** — SSD1677 panel + X4 battery/ADC profile as a sibling machine. - The X3 command trace (`0x18/0x0c/0x44/0x45…`) is already captured. -2. **Compile the patched fork to WASM** — merge Espressif's SoC models with +1. **Compile the patched fork to WASM** — combine Espressif's SoC models with qemu-wasm's WASM TCG/TCI backend; build `qemu-system-riscv32` with Emscripten. - Output: static `.wasm` + `.js` + packaged BIOS/ROM. -3. **Browser front-end (static)** — upload `.bin` → emulated flash; `` - from the display surface; DOM buttons → ADC/GPIO injection; preloaded SD - image; launch-time X3/X4 selection. Fully airgapped. +2. **Browser front-end (static)** — uploaded flash, canvas, physical buttons, + SD block backend, and pre-boot X3/X4 picker. Fully airgapped. +3. **Full X4 panel** — implement SSD1677 RAM/window/update commands when visible + X4 rendering is needed; the selection, dimensions, wiring, and stub exist. ## Repo layout @@ -89,7 +94,7 @@ reuses unchanged — no bespoke abstraction layer: | `flake.nix` | espressif-qemu release pkg (`nix run .#run-upstream`) + full QEMU build devShell | | `Makefile` | `qemu` (source build), `firmware`, `sdimage`, `run`, `chip` | | `scripts/build-qemu.sh` | clone pinned espressif/qemu, apply patch, build riscv32-softmmu | -| `qemu/patches/` | the xteink X3 machine + device models (applied onto the pinned tag) | +| `qemu/patches/` | xteink machine + device models (applied onto the pinned QEMU commit) | | `chip/eink-x3.chip.c` | legacy Wokwi model — the reverse-engineered X3 e-ink protocol reference | | `web/` | (phase 3) static front-end: upload + canvas + buttons | diff --git a/qemu/patches/0001-xteink-x3-machine.patch b/qemu/patches/0001-xteink-machine.patch similarity index 66% rename from qemu/patches/0001-xteink-x3-machine.patch rename to qemu/patches/0001-xteink-machine.patch index 65af824..0f81bb3 100644 --- a/qemu/patches/0001-xteink-x3-machine.patch +++ b/qemu/patches/0001-xteink-machine.patch @@ -129,10 +129,10 @@ index 36a9ace..c73f313 100644 system_ss.add(when: 'CONFIG_VIRTIO', if_true: files('virtio-dmabuf.c')) diff --git i/hw/display/xteink_x3_eink.c w/hw/display/xteink_x3_eink.c new file mode 100644 -index 0000000..bc9a18c +index 0000000..b699aba --- /dev/null +++ w/hw/display/xteink_x3_eink.c -@@ -0,0 +1,165 @@ +@@ -0,0 +1,248 @@ +/* + * xteink X3 UC8253 e-ink panel + * @@ -292,17 +292,108 @@ index 0000000..bc9a18c + .class_init = xteink_x3_eink_class_init, +}; + -+static void xteink_x3_eink_register_types(void) ++typedef struct XteinkX4EinkState { ++ SSIPeripheral parent_obj; ++ QemuConsole *console; ++ qemu_irq busy; ++} XteinkX4EinkState; ++ ++#define XTEINK_X4_EINK(obj) \ ++ OBJECT_CHECK(XteinkX4EinkState, (obj), TYPE_XTEINK_X4_EINK) ++ ++static void xteink_x4_eink_render(XteinkX4EinkState *s) +{ -+ type_register_static(&xteink_x3_eink_info); ++ DisplaySurface *surface = qemu_console_surface(s->console); ++ memset(surface_data(surface), 0xff, ++ surface_stride(surface) * surface_height(surface)); ++ dpy_gfx_update(s->console, 0, 0, 480, 800); +} + -+type_init(xteink_x3_eink_register_types) ++static void xteink_x4_eink_reset(DeviceState *dev) ++{ ++ XteinkX4EinkState *s = XTEINK_X4_EINK(dev); ++ qemu_set_irq(s->busy, 0); ++ xteink_x4_eink_render(s); ++} ++ ++static void xteink_x4_eink_set_input(void *opaque, int n, int level) ++{ ++} ++ ++static uint32_t xteink_x4_eink_transfer(SSIPeripheral *peripheral, ++ uint32_t value) ++{ ++ return 0xff; ++} ++ ++static void xteink_x4_eink_invalidate(void *opaque) ++{ ++ xteink_x4_eink_render(XTEINK_X4_EINK(opaque)); ++} ++ ++static const GraphicHwOps xteink_x4_eink_graphics_ops = { ++ .invalidate = xteink_x4_eink_invalidate, ++}; ++ ++static void xteink_x4_eink_init(Object *obj) ++{ ++ XteinkX4EinkState *s = XTEINK_X4_EINK(obj); ++ ++ s->console = graphic_console_init(DEVICE(s), 0, ++ &xteink_x4_eink_graphics_ops, s); ++ dpy_gfx_replace_surface(s->console, qemu_create_displaysurface(480, 800)); ++ qdev_init_gpio_in_named(DEVICE(s), xteink_x4_eink_set_input, ++ XTEINK_X3_EINK_DC, 1); ++ qdev_init_gpio_in_named(DEVICE(s), xteink_x4_eink_set_input, ++ XTEINK_X3_EINK_RESET, 1); ++ qdev_init_gpio_out_named(DEVICE(s), &s->busy, ++ XTEINK_X3_EINK_BUSY, 1); ++} ++ ++static void xteink_x4_eink_realize(SSIPeripheral *peripheral, Error **errp) ++{ ++} ++ ++static void xteink_x4_eink_class_init(ObjectClass *klass, void *data) ++{ ++ DeviceClass *dc = DEVICE_CLASS(klass); ++ SSIPeripheralClass *ssi = SSI_PERIPHERAL_CLASS(klass); ++ ++ ssi->realize = xteink_x4_eink_realize; ++ ssi->transfer = xteink_x4_eink_transfer; ++ ssi->cs_polarity = SSI_CS_LOW; ++ device_class_set_legacy_reset(dc, xteink_x4_eink_reset); ++ dc->user_creatable = false; ++} ++ ++static const TypeInfo xteink_x4_eink_info = { ++ .name = TYPE_XTEINK_X4_EINK, ++ .parent = TYPE_SSI_PERIPHERAL, ++ .instance_size = sizeof(XteinkX4EinkState), ++ .instance_init = xteink_x4_eink_init, ++ .class_init = xteink_x4_eink_class_init, ++}; ++ ++static void xteink_eink_register_types(void) ++{ ++ type_register_static(&xteink_x3_eink_info); ++ type_register_static(&xteink_x4_eink_info); ++} ++ ++type_init(xteink_eink_register_types) diff --git i/hw/gpio/esp32_gpio.c w/hw/gpio/esp32_gpio.c -index 4fea1f5..55cac08 100644 +index 4fea1f5..9ec8bd9 100644 --- i/hw/gpio/esp32_gpio.c +++ w/hw/gpio/esp32_gpio.c -@@ -19,26 +19,77 @@ +@@ -12,6 +12,7 @@ + #include "qemu/log.h" + #include "qemu/error-report.h" + #include "qapi/error.h" ++#include "qapi/visitor.h" + #include "hw/hw.h" + #include "hw/sysbus.h" + #include "hw/registerfields.h" +@@ -19,26 +20,99 @@ #include "hw/qdev-properties.h" #include "hw/gpio/esp32_gpio.h" @@ -328,6 +419,28 @@ index 4fea1f5..55cac08 100644 +{ + Esp32GpioState *s = ESP32_GPIO(opaque); + s->input_level = deposit32(s->input_level, pin, 1, !!level); ++} ++ ++static void esp32_gpio_get_input(Object *obj, Visitor *v, const char *name, ++ void *opaque, Error **errp) ++{ ++ Esp32GpioState *s = ESP32_GPIO(obj); ++ int pin = GPOINTER_TO_INT(opaque); ++ bool level = extract32(s->input_level, pin, 1); ++ visit_type_bool(v, name, &level, errp); ++} ++ ++static void esp32_gpio_set_input_property(Object *obj, Visitor *v, ++ const char *name, void *opaque, ++ Error **errp) ++{ ++ Esp32GpioState *s = ESP32_GPIO(obj); ++ int pin = GPOINTER_TO_INT(opaque); ++ bool level; ++ ++ if (visit_type_bool(v, name, &level, errp)) { ++ esp32_gpio_set_input(s, pin, level); ++ } +} static uint64_t esp32_gpio_read(void *opaque, hwaddr addr, unsigned int size) @@ -388,7 +501,7 @@ index 4fea1f5..55cac08 100644 } static const MemoryRegionOps uart_ops = { -@@ -49,6 +100,11 @@ static const MemoryRegionOps uart_ops = { +@@ -49,6 +123,11 @@ static const MemoryRegionOps uart_ops = { static void esp32_gpio_reset_hold(Object *obj, ResetType type) { @@ -400,7 +513,7 @@ index 4fea1f5..55cac08 100644 } static void esp32_gpio_realize(DeviceState *dev, Error **errp) -@@ -67,6 +123,12 @@ static void esp32_gpio_init(Object *obj) +@@ -67,6 +146,19 @@ static void esp32_gpio_init(Object *obj) TYPE_ESP32_GPIO, 0x1000); sysbus_init_mmio(sbd, &s->iomem); sysbus_init_irq(sbd, &s->irq); @@ -408,13 +521,201 @@ index 4fea1f5..55cac08 100644 + ESP32_GPIO_INPUT, ESP32_GPIO_COUNT); + qdev_init_gpio_out_named(DEVICE(obj), s->output_lines, + ESP32_GPIO_OUTPUT, ESP32_GPIO_COUNT); ++ for (int i = 0; i < ESP32_GPIO_COUNT; i++) { ++ char *name = g_strdup_printf("input-level[%d]", i); ++ object_property_add(obj, name, "bool", esp32_gpio_get_input, ++ esp32_gpio_set_input_property, NULL, ++ GINT_TO_POINTER(i)); ++ g_free(name); ++ } + + esp32_gpio_reset_hold(obj, RESET_TYPE_COLD); } static Property esp32_gpio_properties[] = { +diff --git i/hw/i2c/esp32_i2c.c w/hw/i2c/esp32_i2c.c +index 5f4022f..1477f15 100644 +--- i/hw/i2c/esp32_i2c.c ++++ w/hw/i2c/esp32_i2c.c +@@ -4,6 +4,7 @@ + #include "qemu/error-report.h" + #include "hw/i2c/esp32_i2c.h" + #include "hw/irq.h" ++#include "hw/qdev-properties.h" + + static void esp32_i2c_do_transaction(Esp32I2CState * s); + static void esp32_i2c_update_irq(Esp32I2CState * s); +@@ -177,7 +178,17 @@ static void esp32_i2c_do_transaction(Esp32I2CState * s) + bool stop_or_end = false; + for (int i_cmd = 0; i_cmd < ESP32_I2C_CMD_COUNT && !stop_or_end; ++i_cmd) { + uint32_t cmd = s->cmd_reg[i_cmd]; +- char opcode = FIELD_EX32(cmd, I2C_CMD, OPCODE); ++ int opcode = FIELD_EX32(cmd, I2C_CMD, OPCODE); ++ if (s->c3) { ++ switch (opcode) { ++ case 6: opcode = I2C_OPCODE_RSTART; break; ++ case 1: opcode = I2C_OPCODE_WRITE; break; ++ case 3: opcode = I2C_OPCODE_READ; break; ++ case 2: opcode = I2C_OPCODE_STOP; break; ++ case 4: opcode = I2C_OPCODE_END; break; ++ default: break; ++ } ++ } + switch (opcode) { + case I2C_OPCODE_RSTART: + i2c_end_transfer(s->bus); +@@ -261,9 +272,16 @@ static void esp32_i2c_init(Object * obj) + fifo8_create(&s->rx_fifo, ESP32_I2C_FIFO_LENGTH); + } + ++static const Property esp32_i2c_properties[] = { ++ DEFINE_PROP_BOOL("c3", Esp32I2CState, c3, false), ++}; ++ + static void esp32_i2c_class_init(ObjectClass * klass, void * data) + { ++ DeviceClass *dc = DEVICE_CLASS(klass); + ResettableClass *rc = RESETTABLE_CLASS(klass); ++ ++ device_class_set_props(dc, esp32_i2c_properties); + rc->phases.hold = esp32_i2c_reset_hold; + } + +diff --git i/hw/i2c/meson.build w/hw/i2c/meson.build +index e72dca0..5df10c2 100644 +--- i/hw/i2c/meson.build ++++ w/hw/i2c/meson.build +@@ -17,6 +17,7 @@ i2c_ss.add(when: 'CONFIG_OMAP', if_true: files('omap_i2c.c')) + i2c_ss.add(when: 'CONFIG_PPC4XX', if_true: files('ppc4xx_i2c.c')) + i2c_ss.add(when: 'CONFIG_XTENSA_ESP32', if_true: files('esp32_i2c.c')) + i2c_ss.add(when: 'CONFIG_XTENSA_ESP32S3', if_true: files('esp32_i2c.c')) ++i2c_ss.add(when: 'CONFIG_RISCV_ESP32C3', if_true: files('esp32_i2c.c', 'xteink_fingerprint.c')) + i2c_ss.add(when: 'CONFIG_PCA954X', if_true: files('i2c_mux_pca954x.c')) + i2c_ss.add(when: 'CONFIG_PMBUS', if_true: files('pmbus_device.c')) + i2c_ss.add(when: 'CONFIG_BCM2835_I2C', if_true: files('bcm2835_i2c.c')) +diff --git i/hw/i2c/xteink_fingerprint.c w/hw/i2c/xteink_fingerprint.c +new file mode 100644 +index 0000000..06a2e83 +--- /dev/null ++++ w/hw/i2c/xteink_fingerprint.c +@@ -0,0 +1,105 @@ ++/* ++ * xteink X3 I²C fingerprint chips. ++ * ++ * The crosspoint firmware distinguishes X3 from X4 by probing three I²C ++ * devices on SDA20/SCL0 (BQ27220 gauge @0x55, DS3231 RTC @0x68, QMI8658 IMU ++ * @0x6B). Presence of >=2/3 on two passes selects X3. These stubs return just ++ * enough register data to pass that probe; full gauge/RTC/IMU behaviour is not ++ * modelled (X3 runtime reads degrade gracefully, matching a missing sensor). ++ */ ++#include "qemu/osdep.h" ++#include "hw/i2c/i2c.h" ++#include "migration/vmstate.h" ++ ++#define TYPE_XTEINK_FPCHIP "xteink-fpchip" ++OBJECT_DECLARE_SIMPLE_TYPE(XteinkFpChipState, XTEINK_FPCHIP) ++ ++struct XteinkFpChipState { ++ I2CSlave parent_obj; ++ uint8_t reg; ++ bool reg_set; ++}; ++ ++/* Fixed register bytes the firmware fingerprint checks. Multi-byte values are ++ * little-endian and read via auto-incrementing register pointer. */ ++static uint8_t fpchip_reg(uint8_t addr, uint8_t reg) ++{ ++ switch (addr) { ++ case 0x55: /* BQ27220: SOC 0x2C (<=100%), Voltage 0x08 (2500..5000mV) */ ++ switch (reg) { ++ case 0x08: return 0x74; /* 0x0E74 = 3700 mV */ ++ case 0x09: return 0x0E; ++ case 0x2C: return 0x50; /* 80% */ ++ case 0x2D: return 0x00; ++ default: return 0x00; ++ } ++ case 0x68: /* DS3231: seconds 0x00 must be valid BCD */ ++ return 0x00; ++ case 0x6B: /* QMI8658: WHO_AM_I 0x00 == 0x05 */ ++ return (reg == 0x00) ? 0x05 : 0x00; ++ default: ++ return 0x00; ++ } ++} ++ ++static int fpchip_event(I2CSlave *i2c, enum i2c_event event) ++{ ++ XteinkFpChipState *s = XTEINK_FPCHIP(i2c); ++ if (event == I2C_START_SEND) { ++ s->reg_set = false; ++ } ++ return 0; ++} ++ ++static int fpchip_send(I2CSlave *i2c, uint8_t data) ++{ ++ XteinkFpChipState *s = XTEINK_FPCHIP(i2c); ++ if (!s->reg_set) { ++ s->reg = data; ++ s->reg_set = true; ++ } ++ return 0; ++} ++ ++static uint8_t fpchip_recv(I2CSlave *i2c) ++{ ++ XteinkFpChipState *s = XTEINK_FPCHIP(i2c); ++ return fpchip_reg(i2c->address, s->reg++); ++} ++ ++static const VMStateDescription vmstate_fpchip = { ++ .name = TYPE_XTEINK_FPCHIP, ++ .version_id = 1, ++ .minimum_version_id = 1, ++ .fields = (const VMStateField[]) { ++ VMSTATE_I2C_SLAVE(parent_obj, XteinkFpChipState), ++ VMSTATE_UINT8(reg, XteinkFpChipState), ++ VMSTATE_BOOL(reg_set, XteinkFpChipState), ++ VMSTATE_END_OF_LIST() ++ } ++}; ++ ++static void fpchip_class_init(ObjectClass *klass, void *data) ++{ ++ DeviceClass *dc = DEVICE_CLASS(klass); ++ I2CSlaveClass *k = I2C_SLAVE_CLASS(klass); ++ ++ k->event = fpchip_event; ++ k->recv = fpchip_recv; ++ k->send = fpchip_send; ++ dc->vmsd = &vmstate_fpchip; ++} ++ ++static const TypeInfo fpchip_info = { ++ .name = TYPE_XTEINK_FPCHIP, ++ .parent = TYPE_I2C_SLAVE, ++ .instance_size = sizeof(XteinkFpChipState), ++ .class_init = fpchip_class_init, ++}; ++ ++static void fpchip_register_types(void) ++{ ++ type_register_static(&fpchip_info); ++} ++ ++type_init(fpchip_register_types) +diff --git i/hw/riscv/Kconfig w/hw/riscv/Kconfig +index 192ab47..293808d 100644 +--- i/hw/riscv/Kconfig ++++ w/hw/riscv/Kconfig +@@ -119,4 +119,5 @@ config RISCV_ESP32C3 + select OPENCORES_ETH + select UNIMP + select ESP_RGB ++ select I2C + diff --git i/hw/riscv/esp32c3.c w/hw/riscv/esp32c3.c -index ed698fb..a49eae6 100644 +index ed698fb..2f1ccbf 100644 --- i/hw/riscv/esp32c3.c +++ w/hw/riscv/esp32c3.c @@ -22,6 +22,7 @@ @@ -425,16 +726,17 @@ index ed698fb..a49eae6 100644 #include "sysemu/sysemu.h" #include "sysemu/kvm.h" #include "sysemu/runstate.h" -@@ -40,6 +41,8 @@ +@@ -40,6 +41,9 @@ #include "hw/timer/esp32c3_timg.h" #include "hw/timer/esp32c3_systimer.h" #include "hw/ssi/esp32c3_spi.h" +#include "hw/ssi/esp32c3_spi2.h" +#include "hw/adc/esp32c3_adc.h" ++#include "hw/i2c/esp32_i2c.h" #include "hw/misc/esp32c3_rtc_cntl.h" #include "hw/misc/esp32c3_aes.h" #include "hw/misc/esp32c3_rsa.h" -@@ -49,6 +52,8 @@ +@@ -49,6 +53,8 @@ #include "hw/misc/esp32c3_jtag.h" #include "hw/dma/esp32c3_gdma.h" #include "hw/display/esp_rgb.h" @@ -443,39 +745,43 @@ index ed698fb..a49eae6 100644 #include "hw/net/can/esp32c3_twai.h" #define ESP32C3_IO_WARNING 0 -@@ -66,6 +71,7 @@ struct Esp32C3MachineState { +@@ -66,6 +72,8 @@ struct Esp32C3MachineState { EspRISCVCPU soc; BusState periph_bus; MemoryRegion iomem; -+ bool xteink_x3; ++ bool xteink; ++ bool x4; qemu_irq cpu_reset; -@@ -86,6 +92,8 @@ struct Esp32C3MachineState { +@@ -86,6 +94,9 @@ struct Esp32C3MachineState { ESP32C3TimgState timg[2]; ESP32C3SysTimerState systimer; ESP32C3SpiState spi1; + ESP32C3Spi2State spi2; + ESP32C3AdcState adc; ++ Esp32I2CState i2c; ESP32C3RtcCntlState rtccntl; ESP32C3UsbJtagState jtag; ESPRgbState rgb; -@@ -420,9 +428,13 @@ static void esp32c3_machine_init(MachineState *machine) +@@ -420,9 +431,15 @@ static void esp32c3_machine_init(MachineState *machine) object_initialize_child(OBJECT(machine), "timg1", &ms->timg[1], TYPE_ESP32C3_TIMG); object_initialize_child(OBJECT(machine), "systimer", &ms->systimer, TYPE_ESP32C3_SYSTIMER); object_initialize_child(OBJECT(machine), "spi1", &ms->spi1, TYPE_ESP32C3_SPI); + object_initialize_child(OBJECT(machine), "spi2", &ms->spi2, TYPE_ESP32C3_SPI2); + object_initialize_child(OBJECT(machine), "adc", &ms->adc, TYPE_ESP32C3_ADC); ++ object_initialize_child(OBJECT(machine), "i2c", &ms->i2c, TYPE_ESP32_I2C); ++ qdev_prop_set_bit(DEVICE(&ms->i2c), "c3", true); object_initialize_child(OBJECT(machine), "rtccntl", &ms->rtccntl, TYPE_ESP32C3_RTC_CNTL); object_initialize_child(OBJECT(machine), "jtag", &ms->jtag, TYPE_ESP32C3_JTAG); - object_initialize_child(OBJECT(machine), "rgb", &ms->rgb, TYPE_ESP_RGB); -+ if (!ms->xteink_x3) { ++ if (!ms->xteink) { + object_initialize_child(OBJECT(machine), "rgb", &ms->rgb, TYPE_ESP_RGB); + } object_initialize_child(OBJECT(machine), "twai", &ms->twai, TYPE_ESP32C3_TWAI); /* Realize all the I/O peripherals we depend on */ -@@ -476,6 +488,20 @@ static void esp32c3_machine_init(MachineState *machine) +@@ -476,6 +493,31 @@ static void esp32c3_machine_init(MachineState *machine) } } @@ -492,31 +798,50 @@ index ed698fb..a49eae6 100644 + MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->adc), 0); + memory_region_add_subregion_overlap(sys_mem, DR_REG_APB_SARADC_BASE, mr, 0); + } ++ ++ /* I2C controller (X3/X4 fingerprint chips live on SDA20/SCL0). The ++ * interrupt must reach the matrix or the ESP-IDF driver blocks on its ++ * completion semaphore and every probe times out. */ ++ { ++ sysbus_realize(SYS_BUS_DEVICE(&ms->i2c), &error_fatal); ++ MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->i2c), 0); ++ memory_region_add_subregion_overlap(sys_mem, DR_REG_I2C_EXT_BASE, mr, 0); ++ sysbus_connect_irq(SYS_BUS_DEVICE(&ms->i2c), 0, ++ qdev_get_gpio_in(intmatrix_dev, ETS_I2C_EXT0_INTR_SOURCE)); ++ } + for (int i = 0; i < ESP32C3_UART_COUNT; ++i) { const hwaddr uart_base[] = { DR_REG_UART_BASE, DR_REG_UART1_BASE }; sysbus_realize(SYS_BUS_DEVICE(&ms->uart[i]), &error_fatal); -@@ -641,7 +667,7 @@ static void esp32c3_machine_init(MachineState *machine) +@@ -641,7 +683,7 @@ static void esp32c3_machine_init(MachineState *machine) } /* RGB display realization */ - { -+ if (!ms->xteink_x3) { ++ if (!ms->xteink) { /* Give the internal RAM memory region to the display */ ms->rgb.intram = dram; sysbus_realize(SYS_BUS_DEVICE(&ms->rgb), &error_fatal); -@@ -659,6 +685,42 @@ static void esp32c3_machine_init(MachineState *machine) +@@ -659,6 +701,50 @@ static void esp32c3_machine_init(MachineState *machine) } -+static void xteink_x3_machine_init(MachineState *machine) ++static void xteink_machine_init(MachineState *machine) +{ + Esp32C3MachineState *ms = ESP32C3_MACHINE(machine); -+ ms->xteink_x3 = true; ++ ms->xteink = true; + esp32c3_machine_init(machine); + ++ /* 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. */ ++ if (!ms->x4) { ++ i2c_slave_create_simple(ms->i2c.bus, "xteink-fpchip", 0x55); ++ i2c_slave_create_simple(ms->i2c.bus, "xteink-fpchip", 0x68); ++ i2c_slave_create_simple(ms->i2c.bus, "xteink-fpchip", 0x6B); ++ } ++ + DeviceState *panel = ssi_create_peripheral(ms->spi2.bus, -+ TYPE_XTEINK_X3_EINK); ++ ms->x4 ? TYPE_XTEINK_X4_EINK : TYPE_XTEINK_X3_EINK); + qdev_connect_gpio_out_named(DEVICE(&ms->gpio), ESP32_GPIO_OUTPUT, 21, + qdev_get_gpio_in_named(panel, SSI_GPIO_CS, 0)); + qdev_connect_gpio_out_named(DEVICE(&ms->gpio), ESP32_GPIO_OUTPUT, 4, @@ -548,27 +873,48 @@ index ed698fb..a49eae6 100644 /* Initialize machine type */ static void esp32c3_machine_class_init(ObjectClass *oc, void *data) { -@@ -683,9 +745,23 @@ static const TypeInfo esp32c3_info = { +@@ -683,9 +769,44 @@ static const TypeInfo esp32c3_info = { .class_init = esp32c3_machine_class_init, }; -+static void xteink_x3_machine_class_init(ObjectClass *oc, void *data) ++static char *xteink_get_variant(Object *obj, Error **errp) +{ -+ MachineClass *mc = MACHINE_CLASS(oc); -+ mc->desc = "xteink X3 (ESP32-C3, UC8253 e-ink)"; -+ mc->init = xteink_x3_machine_init; ++ return g_strdup(ESP32C3_MACHINE(obj)->x4 ? "x4" : "x3"); +} + -+static const TypeInfo xteink_x3_info = { -+ .name = MACHINE_TYPE_NAME("xteink-x3"), ++static void xteink_set_variant(Object *obj, const char *value, Error **errp) ++{ ++ Esp32C3MachineState *ms = ESP32C3_MACHINE(obj); ++ if (g_str_equal(value, "x3")) { ++ ms->x4 = false; ++ } else if (g_str_equal(value, "x4")) { ++ ms->x4 = true; ++ } else { ++ error_setg(errp, "invalid variant '%s' (expected x3 or x4)", value); ++ } ++} ++ ++static void xteink_machine_class_init(ObjectClass *oc, void *data) ++{ ++ MachineClass *mc = MACHINE_CLASS(oc); ++ mc->desc = "xteink X3/X4 (ESP32-C3); variant=x3|x4"; ++ mc->init = xteink_machine_init; ++ object_class_property_add_str(oc, "variant", ++ xteink_get_variant, xteink_set_variant); ++ object_class_property_set_description(oc, "variant", ++ "xteink model: x3 (default) or x4"); ++} ++ ++static const TypeInfo xteink_info = { ++ .name = MACHINE_TYPE_NAME("xteink"), + .parent = TYPE_ESP32C3_MACHINE, -+ .class_init = xteink_x3_machine_class_init, ++ .class_init = xteink_machine_class_init, +}; + static void esp32c3_machine_type_init(void) { type_register_static(&esp32c3_info); -+ type_register_static(&xteink_x3_info); ++ type_register_static(&xteink_info); } type_init(esp32c3_machine_type_init); @@ -785,10 +1131,10 @@ index 0000000..1b914e1 +}; diff --git i/include/hw/display/xteink_x3_eink.h w/include/hw/display/xteink_x3_eink.h new file mode 100644 -index 0000000..64de018 +index 0000000..4adc5fc --- /dev/null +++ w/include/hw/display/xteink_x3_eink.h -@@ -0,0 +1,29 @@ +@@ -0,0 +1,30 @@ +#pragma once + +#include "hw/ssi/ssi.h" @@ -796,6 +1142,7 @@ index 0000000..64de018 +#include "ui/console.h" + +#define TYPE_XTEINK_X3_EINK "xteink-x3-eink" ++#define TYPE_XTEINK_X4_EINK "xteink-x4-eink" +OBJECT_DECLARE_SIMPLE_TYPE(XteinkX3EinkState, XTEINK_X3_EINK) + +#define XTEINK_X3_EINK_DC "dc" @@ -843,6 +1190,18 @@ index 163b41a..e2f52ad 100644 } Esp32GpioState; typedef struct Esp32GpioClass { +diff --git i/include/hw/i2c/esp32_i2c.h w/include/hw/i2c/esp32_i2c.h +index b17dc75..42f30c3 100644 +--- i/include/hw/i2c/esp32_i2c.h ++++ w/include/hw/i2c/esp32_i2c.h +@@ -24,6 +24,7 @@ typedef struct Esp32I2CState { + Fifo8 rx_fifo; + Fifo8 tx_fifo; + bool trans_ongoing; ++ bool c3; + + uint32_t ctr_reg; + uint32_t timeout_reg; diff --git i/include/hw/ssi/esp32c3_spi2.h w/include/hw/ssi/esp32c3_spi2.h new file mode 100644 index 0000000..2155c36 diff --git a/scripts/build-qemu.sh b/scripts/build-qemu.sh index a5c108e..152a147 100755 --- a/scripts/build-qemu.sh +++ b/scripts/build-qemu.sh @@ -4,7 +4,8 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" SRC="${QEMU_SRC:-$ROOT/_scratch/qemu-src}" TAG="esp-develop-9.2.2-20260417" -PATCH="$ROOT/qemu/patches/0001-xteink-x3-machine.patch" +COMMIT="40edccac415693c5130f91c01d84176ae6008566" +PATCH="$ROOT/qemu/patches/0001-xteink-machine.patch" JOBS="${JOBS:-2}" if [ ! -d "$SRC/.git" ]; then @@ -12,6 +13,11 @@ if [ ! -d "$SRC/.git" ]; then fi cd "$SRC" +if [ "$(git rev-parse HEAD)" != "$COMMIT" ]; then + echo "QEMU source must be $COMMIT ($TAG)" >&2 + exit 1 +fi + if git apply --check "$PATCH" 2>/dev/null; then git apply "$PATCH" elif ! git apply --reverse --check "$PATCH" 2>/dev/null; then diff --git a/scripts/qmp-qom.py b/scripts/qmp-qom.py index 9f91411..7395c14 100755 --- a/scripts/qmp-qom.py +++ b/scripts/qmp-qom.py @@ -19,8 +19,12 @@ def cmd(command): cmd({"execute": "qmp_capabilities"}) op, path, prop = sys.argv[2], sys.argv[3], sys.argv[4] if op == "set": - print(cmd({"execute": "qom-set", "arguments": { - "path": path, "property": prop, "value": int(sys.argv[5])}})) + raw_value = sys.argv[5] + value = raw_value.lower() == "true" if raw_value.lower() in ("true", "false") else int(raw_value) + response = cmd({"execute": "qom-set", "arguments": { + "path": path, "property": prop, "value": value}}) + if "error" in response: + print(response) else: print(cmd({"execute": "qom-get", "arguments": { "path": path, "property": prop}}))