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:
+48
-58
@@ -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` |
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
+31
-26
@@ -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) | `<canvas>` from the same display surface |
|
||||
| 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) |
|
||||
| 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; `<canvas>`
|
||||
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 |
|
||||
|
||||
|
||||
+394
-35
@@ -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
|
||||
@@ -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
|
||||
|
||||
+6
-2
@@ -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}}))
|
||||
|
||||
Reference in New Issue
Block a user