initial commit
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
dist/
|
||||
firmware.bin
|
||||
firmware.elf
|
||||
flash16.bin
|
||||
flash.bin
|
||||
*.png
|
||||
*.ppm
|
||||
result
|
||||
_scratch/
|
||||
sd.img
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
# Handoff — xteink-web-emulator
|
||||
|
||||
Everything below is hard-won context from the session that built the native X3
|
||||
emulator. If it isn't in this repo or this doc, it's gone. Read this before
|
||||
touching QEMU internals. Pairs with `ROADMAP.md` (the plan) — this is the
|
||||
"how it actually works and what will bite you" file.
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## 1. Environment / hardware realities (READ FIRST)
|
||||
|
||||
- **The dev box is ~15 yr old, 4 GB RAM.** A full QEMU build is ~1660 objects
|
||||
and takes a long time. **Always `-j2`** (more OOMs the linker) and use
|
||||
**very long timeouts (I used 7200 s)** for any `ninja`/`pio`/`nix build`.
|
||||
Do not treat a long-running build as hung.
|
||||
- **First full QEMU build ≈ many minutes; incremental after a patch edit is
|
||||
seconds** (only the touched `.c` + relink). Iterate incrementally; never
|
||||
`rm -rf build`.
|
||||
- **Nix sandbox capability leak:** running FHS/bwrap-wrapped tools (PlatformIO,
|
||||
and the prebuilt release QEMU) fails with
|
||||
`bwrap: Unexpected capabilities but not setuid, old file caps config?`.
|
||||
Workaround — clear ambient caps:
|
||||
```sh
|
||||
SP=$(nix build nixpkgs#util-linux.bin --no-link --print-out-paths)/bin/setpriv
|
||||
"$SP" --inh-caps=-all --ambient-caps=-all nix develop <flake> --command <cmd>
|
||||
```
|
||||
Our **source-built** QEMU is a normal nix-linked ELF and does NOT need this
|
||||
(that's a bonus reason we build from source, not just to patch).
|
||||
|
||||
---
|
||||
|
||||
## 2. Building espressif-qemu from source (the fiddly part)
|
||||
|
||||
`scripts/build-qemu.sh` encodes all of this, but here's *why*:
|
||||
|
||||
- **Reuse nixpkgs' QEMU build env**: `nix develop nixpkgs#qemu --command …`
|
||||
(or the repo devShell, which has `inputsFrom = [ pkgs.qemu ]`). Getting glib/
|
||||
pixman/meson/ninja right by hand is painful; borrow qemu's.
|
||||
- **slirp**: the fork *unconditionally compiles* `net/slirp.c` even with
|
||||
`--disable-slirp` (build-system bug), so you must **enable** it. nixpkgs
|
||||
provides libslirp; `--enable-slirp` + libslirp on `PKG_CONFIG_PATH` works, or
|
||||
it falls back to the bundled subproject.
|
||||
- **libgcrypt**: the ESP32-C3 crypto devices (`esp32c3_aes.c`, rsa, ds, xts) are
|
||||
gated on `gcrypt.found()`. QEMU prefers gnutls and **skips gcrypt unless you
|
||||
pass `--enable-gcrypt`**. Without it the build succeeds but the machine
|
||||
**fails at runtime**: `unknown type 'misc.esp32c3.aes'`.
|
||||
- **Meson caches `pkg_config_path`**: exporting `PKG_CONFIG_PATH` after the
|
||||
first `configure` is ignored on reconfigure. Use
|
||||
`meson configure build -Dpkg_config_path=…` or pass it to `./configure`.
|
||||
- Config that works: `--target-list=riscv32-softmmu --enable-gcrypt
|
||||
--enable-slirp --disable-werror --disable-docs --disable-tools`.
|
||||
- **`/tmp/qsrc` is where I built it this session and is EPHEMERAL** (lost on
|
||||
reboot). The patch is the durable artifact; `make qemu` reclones+rebuilds.
|
||||
|
||||
---
|
||||
|
||||
## 3. How espressif-qemu's esp32c3 machine is structured
|
||||
|
||||
- File: `hw/riscv/esp32c3.c`. It hand-builds the SoC (no device tree).
|
||||
- **Catch-all MMIO**: `esp32c3_io_ops` (`esp32c3_io_read`/`write`) covers the
|
||||
whole peripheral window from `ESP32C3_IO_START_ADDR` (== `DR_REG_UART_BASE` ==
|
||||
`0x60000000`) for `0xd1000` bytes. **Any peripheral without a dedicated
|
||||
device falls here and reads 0 / ignores writes.** That's why unmodeled
|
||||
peripherals silently misbehave instead of faulting.
|
||||
- Turn on `#define ESP32C3_IO_WARNING 1` (top of the file) to log every
|
||||
unsupported access as `[ESP32-C3] Unsupported read/write to $ADDR`. **This is
|
||||
the single most useful debugging lever.** It's noisy (megabytes/sec), so run
|
||||
~12 s and bucket by address:
|
||||
```sh
|
||||
grep 'Unsupported read' log | sed -E 's/.*\$//' | sort | uniq -c | sort -nr | head
|
||||
```
|
||||
The hottest address is almost always the next thing to model. Leave it `0` in
|
||||
committed code.
|
||||
- Devices that **already exist** in the fork: UART, GPIO (barebones — we fleshed
|
||||
it out), SPI1 (flash only), GDMA, AES/SHA/RSA/HMAC/DS/XTS, RTC_CNTL, systimer,
|
||||
timg, efuse, intmatrix, cache, JTAG, TWAI, `esp_rgb` (a QEMU-specific virtual
|
||||
framebuffer, NOT real X3 hardware — we disable it on the xteink machine).
|
||||
- Devices that **did NOT exist** and we added: SAR ADC, SPI2, the UC8253 panel,
|
||||
and real GPIO register behavior. There is **no I²C controller model** — this
|
||||
matters for X3/X4 detection (see §7).
|
||||
|
||||
---
|
||||
|
||||
## 4. Peripheral register facts (discovered, verified against firmware)
|
||||
|
||||
Base addresses from ESP-IDF `soc/esp32c3` headers; all confirmed live.
|
||||
|
||||
### SAR ADC — `DR_REG_APB_SARADC_BASE = 0x60040000`
|
||||
- The **original boot blocker**: firmware polls `INT_RAW` (`+0x44`) bit 31
|
||||
(`ADC1_ONESHOT_DONE`) after starting a conversion; the unmodeled reg returned
|
||||
0 forever → busy-loop with interrupts off → **interrupt-WDT panic**
|
||||
(`Guru Meditation … Interrupt wdt timeout`). The panic dump's `S0/FP =
|
||||
0x60040000` literally pointed at the ADC base.
|
||||
- Model (`hw/adc/esp32c3_adc.c`): `INT_RAW` always returns done bits 31|30;
|
||||
`ONETIME_SAMPLE` (`+0x20`) bits [28:25] select the channel; `1_DATA_STATUS`
|
||||
(`+0x2c`) / `2_DATA_STATUS` (`+0x30`) return the selected channel's 12-bit
|
||||
value. Per-channel values are QOM props `adci[*]`, default `0xfff`.
|
||||
- `adc_oneshot_ll_get_raw_result` reads `adc1_data & 0xfff`.
|
||||
|
||||
### SPI2 (display + SD) — `DR_REG_SPI2_BASE = 0x60024000`
|
||||
- **Completely different register map from SPI1** (`hw/ssi/esp32c3_spi.c` is
|
||||
flash-specific — do NOT reuse it). GP-SPI2 layout: `CMD 0x00` (USR = bit24,
|
||||
UPDATE = bit23), `MS_DLEN 0x1c` (bits[17:0] = bitlen-1), data buffer
|
||||
`W0..W15` at `0x98..0xd4`.
|
||||
- Model (`hw/ssi/esp32c3_spi2.c`): on `CMD` write with USR set, clock
|
||||
`(MS_DLEN/8)+1` bytes out of W0.. onto an `SSIBus` via `ssi_transfer`, writing
|
||||
RX back into the same words. **DMA path is intentionally unimplemented** — the
|
||||
firmware's CPU-buffer path is enough; add DMA only if a future firmware needs
|
||||
it (it'll show up as SPI2 traffic that the CPU-buffer model can't explain).
|
||||
|
||||
### GPIO — `DR_REG_GPIO_BASE = 0x60004000`
|
||||
- The stock model only returned the strap register. We added real
|
||||
`OUT 0x04` / `OUT_W1TS 0x08` / `OUT_W1TC 0x0c` / `ENABLE 0x20` (+W1TS/W1TC) /
|
||||
`IN 0x3c`, with 32 named input + output qemu_irq lines
|
||||
(`ESP32_GPIO_INPUT`/`ESP32_GPIO_OUTPUT`).
|
||||
- **CRITICAL SUBTLETY**: an output line must read **HIGH when the pin is
|
||||
configured as input** (`output_enable` bit clear), not follow the (zero) OUT
|
||||
latch. Real boards have pull-ups; the X3 display **CS is pulled high** while
|
||||
SD is being probed. Without this, CS floated LOW during SD init and the panel
|
||||
greedily consumed the SD command bytes (`FF … 40 00 … 95`) as garbage
|
||||
commands. This one bit cost real debugging time.
|
||||
|
||||
### SD-over-SPI — QEMU's `ssi-sd` + `sd-card-spi`
|
||||
- These exist upstream; we attach them to SPI2 at CS12. But the 9.2 fork's
|
||||
`ssi-sd.c` has a **bug**: CMD58 (read OCR) hardcodes the R1 byte to `0x01`
|
||||
(idle) even after ACMD41 reported ready → SdFat rejects the card → firmware
|
||||
shows "SD card error". Fix: track `idle` state, clear it when ACMD41 returns
|
||||
0, return it for CMD58. (Upstream qemu master rewrote this path entirely; we
|
||||
did the minimal equivalent + a vmstate version bump to 8.)
|
||||
|
||||
---
|
||||
|
||||
## 5. The e-ink panel model (`hw/display/xteink_x3_eink.c`)
|
||||
|
||||
- X3 uses a **UC8253** controller, 792×528, 99 bytes/row, **52 272 bytes/plane**.
|
||||
- Protocol (from `chip/eink-x3.chip.c`, the old Wokwi model — keep it as the
|
||||
reference): DC low = command, DC high = data. `0x10`=DTM1 (old plane),
|
||||
`0x13`=DTM2 (new plane), `0x12`=refresh (render + pulse BUSY), `0x04`/`0x02`=
|
||||
power on/off (pulse BUSY). Bit 1 = white, 0 = black. Rows arrive
|
||||
**bottom-to-top** (wire row i → framebuffer row H-1-i).
|
||||
- BUSY is a GPIO output from the panel back into GPIO6 (active low; we pulse it
|
||||
low for 5 ms of virtual time via a QEMUTimer, then high).
|
||||
- **Rendered to a QEMU graphical console** (not a custom file callback) so the
|
||||
WASM front-end reuses QEMU's display surface. Capture headless with QMP
|
||||
`screendump` → PPM.
|
||||
- **Orientation**: the controller framebuffer (792×528 landscape) is **rotated
|
||||
90° clockwise** to the physical portrait panel (**528×792**). We bake that
|
||||
into the render loop (`screen_x = H-1-y`, `screen_y = x`) so the console is
|
||||
already physical-orientation. Verified by eye: text reads upright at 528×792.
|
||||
- **SSI gotcha**: a `TYPE_SSI_PERIPHERAL` subclass **must set `ssi->realize`**
|
||||
even if empty — the base class calls it unconditionally and a null pointer
|
||||
segfaults at machine init. Also set `cs_polarity = SSI_CS_LOW` and
|
||||
`user_creatable = false`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Machine wiring (`xteink_x3_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;
|
||||
- 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);
|
||||
- **defaults both CS lines high** at init (`qemu_set_irq(cs, 1)`) so nothing is
|
||||
selected before the firmware drives GPIO.
|
||||
|
||||
Pin facts (X3, from `freeink-sdk` `BoardConfig.h` `XTEINK_X3`): display
|
||||
SCLK8/MOSI10/CS21/DC4/RST5/BUSY6; SD shares SCLK8/MOSI10, MISO7, CS12; button
|
||||
ADC ladders on GPIO1 & GPIO2; power button GPIO3; UC8253 @ 16 MHz.
|
||||
|
||||
---
|
||||
|
||||
## 7. X3 vs X4 detection — the thing that wasted time
|
||||
|
||||
- **Both X3 and X4 ship in ONE ESP32-C3 binary.** Which panel/battery profile is
|
||||
used is decided **at runtime** by an I²C fingerprint on SDA20/SCL0: presence of
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
```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
|
||||
```
|
||||
(Reverse-check on the dirty tree proves patch == pristine→current diff, which
|
||||
is equivalent to forward-applying to pristine.)
|
||||
- Firmware/SD/flash images and `/tmp/qsrc` are all disposable; `make qemu` +
|
||||
`make firmware sdimage` rebuild them.
|
||||
|
||||
---
|
||||
|
||||
## 9. Debugging workflow cheat-sheet
|
||||
|
||||
- **Headless run + capture** (canonical loop this session):
|
||||
```sh
|
||||
qemu-system-riscv32 -machine xteink-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
|
||||
scripts/qmp-screendump.py /tmp/q.sock out.ppm
|
||||
```
|
||||
- **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.**
|
||||
- **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).
|
||||
- **Symbolize a panic PC**: `llvm-addr2line -f -C -e firmware.elf 0xADDR`
|
||||
(ignore DWARF "premature terminator" warnings).
|
||||
- **Flash images must be 2/4/8/16 MB.** The partition table spans 16 MB (app is
|
||||
<4 MB), so always pad to 16 MB (`truncate -s 16M`); a mismatched size errors
|
||||
with "only 2, 4, 8, and 16MB images are supported".
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
before boot. Must run fully from static assets (airgapped).
|
||||
|
||||
---
|
||||
|
||||
## 11. Key paths
|
||||
|
||||
| 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 |
|
||||
| espressif/qemu tag | `esp-develop-9.2.2-20260417` |
|
||||
| qemu-wasm | `github.com/ktock/qemu-wasm` |
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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
|
||||
QEMU_SRC ?= _scratch/qemu-src
|
||||
QEMU_BIN = $(QEMU_SRC)/build/qemu-system-riscv32
|
||||
|
||||
.PHONY: firmware sdimage qemu run run-upstream chip clean
|
||||
|
||||
firmware:
|
||||
esptool --chip esp32c3 merge-bin -o flash.bin \
|
||||
0x0 $(FIRMWARE_DIR)/bootloader.bin \
|
||||
0x8000 $(FIRMWARE_DIR)/partitions.bin \
|
||||
0x10000 $(FIRMWARE_DIR)/firmware.bin
|
||||
truncate -s 16M flash.bin
|
||||
cp $(FIRMWARE_DIR)/firmware.elf firmware.elf
|
||||
|
||||
# Local-disk implementation of the SD block interface.
|
||||
sdimage:
|
||||
scripts/mksd.sh $(SD_SRC) sd.img
|
||||
|
||||
qemu:
|
||||
QEMU_SRC=$(QEMU_SRC) scripts/build-qemu.sh
|
||||
|
||||
# Patched xteink X3 machine. Add `-display none` for a headless run.
|
||||
run: qemu
|
||||
$(QEMU_BIN) -machine xteink-x3 -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)
|
||||
|
||||
# Unpatched release binary retained as the fast generic ESP32-C3 baseline.
|
||||
run-upstream:
|
||||
nix run .#run-upstream -- flash.bin
|
||||
|
||||
# Legacy Wokwi protocol model; retained as a small host-tested reference.
|
||||
chip:
|
||||
$(MAKE) -C chip
|
||||
|
||||
clean:
|
||||
$(MAKE) -C chip clean
|
||||
rm -f flash.bin flash16.bin firmware.elf
|
||||
@@ -0,0 +1,43 @@
|
||||
# xteink-web-emulator
|
||||
|
||||
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)
|
||||
port is next. See [ROADMAP.md](./ROADMAP.md) for architecture and plan.
|
||||
|
||||
## Quick start (native QEMU)
|
||||
|
||||
```sh
|
||||
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)
|
||||
```
|
||||
|
||||
Point at a firmware build with `make firmware FIRMWARE_DIR=/path/to/.pio/build/gh_release`.
|
||||
`make qemu` is a long compile on modest hardware — set `JOBS` to taste.
|
||||
|
||||
## 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`.
|
||||
- `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`.
|
||||
- `scripts/mksd.sh` — build the FAT32 SD backing image (local-disk impl of the
|
||||
SD block interface).
|
||||
- `chip/eink-x3.chip.c` — the reverse-engineered X3 e-ink protocol, first written
|
||||
as a Wokwi chip and now the reference for the QEMU display device.
|
||||
`make -C chip test` runs its host self-check.
|
||||
|
||||
## Why not Wokwi
|
||||
|
||||
Wokwi runs any bin in a browser and we had the e-ink chip working — but its
|
||||
simulation engine is proprietary and gated behind a license/service, so it can't
|
||||
be self-hosted or run airgapped. QEMU is the open path; the e-ink protocol work
|
||||
carried straight over.
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# xteink-web-emulator — architecture & roadmap
|
||||
|
||||
**Goal:** an offline, static-asset web page where you upload an xteink X3/X4
|
||||
firmware `.bin` and it runs *client-side* — real ESP32-C3 machine code, with the
|
||||
e-ink screen on a canvas, on-screen buttons, and a virtual SD card. No server,
|
||||
no proprietary engine.
|
||||
|
||||
## Why QEMU (and why not the alternatives)
|
||||
|
||||
| Approach | Runs any `.bin` | Open source | Offline in browser |
|
||||
|----------|:---:|:---:|:---:|
|
||||
| **Wokwi** | ✅ | ❌ (closed engine, licensed) | ❌ |
|
||||
| **Recompile firmware → WASM** (crosspoint-simulator + Emscripten) | ❌ source only | ✅ | ✅ |
|
||||
| **QEMU (espressif fork) → WASM** | ✅ | ✅ | ✅ |
|
||||
|
||||
Only QEMU satisfies all three. `espressif/qemu` models the ESP32-C3 SoC and
|
||||
executes real firmware; `ktock/qemu-wasm` shows QEMU compiling to WASM and
|
||||
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:
|
||||
|
||||
```sh
|
||||
make qemu # clone espressif/qemu @ pinned tag, apply 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)
|
||||
```
|
||||
|
||||
Screenshots (via QMP `screendump`) confirmed: boot → home → Browse Files.
|
||||
|
||||
### 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.
|
||||
|
||||
## The interfaces (native now → browser later)
|
||||
|
||||
Each peripheral is modelled behind a QEMU-native seam that the WASM front-end
|
||||
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 |
|
||||
| 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 |
|
||||
| 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`)
|
||||
|
||||
- **`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).
|
||||
- **`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.
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
## Repo layout
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `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) |
|
||||
| `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 |
|
||||
|
||||
## Debugging notes
|
||||
|
||||
- Headless capture: run with `-qmp unix:…` and use `scripts/qmp-screendump.py`;
|
||||
the console is 528×792 (physical portrait), rotate not needed.
|
||||
- Inject a button: `scripts/qmp-qom.py <sock> set /machine/adc "adci[1]" 2760`
|
||||
(ladder-1 midpoints: Back 3512 / Confirm 2694 / Left 1493 / Right ~5; ladder-2
|
||||
Up 2242 / Down ~5). Value > 3900 = no button.
|
||||
- Symbolize a panic PC: `llvm-addr2line -f -C -e firmware.elf 0x4210d286`.
|
||||
- Flash must be 16 MB — the partition table spans 16 MB even though the app fits
|
||||
under 4 MB. Espressif QEMU only accepts 2/4/8/16 MB mtd images.
|
||||
- Trace SD/panel: flip `DEBUG_SSI_SD` in `hw/sd/ssi-sd.c` or add an
|
||||
`info_report` in `xteink_x3_eink.c` (both off by default in the patch).
|
||||
@@ -0,0 +1,23 @@
|
||||
TARGET = dist/eink-x3.chip.wasm
|
||||
SOURCES = eink-x3.chip.c
|
||||
|
||||
.PHONY: all clean test
|
||||
all: $(TARGET) dist/eink-x3.chip.json
|
||||
|
||||
test: dist
|
||||
$(CC) -O2 -Wall -o dist/test test/test.c
|
||||
./dist/test
|
||||
|
||||
dist:
|
||||
mkdir -p dist
|
||||
|
||||
$(TARGET): dist $(SOURCES) wokwi-api.h
|
||||
clang --target=wasm32 -nostdlib -O2 -Wall -Werror -Wno-unused-function \
|
||||
-Wl,--no-entry -Wl,--import-memory -Wl,--export-table -Wl,--export-dynamic \
|
||||
-o $(TARGET) $(SOURCES)
|
||||
|
||||
dist/eink-x3.chip.json: dist eink-x3.chip.json
|
||||
cp eink-x3.chip.json dist/
|
||||
|
||||
clean:
|
||||
rm -rf dist
|
||||
@@ -0,0 +1,157 @@
|
||||
// xteink X3 e-ink panel model for Wokwi.
|
||||
//
|
||||
// The X3 firmware drives a UC8179-style controller (see EInkDisplay.cpp
|
||||
// setDisplayX3 / displayBuffer). This chip reconstructs the intended image
|
||||
// from the SPI stream and paints it to a Wokwi framebuffer.
|
||||
//
|
||||
// Protocol facts this relies on (from the driver, not a datasheet):
|
||||
// - SPI mode 0, MSB first. DC low = command byte, DC high = data byte.
|
||||
// - Pixel planes are streamed after command 0x13 (new frame), 99 bytes/row,
|
||||
// bottom-to-top (row i on the wire = framebuffer row H-1-i).
|
||||
// - Bit = 1 -> white, bit = 0 -> black (0xFF = white byte).
|
||||
// - Full-sync writes the plane inverted and flags it with 0x50 payload 0xA9;
|
||||
// fast refresh writes it upright with 0x50 payload 0x29.
|
||||
// - Command 0x12 triggers the refresh; BUSY is active LOW (idle HIGH).
|
||||
//
|
||||
// Commands 0x20-0x24 are LUT registers here (not RAM/activation), so they are
|
||||
// ignored. Only 0x13 (plane), 0x50 (polarity), and 0x12/0x04/0x02 (refresh/
|
||||
// power -> BUSY pulse) matter for rendering.
|
||||
|
||||
#include "wokwi-api.h"
|
||||
|
||||
#define WIDTH 792
|
||||
#define HEIGHT 528
|
||||
#define WIDTH_BYTES (WIDTH / 8) // 99
|
||||
#define PLANE_SIZE (WIDTH_BYTES * HEIGHT) // 52272
|
||||
|
||||
#define CMD_NEW_PLANE 0x13
|
||||
#define CMD_POLARITY 0x50
|
||||
#define CMD_REFRESH 0x12
|
||||
#define CMD_POWER_ON 0x04
|
||||
#define CMD_POWER_OFF 0x02
|
||||
|
||||
#define BUSY_PULSE_US 5000
|
||||
#define SPI_CHUNK 4096
|
||||
|
||||
typedef struct {
|
||||
pin_t cs, dc, busy;
|
||||
spi_dev_t spi;
|
||||
buffer_t fb;
|
||||
timer_t busy_timer;
|
||||
uint8_t spi_buf[SPI_CHUNK];
|
||||
|
||||
bool span_is_command; // this CS-low span began with DC low
|
||||
uint32_t span_pos; // byte index within the current span
|
||||
uint8_t cmd; // last command byte
|
||||
uint32_t plane_pos; // write cursor into plane[]
|
||||
bool invert; // recovered from the 0x50 polarity selector
|
||||
uint8_t plane[PLANE_SIZE];
|
||||
} chip_state_t;
|
||||
|
||||
static chip_state_t chip;
|
||||
|
||||
static void render(void) {
|
||||
static uint8_t row_rgba[WIDTH * 4];
|
||||
for (uint32_t i = 0; i < HEIGHT; i++) {
|
||||
const uint32_t y = HEIGHT - 1 - i; // un-mirror: wire is bottom-to-top
|
||||
const uint8_t *src = &chip.plane[i * WIDTH_BYTES];
|
||||
uint32_t o = 0;
|
||||
for (uint32_t bx = 0; bx < WIDTH_BYTES; bx++) {
|
||||
uint8_t v = src[bx];
|
||||
if (chip.invert) v = (uint8_t)~v;
|
||||
for (int bit = 0; bit < 8; bit++) {
|
||||
const uint8_t px = (v & (0x80 >> bit)) ? 0xFF : 0x00; // 1=white
|
||||
row_rgba[o++] = px; // R
|
||||
row_rgba[o++] = px; // G
|
||||
row_rgba[o++] = px; // B
|
||||
row_rgba[o++] = 0xFF; // A
|
||||
}
|
||||
}
|
||||
buffer_write(chip.fb, y * WIDTH * 4, row_rgba, WIDTH * 4);
|
||||
}
|
||||
}
|
||||
|
||||
static void pulse_busy(void) {
|
||||
pin_write(chip.busy, LOW);
|
||||
timer_start(chip.busy_timer, BUSY_PULSE_US, false);
|
||||
}
|
||||
|
||||
static void on_busy_timer(void *user_data) {
|
||||
(void)user_data;
|
||||
pin_write(chip.busy, HIGH);
|
||||
}
|
||||
|
||||
static void handle_command(uint8_t b) {
|
||||
chip.cmd = b;
|
||||
chip.span_pos = 1;
|
||||
if (b == CMD_NEW_PLANE) chip.plane_pos = 0;
|
||||
if (b == CMD_REFRESH) render();
|
||||
if (b == CMD_REFRESH || b == CMD_POWER_ON || b == CMD_POWER_OFF) pulse_busy();
|
||||
}
|
||||
|
||||
static void handle_data(uint8_t b) {
|
||||
const uint32_t data_index = chip.span_pos - (chip.span_is_command ? 1 : 0);
|
||||
if (chip.cmd == CMD_NEW_PLANE) {
|
||||
if (chip.plane_pos < PLANE_SIZE) chip.plane[chip.plane_pos++] = b;
|
||||
} else if (chip.cmd == CMD_POLARITY && data_index == 0) {
|
||||
chip.invert = (b == 0xA9);
|
||||
}
|
||||
chip.span_pos++;
|
||||
}
|
||||
|
||||
static void on_spi_done(void *user_data, uint8_t *buffer, uint32_t count) {
|
||||
(void)user_data;
|
||||
if (count == 0) return; // spi_stop with no data
|
||||
|
||||
if (chip.span_is_command) {
|
||||
const uint8_t b = buffer[0];
|
||||
if (chip.span_pos == 0)
|
||||
handle_command(b);
|
||||
else
|
||||
handle_data(b);
|
||||
if (pin_read(chip.cs) == LOW) spi_start(chip.spi, chip.spi_buf, 1);
|
||||
} else {
|
||||
for (uint32_t i = 0; i < count; i++) handle_data(buffer[i]);
|
||||
if (pin_read(chip.cs) == LOW) spi_start(chip.spi, chip.spi_buf, SPI_CHUNK);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_cs_change(void *user_data, pin_t pin, uint32_t value) {
|
||||
(void)user_data;
|
||||
(void)pin;
|
||||
if (value == LOW) {
|
||||
chip.span_is_command = (pin_read(chip.dc) == LOW);
|
||||
chip.span_pos = 0;
|
||||
spi_start(chip.spi, chip.spi_buf, chip.span_is_command ? 1 : SPI_CHUNK);
|
||||
} else {
|
||||
spi_stop(chip.spi);
|
||||
}
|
||||
}
|
||||
|
||||
void chip_init(void) {
|
||||
const pin_t sck = pin_init("SCLK", INPUT);
|
||||
const pin_t mosi = pin_init("MOSI", INPUT);
|
||||
chip.cs = pin_init("CS", INPUT_PULLUP);
|
||||
chip.dc = pin_init("DC", INPUT);
|
||||
pin_init("RST", INPUT);
|
||||
chip.busy = pin_init("BUSY", OUTPUT_HIGH);
|
||||
|
||||
uint32_t w = 0, h = 0;
|
||||
chip.fb = framebuffer_init(&w, &h);
|
||||
|
||||
const spi_config_t spi_cfg = {
|
||||
.sck = sck,
|
||||
.mosi = mosi,
|
||||
.miso = NO_PIN,
|
||||
.mode = 0,
|
||||
.done = on_spi_done,
|
||||
.user_data = &chip,
|
||||
};
|
||||
chip.spi = spi_init(&spi_cfg);
|
||||
|
||||
const timer_config_t timer_cfg = {.callback = on_busy_timer, .user_data = &chip};
|
||||
chip.busy_timer = timer_init(&timer_cfg);
|
||||
|
||||
const pin_watch_config_t cs_watch = {.edge = BOTH, .pin_change = on_cs_change, .user_data = &chip};
|
||||
pin_watch(chip.cs, &cs_watch);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "xteink X3 e-ink",
|
||||
"author": "xteink-web-emulator",
|
||||
"license": "MIT",
|
||||
"pins": ["SCLK", "MOSI", "CS", "DC", "RST", "BUSY", "VCC", "GND"],
|
||||
"display": {
|
||||
"width": 792,
|
||||
"height": 528
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Host self-check for the plane reconstruction (mirror + invert + bit unpack).
|
||||
// Drives the chip's command/data handlers with a full-sync stream built the way
|
||||
// EInkDisplay::displayBuffer emits it, then asserts the framebuffer matches the
|
||||
// source image. Run: make -C chip test
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define W 792
|
||||
#define H 528
|
||||
#define WB (W / 8)
|
||||
|
||||
static uint8_t g_fb[W * H * 4];
|
||||
|
||||
// Minimal Wokwi API mocks (chip only calls these at runtime).
|
||||
typedef int32_t pin_t;
|
||||
typedef uint32_t spi_dev_t;
|
||||
typedef uint32_t timer_t;
|
||||
typedef uint32_t buffer_t;
|
||||
typedef struct { void *user_data; uint32_t edge; void (*pin_change)(void *, pin_t, uint32_t); } pin_watch_config_t;
|
||||
typedef struct { void *user_data; pin_t sck, mosi, miso; uint32_t mode; void (*done)(void *, uint8_t *, uint32_t); uint32_t reserved[8]; } spi_config_t;
|
||||
typedef struct { void *user_data; void (*callback)(void *); uint32_t reserved[8]; } timer_config_t;
|
||||
enum { LOW = 0, HIGH = 1, INPUT = 0, OUTPUT = 1, INPUT_PULLUP = 2, OUTPUT_HIGH = 17, BOTH = 3 };
|
||||
#define NO_PIN ((pin_t)-1)
|
||||
|
||||
static pin_t pin_init(const char *n, uint32_t m) { (void)n; (void)m; return 0; }
|
||||
static uint32_t pin_read(pin_t p) { (void)p; return HIGH; }
|
||||
static void pin_write(pin_t p, uint32_t v) { (void)p; (void)v; }
|
||||
static int pin_watch(pin_t p, const pin_watch_config_t *c) { (void)p; (void)c; return 1; }
|
||||
static spi_dev_t spi_init(const spi_config_t *c) { (void)c; return 0; }
|
||||
static void spi_start(spi_dev_t s, uint8_t *b, uint32_t n) { (void)s; (void)b; (void)n; }
|
||||
static void spi_stop(spi_dev_t s) { (void)s; }
|
||||
static timer_t timer_init(const timer_config_t *c) { (void)c; return 0; }
|
||||
static void timer_start(timer_t t, uint32_t us, int r) { (void)t; (void)us; (void)r; }
|
||||
static buffer_t framebuffer_init(uint32_t *w, uint32_t *h) { *w = W; *h = H; return 1; }
|
||||
static void buffer_write(buffer_t b, uint32_t off, uint8_t *d, uint32_t n) { (void)b; memcpy(g_fb + off, d, n); }
|
||||
|
||||
#define WOKWI_API_H
|
||||
#include "../eink-x3.chip.c"
|
||||
|
||||
static void feed_command(uint8_t c) { chip.span_is_command = true; handle_command(c); }
|
||||
static void feed_data(uint8_t d) { handle_data(d); }
|
||||
|
||||
int main(void) {
|
||||
chip.fb = framebuffer_init(&(uint32_t){0}, &(uint32_t){0});
|
||||
|
||||
// Source image: white, with a few black pixels at known coordinates.
|
||||
static uint8_t src[WB * H];
|
||||
memset(src, 0xFF, sizeof(src));
|
||||
const int bx[] = {16, 0, 791, 400};
|
||||
const int by[] = {3, 0, 527, 260};
|
||||
for (int k = 0; k < 4; k++)
|
||||
src[by[k] * WB + bx[k] / 8] &= (uint8_t)~(0x80 >> (bx[k] % 8));
|
||||
|
||||
// Full-sync stream: 0x13 + inverted, bottom-to-top plane; polarity 0xA9; 0x12.
|
||||
feed_command(0x13);
|
||||
for (int i = 0; i < H; i++) {
|
||||
const int srcY = H - 1 - i;
|
||||
for (int x = 0; x < WB; x++) feed_data((uint8_t)~src[srcY * WB + x]);
|
||||
}
|
||||
feed_command(0x50);
|
||||
feed_data(0xA9);
|
||||
feed_data(0x07);
|
||||
feed_command(0x12);
|
||||
|
||||
for (int y = 0; y < H; y++)
|
||||
for (int x = 0; x < W; x++) {
|
||||
const int black = !(src[y * WB + x / 8] & (0x80 >> (x % 8)));
|
||||
const uint8_t got = g_fb[(y * W + x) * 4];
|
||||
assert(got == (black ? 0x00 : 0xFF));
|
||||
}
|
||||
printf("ok: %dx%d reconstructed, mirror+invert correct\n", W, H);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifndef WOKWI_API_H
|
||||
#define WOKWI_API_H
|
||||
|
||||
enum pin_value {
|
||||
LOW = 0,
|
||||
HIGH = 1
|
||||
};
|
||||
|
||||
enum pin_mode {
|
||||
INPUT = 0,
|
||||
OUTPUT = 1,
|
||||
INPUT_PULLUP = 2,
|
||||
INPUT_PULLDOWN = 3,
|
||||
ANALOG = 4,
|
||||
|
||||
OUTPUT_LOW = 16,
|
||||
OUTPUT_HIGH = 17,
|
||||
};
|
||||
|
||||
enum edge {
|
||||
RISING = 1,
|
||||
FALLING = 2,
|
||||
BOTH = 3,
|
||||
};
|
||||
|
||||
int __attribute__((export_name("__wokwi_api_version_1"))) __attribute__((weak)) __wokwi_api_version_1(void) { return 1; }
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef int32_t pin_t;
|
||||
#define NO_PIN ((pin_t)-1)
|
||||
|
||||
typedef struct {
|
||||
void *user_data;
|
||||
uint32_t edge;
|
||||
void (*pin_change)(void *user_data, pin_t pin, uint32_t value);
|
||||
} pin_watch_config_t;
|
||||
|
||||
extern __attribute__((export_name("chipInit"))) void chip_init(void);
|
||||
|
||||
extern __attribute__((import_name("pinInit"))) pin_t pin_init(const char *name, uint32_t mode);
|
||||
|
||||
extern __attribute__((import_name("pinRead"))) uint32_t pin_read(pin_t pin);
|
||||
extern __attribute__((import_name("pinWrite"))) void pin_write(pin_t pin, uint32_t value);
|
||||
extern __attribute__((import_name("pinWatch"))) bool pin_watch(pin_t pin, const pin_watch_config_t *config);
|
||||
extern __attribute__((import_name("pinWatchStop"))) void pin_watch_stop(pin_t pin);
|
||||
extern __attribute__((import_name("pinMode"))) void pin_mode(pin_t pin, uint32_t value);
|
||||
extern __attribute__((import_name("pinADCRead"))) float pin_adc_read(pin_t pin);
|
||||
extern __attribute__((import_name("pinDACWrite"))) float pin_dac_write(pin_t pin, float voltage);
|
||||
|
||||
extern __attribute__((import_name("attrInit"))) uint32_t attr_init(const char *name, uint32_t default_value);
|
||||
extern __attribute__((import_name("attrInit"))) uint32_t attr_init_float(const char *name, float default_value);
|
||||
extern __attribute__((import_name("attrRead"))) uint32_t attr_read(uint32_t attr_id);
|
||||
extern __attribute__((import_name("attrReadFloat"))) float attr_read_float(uint32_t attr_id);
|
||||
|
||||
typedef struct {
|
||||
void *user_data;
|
||||
uint32_t address;
|
||||
pin_t scl;
|
||||
pin_t sda;
|
||||
bool (*connect)(void *user_data, uint32_t address, bool connect);
|
||||
uint8_t (*read)(void *user_data);
|
||||
bool (*write)(void *user_data, uint8_t data);
|
||||
void (*disconnect)(void *user_data);
|
||||
uint32_t reserved[8];
|
||||
} i2c_config_t;
|
||||
|
||||
typedef uint32_t i2c_dev_t;
|
||||
|
||||
extern __attribute__((import_name("i2cInit"))) i2c_dev_t i2c_init(const i2c_config_t *config);
|
||||
|
||||
typedef struct {
|
||||
void *user_data;
|
||||
pin_t rx;
|
||||
pin_t tx;
|
||||
uint32_t baud_rate;
|
||||
void (*rx_data)(void *user_data, uint8_t byte);
|
||||
void (*write_done)(void *user_data);
|
||||
uint32_t reserved[8];
|
||||
} uart_config_t;
|
||||
|
||||
typedef uint32_t uart_dev_t;
|
||||
|
||||
extern __attribute__((import_name("uartInit"))) uart_dev_t uart_init(const uart_config_t *config);
|
||||
extern __attribute__((import_name("uartWrite"))) bool uart_write(uart_dev_t uart, uint8_t *buffer, uint32_t count);
|
||||
|
||||
typedef struct {
|
||||
void *user_data;
|
||||
pin_t sck;
|
||||
pin_t mosi;
|
||||
pin_t miso;
|
||||
uint32_t mode;
|
||||
void (*done)(void *user_data, uint8_t *buffer, uint32_t count);
|
||||
uint32_t reserved[8];
|
||||
} spi_config_t;
|
||||
typedef uint32_t spi_dev_t;
|
||||
|
||||
extern __attribute__((import_name("spiInit"))) spi_dev_t spi_init(const spi_config_t *spi_config);
|
||||
extern __attribute__((import_name("spiStart"))) void spi_start(const spi_dev_t spi, uint8_t *buffer, uint32_t count);
|
||||
extern __attribute__((import_name("spiStop"))) void spi_stop(const spi_dev_t spi);
|
||||
|
||||
typedef struct {
|
||||
void *user_data;
|
||||
void (*callback)(void *user_data);
|
||||
uint32_t reserved[8];
|
||||
} timer_config_t;
|
||||
|
||||
typedef uint32_t timer_t;
|
||||
|
||||
extern __attribute__((import_name("timerInit"))) timer_t timer_init(const timer_config_t *config);
|
||||
extern __attribute__((import_name("timerStart"))) void timer_start(const timer_t timer, uint32_t micros, bool repeat);
|
||||
extern __attribute__((import_name("timerStartNanos"))) void timer_start_ns_d(const timer_t timer, double nanos, bool repeat);
|
||||
static void timer_start_ns(const timer_t timer, uint64_t nanos, bool repeat) {
|
||||
timer_start_ns_d(timer, (double)nanos, repeat);
|
||||
}
|
||||
extern __attribute__((import_name("timerStop"))) void timer_stop(const timer_t timer);
|
||||
|
||||
extern __attribute__((import_name("getSimNanos"))) double get_sim_nanos_d(void);
|
||||
|
||||
static uint64_t get_sim_nanos(void) {
|
||||
return (uint64_t)get_sim_nanos_d();
|
||||
}
|
||||
|
||||
typedef uint32_t buffer_t;
|
||||
extern __attribute__((import_name("framebufferInit"))) buffer_t framebuffer_init(uint32_t *pixel_width, uint32_t *pixel_height);
|
||||
extern __attribute__((import_name("bufferRead"))) void buffer_read(buffer_t buffer, uint32_t offset, uint8_t *data, uint32_t data_len);
|
||||
extern __attribute__((import_name("bufferWrite"))) void buffer_write(buffer_t buffer, uint32_t offset, uint8_t *data, uint32_t data_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* WOKWI_API_H */
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": 1,
|
||||
"author": "xteink-web-emulator",
|
||||
"editor": "wokwi",
|
||||
"parts": [
|
||||
{ "type": "board-esp32-c3-devkitm-1", "id": "esp", "top": 0, "left": 0, "attrs": { "flashSize": "16" } },
|
||||
{ "type": "chip-eink-x3", "id": "disp", "top": -140, "left": 200, "attrs": {} }
|
||||
],
|
||||
"connections": [
|
||||
["esp:8", "disp:SCLK", "green", []],
|
||||
["esp:10", "disp:MOSI", "blue", []],
|
||||
["esp:TX", "disp:CS", "orange", []],
|
||||
["esp:4", "disp:DC", "purple", []],
|
||||
["esp:5", "disp:RST", "gray", []],
|
||||
["esp:6", "disp:BUSY", "yellow", []],
|
||||
["esp:3V3.1", "disp:VCC", "red", []],
|
||||
["esp:GND.1", "disp:GND", "black", []]
|
||||
],
|
||||
"dependencies": {}
|
||||
}
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1784356753,
|
||||
"narHash": "sha256-12KrbMiWLcf8m7pCvAtZh1ZrgF85ZXDXvfR/fWTKy84=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "61b7c44c4073f0b827768aff0049561b5110ea5a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
description = "Browser emulator for the xteink X3 (ESP32-C3) — QEMU-based, runs any firmware .bin";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self
|
||||
, nixpkgs
|
||||
, flake-utils
|
||||
,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
# Espressif's QEMU fork (ESP32-C3 SoC). Prebuilt generic-linux binary,
|
||||
# made runnable on NixOS with autoPatchelfHook (no FHS/bwrap needed).
|
||||
qemu-esp32c3 = pkgs.stdenv.mkDerivation {
|
||||
pname = "qemu-esp32c3";
|
||||
version = "esp-develop-9.2.2-20260417";
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20260417/qemu-riscv32-softmmu-esp_develop_9.2.2_20260417-x86_64-linux-gnu.tar.xz";
|
||||
hash = "sha256-VH8D4EcBqSy7aZ9/fQFa3B9bXvk8u5TA3ZtxB+LYTnc=";
|
||||
};
|
||||
sourceRoot = "qemu";
|
||||
nativeBuildInputs = [ pkgs.autoPatchelfHook ];
|
||||
buildInputs = with pkgs; [ pixman libgcrypt SDL2 zlib libslirp stdenv.cc.cc.lib ];
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r bin share $out/
|
||||
'';
|
||||
};
|
||||
|
||||
# `nix run .#run-upstream -- flash.bin [extra qemu args]`
|
||||
runScript = pkgs.writeShellScriptBin "xteink-qemu-upstream" ''
|
||||
flash="''${1:?usage: run <flash.bin> [qemu args...]}"; shift || true
|
||||
exec ${qemu-esp32c3}/bin/qemu-system-riscv32 \
|
||||
-machine esp32c3 -L ${qemu-esp32c3}/share/qemu \
|
||||
-nographic -drive file="$flash",if=mtd,format=raw "$@"
|
||||
'';
|
||||
in
|
||||
{
|
||||
packages.qemu-esp32c3 = qemu-esp32c3;
|
||||
packages.default = qemu-esp32c3;
|
||||
|
||||
apps.run-upstream = {
|
||||
type = "app";
|
||||
program = "${runScript}/bin/xteink-qemu-upstream";
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
# Reuse nixpkgs' complete QEMU build environment; Espressif's crypto
|
||||
# devices additionally require libgcrypt.
|
||||
inputsFrom = [ pkgs.qemu ];
|
||||
packages = with pkgs; [
|
||||
qemu-esp32c3
|
||||
esptool
|
||||
gnumake
|
||||
git
|
||||
libgcrypt
|
||||
libslirp
|
||||
dosfstools
|
||||
mtools
|
||||
llvmPackages.clang-unwrapped
|
||||
lld
|
||||
gcc
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,868 @@
|
||||
diff --git i/hw/adc/esp32c3_adc.c w/hw/adc/esp32c3_adc.c
|
||||
new file mode 100644
|
||||
index 0000000..8908742
|
||||
--- /dev/null
|
||||
+++ w/hw/adc/esp32c3_adc.c
|
||||
@@ -0,0 +1,101 @@
|
||||
+/*
|
||||
+ * ESP32-C3 SAR ADC
|
||||
+ *
|
||||
+ * SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+ */
|
||||
+
|
||||
+#include "qemu/osdep.h"
|
||||
+#include "qemu/module.h"
|
||||
+#include "hw/adc/esp32c3_adc.h"
|
||||
+
|
||||
+#define ADC_ONETIME_SAMPLE 0x020
|
||||
+#define ADC_DATA1 0x02c
|
||||
+#define ADC_DATA2 0x030
|
||||
+#define ADC_INT_RAW 0x044
|
||||
+
|
||||
+#define ADC_CHANNEL_SHIFT 25
|
||||
+#define ADC_CHANNEL_MASK 0xf
|
||||
+#define ADC_DONE (BIT(31) | BIT(30))
|
||||
+#define ADC_MAX 0xfff
|
||||
+
|
||||
+static uint64_t esp32c3_adc_read(void *opaque, hwaddr addr, unsigned size)
|
||||
+{
|
||||
+ ESP32C3AdcState *s = ESP32C3_ADC(opaque);
|
||||
+
|
||||
+ if (addr == ADC_INT_RAW) {
|
||||
+ return ADC_DONE;
|
||||
+ }
|
||||
+ if (addr == ADC_DATA1 || addr == ADC_DATA2) {
|
||||
+ return s->channel < ESP32C3_ADC_CHANNELS
|
||||
+ ? MIN(s->input[s->channel], ADC_MAX)
|
||||
+ : ADC_MAX;
|
||||
+ }
|
||||
+ return s->regs[addr / 4];
|
||||
+}
|
||||
+
|
||||
+static void esp32c3_adc_write(void *opaque, hwaddr addr, uint64_t value,
|
||||
+ unsigned size)
|
||||
+{
|
||||
+ ESP32C3AdcState *s = ESP32C3_ADC(opaque);
|
||||
+ s->regs[addr / 4] = value;
|
||||
+
|
||||
+ if (addr == ADC_ONETIME_SAMPLE) {
|
||||
+ s->channel = (value >> ADC_CHANNEL_SHIFT) & ADC_CHANNEL_MASK;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static const MemoryRegionOps esp32c3_adc_ops = {
|
||||
+ .read = esp32c3_adc_read,
|
||||
+ .write = esp32c3_adc_write,
|
||||
+ .endianness = DEVICE_LITTLE_ENDIAN,
|
||||
+ .valid = {
|
||||
+ .min_access_size = 4,
|
||||
+ .max_access_size = 4,
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+static void esp32c3_adc_reset_hold(Object *obj, ResetType type)
|
||||
+{
|
||||
+ ESP32C3AdcState *s = ESP32C3_ADC(obj);
|
||||
+ memset(s->regs, 0, sizeof(s->regs));
|
||||
+ for (int i = 0; i < ESP32C3_ADC_CHANNELS; i++) {
|
||||
+ s->input[i] = ADC_MAX;
|
||||
+ }
|
||||
+ s->channel = 0;
|
||||
+}
|
||||
+
|
||||
+static void esp32c3_adc_init(Object *obj)
|
||||
+{
|
||||
+ ESP32C3AdcState *s = ESP32C3_ADC(obj);
|
||||
+ SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
|
||||
+
|
||||
+ memory_region_init_io(&s->iomem, obj, &esp32c3_adc_ops, s,
|
||||
+ TYPE_ESP32C3_ADC, ESP32C3_ADC_IO_SIZE);
|
||||
+ sysbus_init_mmio(sbd, &s->iomem);
|
||||
+ for (int i = 0; i < ESP32C3_ADC_CHANNELS; i++) {
|
||||
+ object_property_add_uint32_ptr(obj, "adci[*]", &s->input[i],
|
||||
+ OBJ_PROP_FLAG_READWRITE);
|
||||
+ }
|
||||
+ esp32c3_adc_reset_hold(obj, RESET_TYPE_COLD);
|
||||
+}
|
||||
+
|
||||
+static void esp32c3_adc_class_init(ObjectClass *klass, void *data)
|
||||
+{
|
||||
+ ResettableClass *rc = RESETTABLE_CLASS(klass);
|
||||
+ rc->phases.hold = esp32c3_adc_reset_hold;
|
||||
+}
|
||||
+
|
||||
+static const TypeInfo esp32c3_adc_info = {
|
||||
+ .name = TYPE_ESP32C3_ADC,
|
||||
+ .parent = TYPE_SYS_BUS_DEVICE,
|
||||
+ .instance_size = sizeof(ESP32C3AdcState),
|
||||
+ .instance_init = esp32c3_adc_init,
|
||||
+ .class_init = esp32c3_adc_class_init,
|
||||
+};
|
||||
+
|
||||
+static void esp32c3_adc_register_types(void)
|
||||
+{
|
||||
+ type_register_static(&esp32c3_adc_info);
|
||||
+}
|
||||
+
|
||||
+type_init(esp32c3_adc_register_types)
|
||||
diff --git i/hw/adc/meson.build w/hw/adc/meson.build
|
||||
index 7f7acc1..541d5d3 100644
|
||||
--- i/hw/adc/meson.build
|
||||
+++ w/hw/adc/meson.build
|
||||
@@ -1,4 +1,5 @@
|
||||
system_ss.add(when: 'CONFIG_STM32F2XX_ADC', if_true: files('stm32f2xx_adc.c'))
|
||||
+system_ss.add(when: 'CONFIG_RISCV_ESP32C3', if_true: files('esp32c3_adc.c'))
|
||||
system_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: files('aspeed_adc.c'))
|
||||
system_ss.add(when: 'CONFIG_NPCM7XX', if_true: files('npcm7xx_adc.c'))
|
||||
system_ss.add(when: 'CONFIG_ZYNQ', if_true: files('zynq-xadc.c'))
|
||||
diff --git i/hw/display/meson.build w/hw/display/meson.build
|
||||
index 36a9ace..c73f313 100644
|
||||
--- i/hw/display/meson.build
|
||||
+++ w/hw/display/meson.build
|
||||
@@ -32,6 +32,7 @@ system_ss.add(when: 'CONFIG_CG3', if_true: files('cg3.c'))
|
||||
system_ss.add(when: 'CONFIG_MACFB', if_true: files('macfb.c'))
|
||||
system_ss.add(when: 'CONFIG_NEXTCUBE', if_true: files('next-fb.c'))
|
||||
system_ss.add(when: 'CONFIG_ESP_RGB', if_true: files('esp_rgb.c'))
|
||||
+system_ss.add(when: 'CONFIG_RISCV_ESP32C3', if_true: files('xteink_x3_eink.c'))
|
||||
|
||||
system_ss.add(when: 'CONFIG_VGA', if_true: files('vga.c'))
|
||||
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
|
||||
--- /dev/null
|
||||
+++ w/hw/display/xteink_x3_eink.c
|
||||
@@ -0,0 +1,165 @@
|
||||
+/*
|
||||
+ * xteink X3 UC8253 e-ink panel
|
||||
+ *
|
||||
+ * SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+ */
|
||||
+
|
||||
+#include "qemu/osdep.h"
|
||||
+#include "qemu/module.h"
|
||||
+#include "hw/irq.h"
|
||||
+#include "hw/display/xteink_x3_eink.h"
|
||||
+
|
||||
+#define CMD_POWER_OFF 0x02
|
||||
+#define CMD_POWER_ON 0x04
|
||||
+#define CMD_DTM1 0x10
|
||||
+#define CMD_REFRESH 0x12
|
||||
+#define CMD_DTM2 0x13
|
||||
+
|
||||
+#define BUSY_TIME_NS (5 * SCALE_MS)
|
||||
+
|
||||
+static void xteink_x3_eink_render(XteinkX3EinkState *s)
|
||||
+{
|
||||
+ DisplaySurface *surface = qemu_console_surface(s->console);
|
||||
+ uint32_t *pixels = (uint32_t *)surface_data(surface);
|
||||
+
|
||||
+ for (unsigned y = 0; y < XTEINK_X3_HEIGHT; y++) {
|
||||
+ const uint8_t *row = &s->new_plane[(XTEINK_X3_HEIGHT - 1 - y) *
|
||||
+ XTEINK_X3_WIDTH_BYTES];
|
||||
+ for (unsigned x = 0; x < XTEINK_X3_WIDTH; x++) {
|
||||
+ unsigned screen_x = XTEINK_X3_HEIGHT - 1 - y;
|
||||
+ unsigned screen_y = x;
|
||||
+ pixels[screen_y * XTEINK_X3_HEIGHT + screen_x] =
|
||||
+ (row[x / 8] & (0x80 >> (x % 8))) ? 0x00ffffff : 0;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ dpy_gfx_update(s->console, 0, 0, XTEINK_X3_HEIGHT, XTEINK_X3_WIDTH);
|
||||
+}
|
||||
+
|
||||
+static void xteink_x3_eink_busy_done(void *opaque)
|
||||
+{
|
||||
+ XteinkX3EinkState *s = XTEINK_X3_EINK(opaque);
|
||||
+ qemu_set_irq(s->busy, 1);
|
||||
+}
|
||||
+
|
||||
+static void xteink_x3_eink_pulse_busy(XteinkX3EinkState *s)
|
||||
+{
|
||||
+ qemu_set_irq(s->busy, 0);
|
||||
+ timer_mod(s->busy_timer,
|
||||
+ qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + BUSY_TIME_NS);
|
||||
+}
|
||||
+
|
||||
+static void xteink_x3_eink_reset(DeviceState *dev)
|
||||
+{
|
||||
+ XteinkX3EinkState *s = XTEINK_X3_EINK(dev);
|
||||
+
|
||||
+ s->command = 0;
|
||||
+ s->plane_pos = 0;
|
||||
+ memset(s->old_plane, 0xff, sizeof(s->old_plane));
|
||||
+ memset(s->new_plane, 0xff, sizeof(s->new_plane));
|
||||
+ qemu_set_irq(s->busy, 1);
|
||||
+ xteink_x3_eink_render(s);
|
||||
+}
|
||||
+
|
||||
+static void xteink_x3_eink_set_dc(void *opaque, int n, int level)
|
||||
+{
|
||||
+ XteinkX3EinkState *s = XTEINK_X3_EINK(opaque);
|
||||
+ s->dc = level;
|
||||
+}
|
||||
+
|
||||
+static void xteink_x3_eink_set_reset(void *opaque, int n, int level)
|
||||
+{
|
||||
+ if (!level) {
|
||||
+ xteink_x3_eink_reset(DEVICE(opaque));
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static uint32_t xteink_x3_eink_transfer(SSIPeripheral *peripheral,
|
||||
+ uint32_t value)
|
||||
+{
|
||||
+ XteinkX3EinkState *s = XTEINK_X3_EINK(peripheral);
|
||||
+ uint8_t byte = value;
|
||||
+
|
||||
+ if (!s->dc) {
|
||||
+ s->command = byte;
|
||||
+ if (byte == CMD_DTM1 || byte == CMD_DTM2) {
|
||||
+ s->plane_pos = 0;
|
||||
+ } else if (byte == CMD_REFRESH) {
|
||||
+ xteink_x3_eink_render(s);
|
||||
+ xteink_x3_eink_pulse_busy(s);
|
||||
+ } else if (byte == CMD_POWER_ON || byte == CMD_POWER_OFF) {
|
||||
+ xteink_x3_eink_pulse_busy(s);
|
||||
+ }
|
||||
+ } else if (s->plane_pos < XTEINK_X3_PLANE_SIZE) {
|
||||
+ if (s->command == CMD_DTM1) {
|
||||
+ s->old_plane[s->plane_pos++] = byte;
|
||||
+ } else if (s->command == CMD_DTM2) {
|
||||
+ s->new_plane[s->plane_pos++] = byte;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return 0xff;
|
||||
+}
|
||||
+
|
||||
+static void xteink_x3_eink_invalidate(void *opaque)
|
||||
+{
|
||||
+ xteink_x3_eink_render(XTEINK_X3_EINK(opaque));
|
||||
+}
|
||||
+
|
||||
+static void xteink_x3_eink_update(void *opaque)
|
||||
+{
|
||||
+}
|
||||
+
|
||||
+static const GraphicHwOps xteink_x3_eink_graphics_ops = {
|
||||
+ .invalidate = xteink_x3_eink_invalidate,
|
||||
+ .gfx_update = xteink_x3_eink_update,
|
||||
+};
|
||||
+
|
||||
+static void xteink_x3_eink_init(Object *obj)
|
||||
+{
|
||||
+ XteinkX3EinkState *s = XTEINK_X3_EINK(obj);
|
||||
+
|
||||
+ s->console = graphic_console_init(DEVICE(s), 0,
|
||||
+ &xteink_x3_eink_graphics_ops, s);
|
||||
+ dpy_gfx_replace_surface(s->console,
|
||||
+ qemu_create_displaysurface(XTEINK_X3_HEIGHT, XTEINK_X3_WIDTH));
|
||||
+ s->busy_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
|
||||
+ xteink_x3_eink_busy_done, s);
|
||||
+ qdev_init_gpio_in_named(DEVICE(s), xteink_x3_eink_set_dc,
|
||||
+ XTEINK_X3_EINK_DC, 1);
|
||||
+ qdev_init_gpio_in_named(DEVICE(s), xteink_x3_eink_set_reset,
|
||||
+ XTEINK_X3_EINK_RESET, 1);
|
||||
+ qdev_init_gpio_out_named(DEVICE(s), &s->busy,
|
||||
+ XTEINK_X3_EINK_BUSY, 1);
|
||||
+}
|
||||
+
|
||||
+static void xteink_x3_eink_realize(SSIPeripheral *peripheral, Error **errp)
|
||||
+{
|
||||
+}
|
||||
+
|
||||
+static void xteink_x3_eink_class_init(ObjectClass *klass, void *data)
|
||||
+{
|
||||
+ DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
+ SSIPeripheralClass *ssi = SSI_PERIPHERAL_CLASS(klass);
|
||||
+
|
||||
+ ssi->realize = xteink_x3_eink_realize;
|
||||
+ ssi->transfer = xteink_x3_eink_transfer;
|
||||
+ ssi->cs_polarity = SSI_CS_LOW;
|
||||
+ device_class_set_legacy_reset(dc, xteink_x3_eink_reset);
|
||||
+ dc->user_creatable = false;
|
||||
+}
|
||||
+
|
||||
+static const TypeInfo xteink_x3_eink_info = {
|
||||
+ .name = TYPE_XTEINK_X3_EINK,
|
||||
+ .parent = TYPE_SSI_PERIPHERAL,
|
||||
+ .instance_size = sizeof(XteinkX3EinkState),
|
||||
+ .instance_init = xteink_x3_eink_init,
|
||||
+ .class_init = xteink_x3_eink_class_init,
|
||||
+};
|
||||
+
|
||||
+static void xteink_x3_eink_register_types(void)
|
||||
+{
|
||||
+ type_register_static(&xteink_x3_eink_info);
|
||||
+}
|
||||
+
|
||||
+type_init(xteink_x3_eink_register_types)
|
||||
diff --git i/hw/gpio/esp32_gpio.c w/hw/gpio/esp32_gpio.c
|
||||
index 4fea1f5..55cac08 100644
|
||||
--- i/hw/gpio/esp32_gpio.c
|
||||
+++ w/hw/gpio/esp32_gpio.c
|
||||
@@ -19,26 +19,77 @@
|
||||
#include "hw/qdev-properties.h"
|
||||
#include "hw/gpio/esp32_gpio.h"
|
||||
|
||||
+#define GPIO_OUT 0x04
|
||||
+#define GPIO_OUT_W1TS 0x08
|
||||
+#define GPIO_OUT_W1TC 0x0c
|
||||
+#define GPIO_ENABLE 0x20
|
||||
+#define GPIO_ENABLE_W1TS 0x24
|
||||
+#define GPIO_ENABLE_W1TC 0x28
|
||||
+#define GPIO_IN 0x3c
|
||||
|
||||
+static void esp32_gpio_drive_outputs(Esp32GpioState *s)
|
||||
+{
|
||||
+ for (int i = 0; i < ESP32_GPIO_COUNT; i++) {
|
||||
+ int level = (s->output_enable & BIT(i))
|
||||
+ ? !!(s->output_level & BIT(i))
|
||||
+ : 1;
|
||||
+ qemu_set_irq(s->output_lines[i], level);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static void esp32_gpio_set_input(void *opaque, int pin, int level)
|
||||
+{
|
||||
+ Esp32GpioState *s = ESP32_GPIO(opaque);
|
||||
+ s->input_level = deposit32(s->input_level, pin, 1, !!level);
|
||||
+}
|
||||
|
||||
static uint64_t esp32_gpio_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
Esp32GpioState *s = ESP32_GPIO(opaque);
|
||||
- uint64_t r = 0;
|
||||
- switch (addr) {
|
||||
- case A_GPIO_STRAP:
|
||||
- r = s->strap_mode;
|
||||
- break;
|
||||
|
||||
+ switch (addr) {
|
||||
+ case GPIO_OUT:
|
||||
+ return s->output_level;
|
||||
+ case GPIO_ENABLE:
|
||||
+ return s->output_enable;
|
||||
+ case A_GPIO_STRAP:
|
||||
+ return s->strap_mode;
|
||||
+ case GPIO_IN:
|
||||
+ return s->input_level;
|
||||
default:
|
||||
- break;
|
||||
+ return 0;
|
||||
}
|
||||
- return r;
|
||||
}
|
||||
|
||||
static void esp32_gpio_write(void *opaque, hwaddr addr,
|
||||
- uint64_t value, unsigned int size)
|
||||
+ uint64_t value, unsigned int size)
|
||||
{
|
||||
+ Esp32GpioState *s = ESP32_GPIO(opaque);
|
||||
+
|
||||
+ switch (addr) {
|
||||
+ case GPIO_OUT:
|
||||
+ s->output_level = value;
|
||||
+ break;
|
||||
+ case GPIO_OUT_W1TS:
|
||||
+ s->output_level |= value;
|
||||
+ break;
|
||||
+ case GPIO_OUT_W1TC:
|
||||
+ s->output_level &= ~value;
|
||||
+ break;
|
||||
+ case GPIO_ENABLE:
|
||||
+ s->output_enable = value;
|
||||
+ break;
|
||||
+ case GPIO_ENABLE_W1TS:
|
||||
+ s->output_enable |= value;
|
||||
+ break;
|
||||
+ case GPIO_ENABLE_W1TC:
|
||||
+ s->output_enable &= ~value;
|
||||
+ break;
|
||||
+ default:
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ esp32_gpio_drive_outputs(s);
|
||||
}
|
||||
|
||||
static const MemoryRegionOps uart_ops = {
|
||||
@@ -49,6 +100,11 @@ static const MemoryRegionOps uart_ops = {
|
||||
|
||||
static void esp32_gpio_reset_hold(Object *obj, ResetType type)
|
||||
{
|
||||
+ Esp32GpioState *s = ESP32_GPIO(obj);
|
||||
+ s->input_level = UINT32_MAX;
|
||||
+ s->output_level = 0;
|
||||
+ s->output_enable = 0;
|
||||
+ esp32_gpio_drive_outputs(s);
|
||||
}
|
||||
|
||||
static void esp32_gpio_realize(DeviceState *dev, Error **errp)
|
||||
@@ -67,6 +123,12 @@ static void esp32_gpio_init(Object *obj)
|
||||
TYPE_ESP32_GPIO, 0x1000);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
sysbus_init_irq(sbd, &s->irq);
|
||||
+ qdev_init_gpio_in_named(DEVICE(obj), esp32_gpio_set_input,
|
||||
+ ESP32_GPIO_INPUT, ESP32_GPIO_COUNT);
|
||||
+ qdev_init_gpio_out_named(DEVICE(obj), s->output_lines,
|
||||
+ ESP32_GPIO_OUTPUT, ESP32_GPIO_COUNT);
|
||||
+
|
||||
+ esp32_gpio_reset_hold(obj, RESET_TYPE_COLD);
|
||||
}
|
||||
|
||||
static Property esp32_gpio_properties[] = {
|
||||
diff --git i/hw/riscv/esp32c3.c w/hw/riscv/esp32c3.c
|
||||
index ed698fb..a49eae6 100644
|
||||
--- i/hw/riscv/esp32c3.c
|
||||
+++ w/hw/riscv/esp32c3.c
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "hw/riscv/boot.h"
|
||||
#include "hw/riscv/numa.h"
|
||||
#include "sysemu/device_tree.h"
|
||||
+#include "sysemu/blockdev.h"
|
||||
#include "sysemu/sysemu.h"
|
||||
#include "sysemu/kvm.h"
|
||||
#include "sysemu/runstate.h"
|
||||
@@ -40,6 +41,8 @@
|
||||
#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/misc/esp32c3_rtc_cntl.h"
|
||||
#include "hw/misc/esp32c3_aes.h"
|
||||
#include "hw/misc/esp32c3_rsa.h"
|
||||
@@ -49,6 +52,8 @@
|
||||
#include "hw/misc/esp32c3_jtag.h"
|
||||
#include "hw/dma/esp32c3_gdma.h"
|
||||
#include "hw/display/esp_rgb.h"
|
||||
+#include "hw/display/xteink_x3_eink.h"
|
||||
+#include "hw/sd/sd.h"
|
||||
#include "hw/net/can/esp32c3_twai.h"
|
||||
|
||||
#define ESP32C3_IO_WARNING 0
|
||||
@@ -66,6 +71,7 @@ struct Esp32C3MachineState {
|
||||
EspRISCVCPU soc;
|
||||
BusState periph_bus;
|
||||
MemoryRegion iomem;
|
||||
+ bool xteink_x3;
|
||||
|
||||
qemu_irq cpu_reset;
|
||||
|
||||
@@ -86,6 +92,8 @@ struct Esp32C3MachineState {
|
||||
ESP32C3TimgState timg[2];
|
||||
ESP32C3SysTimerState systimer;
|
||||
ESP32C3SpiState spi1;
|
||||
+ ESP32C3Spi2State spi2;
|
||||
+ ESP32C3AdcState adc;
|
||||
ESP32C3RtcCntlState rtccntl;
|
||||
ESP32C3UsbJtagState jtag;
|
||||
ESPRgbState rgb;
|
||||
@@ -420,9 +428,13 @@ 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), "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) {
|
||||
+ 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)
|
||||
}
|
||||
}
|
||||
|
||||
+ /* SPI2 controller (display and SD card) */
|
||||
+ {
|
||||
+ sysbus_realize(SYS_BUS_DEVICE(&ms->spi2), &error_fatal);
|
||||
+ MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->spi2), 0);
|
||||
+ memory_region_add_subregion_overlap(sys_mem, DR_REG_SPI2_BASE, mr, 0);
|
||||
+ }
|
||||
+
|
||||
+ /* SAR ADC (battery and resistor-ladder buttons) */
|
||||
+ {
|
||||
+ sysbus_realize(SYS_BUS_DEVICE(&ms->adc), &error_fatal);
|
||||
+ 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);
|
||||
+ }
|
||||
+
|
||||
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)
|
||||
}
|
||||
|
||||
/* RGB display realization */
|
||||
- {
|
||||
+ if (!ms->xteink_x3) {
|
||||
/* 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)
|
||||
}
|
||||
|
||||
|
||||
+static void xteink_x3_machine_init(MachineState *machine)
|
||||
+{
|
||||
+ Esp32C3MachineState *ms = ESP32C3_MACHINE(machine);
|
||||
+ ms->xteink_x3 = true;
|
||||
+ esp32c3_machine_init(machine);
|
||||
+
|
||||
+ DeviceState *panel = ssi_create_peripheral(ms->spi2.bus,
|
||||
+ 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,
|
||||
+ qdev_get_gpio_in_named(panel, XTEINK_X3_EINK_DC, 0));
|
||||
+ qdev_connect_gpio_out_named(DEVICE(&ms->gpio), ESP32_GPIO_OUTPUT, 5,
|
||||
+ qdev_get_gpio_in_named(panel, XTEINK_X3_EINK_RESET, 0));
|
||||
+ qdev_connect_gpio_out_named(panel, XTEINK_X3_EINK_BUSY, 0,
|
||||
+ qdev_get_gpio_in_named(DEVICE(&ms->gpio), ESP32_GPIO_INPUT, 6));
|
||||
+ device_cold_reset(panel);
|
||||
+ qemu_set_irq(qdev_get_gpio_in_named(panel, SSI_GPIO_CS, 0), 1);
|
||||
+
|
||||
+ DeviceState *sd_adapter = qdev_new("ssi-sd");
|
||||
+ qdev_prop_set_uint8(sd_adapter, "cs", 1);
|
||||
+ qdev_realize_and_unref(sd_adapter, BUS(ms->spi2.bus), &error_fatal);
|
||||
+ qdev_connect_gpio_out_named(DEVICE(&ms->gpio), ESP32_GPIO_OUTPUT, 12,
|
||||
+ qdev_get_gpio_in_named(sd_adapter, SSI_GPIO_CS, 0));
|
||||
+ qemu_set_irq(qdev_get_gpio_in_named(sd_adapter, SSI_GPIO_CS, 0), 1);
|
||||
+
|
||||
+ DriveInfo *dinfo = drive_get(IF_SD, 0, 0);
|
||||
+ DeviceState *sd_card = qdev_new(TYPE_SD_CARD_SPI);
|
||||
+ qdev_prop_set_drive_err(sd_card, "drive",
|
||||
+ dinfo ? blk_by_legacy_dinfo(dinfo) : NULL,
|
||||
+ &error_fatal);
|
||||
+ qdev_realize_and_unref(sd_card,
|
||||
+ qdev_get_child_bus(sd_adapter, "sd-bus"),
|
||||
+ &error_fatal);
|
||||
+}
|
||||
+
|
||||
/* Initialize machine type */
|
||||
static void esp32c3_machine_class_init(ObjectClass *oc, void *data)
|
||||
{
|
||||
@@ -683,9 +745,23 @@ static const TypeInfo esp32c3_info = {
|
||||
.class_init = esp32c3_machine_class_init,
|
||||
};
|
||||
|
||||
+static void xteink_x3_machine_class_init(ObjectClass *oc, void *data)
|
||||
+{
|
||||
+ MachineClass *mc = MACHINE_CLASS(oc);
|
||||
+ mc->desc = "xteink X3 (ESP32-C3, UC8253 e-ink)";
|
||||
+ mc->init = xteink_x3_machine_init;
|
||||
+}
|
||||
+
|
||||
+static const TypeInfo xteink_x3_info = {
|
||||
+ .name = MACHINE_TYPE_NAME("xteink-x3"),
|
||||
+ .parent = TYPE_ESP32C3_MACHINE,
|
||||
+ .class_init = xteink_x3_machine_class_init,
|
||||
+};
|
||||
+
|
||||
static void esp32c3_machine_type_init(void)
|
||||
{
|
||||
type_register_static(&esp32c3_info);
|
||||
+ type_register_static(&xteink_x3_info);
|
||||
}
|
||||
|
||||
type_init(esp32c3_machine_type_init);
|
||||
diff --git i/hw/sd/ssi-sd.c w/hw/sd/ssi-sd.c
|
||||
index 1594051..552dc2c 100644
|
||||
--- i/hw/sd/ssi-sd.c
|
||||
+++ w/hw/sd/ssi-sd.c
|
||||
@@ -64,6 +64,7 @@ struct ssi_sd_state {
|
||||
int32_t arglen;
|
||||
int32_t response_pos;
|
||||
int32_t stopping;
|
||||
+ bool idle;
|
||||
SDBus sdbus;
|
||||
};
|
||||
|
||||
@@ -180,7 +181,7 @@ static uint32_t ssi_sd_transfer(SSIPeripheral *dev, uint32_t val)
|
||||
/* CMD8/CMD58 returns R3/R7 response */
|
||||
DPRINTF("Returned R3/R7\n");
|
||||
s->arglen = 5;
|
||||
- s->response[0] = 1;
|
||||
+ s->response[0] = s->cmd == 58 ? s->idle : 1;
|
||||
memcpy(&s->response[1], longresp, 4);
|
||||
} else if (s->arglen != 4) {
|
||||
BADF("Unexpected response to cmd %d\n", s->cmd);
|
||||
@@ -236,6 +237,11 @@ static uint32_t ssi_sd_transfer(SSIPeripheral *dev, uint32_t val)
|
||||
status |= SSI_SDR_PARAMETER_ERROR;
|
||||
s->response[0] = status >> 8;
|
||||
s->response[1] = status;
|
||||
+ if (s->cmd == 0) {
|
||||
+ s->idle = true;
|
||||
+ } else if (s->cmd == 41 && s->response[0] == 0) {
|
||||
+ s->idle = false;
|
||||
+ }
|
||||
DPRINTF("Card status 0x%02x\n", status);
|
||||
}
|
||||
s->mode = SSI_SD_PREP_RESP;
|
||||
@@ -329,6 +335,9 @@ static int ssi_sd_post_load(void *opaque, int version_id)
|
||||
{
|
||||
ssi_sd_state *s = (ssi_sd_state *)opaque;
|
||||
|
||||
+ if (version_id < 8) {
|
||||
+ s->idle = false;
|
||||
+ }
|
||||
if (s->mode > SSI_SD_SKIP_CRC16) {
|
||||
return -EINVAL;
|
||||
}
|
||||
@@ -347,7 +356,7 @@ static int ssi_sd_post_load(void *opaque, int version_id)
|
||||
|
||||
static const VMStateDescription vmstate_ssi_sd = {
|
||||
.name = "ssi_sd",
|
||||
- .version_id = 7,
|
||||
+ .version_id = 8,
|
||||
.minimum_version_id = 7,
|
||||
.post_load = ssi_sd_post_load,
|
||||
.fields = (const VMStateField []) {
|
||||
@@ -361,6 +370,7 @@ static const VMStateDescription vmstate_ssi_sd = {
|
||||
VMSTATE_INT32(arglen, ssi_sd_state),
|
||||
VMSTATE_INT32(response_pos, ssi_sd_state),
|
||||
VMSTATE_INT32(stopping, ssi_sd_state),
|
||||
+ VMSTATE_BOOL_V(idle, ssi_sd_state, 8),
|
||||
VMSTATE_SSI_PERIPHERAL(ssidev, ssi_sd_state),
|
||||
VMSTATE_END_OF_LIST()
|
||||
}
|
||||
@@ -387,6 +397,7 @@ static void ssi_sd_reset(DeviceState *dev)
|
||||
s->arglen = 0;
|
||||
s->response_pos = 0;
|
||||
s->stopping = 0;
|
||||
+ s->idle = true;
|
||||
}
|
||||
|
||||
static void ssi_sd_class_init(ObjectClass *klass, void *data)
|
||||
diff --git i/hw/ssi/esp32c3_spi2.c w/hw/ssi/esp32c3_spi2.c
|
||||
new file mode 100644
|
||||
index 0000000..28fcc9d
|
||||
--- /dev/null
|
||||
+++ w/hw/ssi/esp32c3_spi2.c
|
||||
@@ -0,0 +1,101 @@
|
||||
+/*
|
||||
+ * ESP32-C3 general-purpose SPI2 controller
|
||||
+ *
|
||||
+ * SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+ */
|
||||
+
|
||||
+#include "qemu/osdep.h"
|
||||
+#include "qemu/module.h"
|
||||
+#include "hw/ssi/esp32c3_spi2.h"
|
||||
+
|
||||
+#define SPI2_CMD 0x000
|
||||
+#define SPI2_MS_DLEN 0x01c
|
||||
+#define SPI2_W0 0x098
|
||||
+
|
||||
+#define SPI2_CMD_USR BIT(24)
|
||||
+#define SPI2_DLEN_MASK 0x3ffff
|
||||
+#define SPI2_BUFFER_SIZE 64
|
||||
+
|
||||
+static void esp32c3_spi2_transfer(ESP32C3Spi2State *s)
|
||||
+{
|
||||
+ size_t bytes = (s->regs[SPI2_MS_DLEN / 4] & SPI2_DLEN_MASK) / 8 + 1;
|
||||
+ bytes = MIN(bytes, SPI2_BUFFER_SIZE);
|
||||
+
|
||||
+ for (size_t i = 0; i < bytes; i++) {
|
||||
+ uint32_t *word = &s->regs[SPI2_W0 / 4 + i / 4];
|
||||
+ unsigned shift = (i % 4) * 8;
|
||||
+ uint8_t tx = *word >> shift;
|
||||
+ uint8_t rx = ssi_transfer(s->bus, tx);
|
||||
+ *word = (*word & ~(0xffu << shift)) | ((uint32_t)rx << shift);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static uint64_t esp32c3_spi2_read(void *opaque, hwaddr addr, unsigned size)
|
||||
+{
|
||||
+ ESP32C3Spi2State *s = ESP32C3_SPI2(opaque);
|
||||
+ return s->regs[addr / 4];
|
||||
+}
|
||||
+
|
||||
+static void esp32c3_spi2_write(void *opaque, hwaddr addr, uint64_t value,
|
||||
+ unsigned size)
|
||||
+{
|
||||
+ ESP32C3Spi2State *s = ESP32C3_SPI2(opaque);
|
||||
+
|
||||
+ if (addr == SPI2_CMD) {
|
||||
+ if (value & SPI2_CMD_USR) {
|
||||
+ esp32c3_spi2_transfer(s);
|
||||
+ }
|
||||
+ s->regs[SPI2_CMD / 4] = 0;
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ s->regs[addr / 4] = value;
|
||||
+}
|
||||
+
|
||||
+static const MemoryRegionOps esp32c3_spi2_ops = {
|
||||
+ .read = esp32c3_spi2_read,
|
||||
+ .write = esp32c3_spi2_write,
|
||||
+ .endianness = DEVICE_LITTLE_ENDIAN,
|
||||
+ .valid = {
|
||||
+ .min_access_size = 4,
|
||||
+ .max_access_size = 4,
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+static void esp32c3_spi2_reset_hold(Object *obj, ResetType type)
|
||||
+{
|
||||
+ ESP32C3Spi2State *s = ESP32C3_SPI2(obj);
|
||||
+ memset(s->regs, 0, sizeof(s->regs));
|
||||
+}
|
||||
+
|
||||
+static void esp32c3_spi2_init(Object *obj)
|
||||
+{
|
||||
+ ESP32C3Spi2State *s = ESP32C3_SPI2(obj);
|
||||
+ SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
|
||||
+
|
||||
+ memory_region_init_io(&s->iomem, obj, &esp32c3_spi2_ops, s,
|
||||
+ TYPE_ESP32C3_SPI2, ESP32C3_SPI2_IO_SIZE);
|
||||
+ sysbus_init_mmio(sbd, &s->iomem);
|
||||
+ s->bus = ssi_create_bus(DEVICE(s), "spi");
|
||||
+}
|
||||
+
|
||||
+static void esp32c3_spi2_class_init(ObjectClass *klass, void *data)
|
||||
+{
|
||||
+ ResettableClass *rc = RESETTABLE_CLASS(klass);
|
||||
+ rc->phases.hold = esp32c3_spi2_reset_hold;
|
||||
+}
|
||||
+
|
||||
+static const TypeInfo esp32c3_spi2_info = {
|
||||
+ .name = TYPE_ESP32C3_SPI2,
|
||||
+ .parent = TYPE_SYS_BUS_DEVICE,
|
||||
+ .instance_size = sizeof(ESP32C3Spi2State),
|
||||
+ .instance_init = esp32c3_spi2_init,
|
||||
+ .class_init = esp32c3_spi2_class_init,
|
||||
+};
|
||||
+
|
||||
+static void esp32c3_spi2_register_types(void)
|
||||
+{
|
||||
+ type_register_static(&esp32c3_spi2_info);
|
||||
+}
|
||||
+
|
||||
+type_init(esp32c3_spi2_register_types)
|
||||
diff --git i/hw/ssi/meson.build w/hw/ssi/meson.build
|
||||
index 30f1a87..5524459 100644
|
||||
--- i/hw/ssi/meson.build
|
||||
+++ w/hw/ssi/meson.build
|
||||
@@ -11,7 +11,7 @@ system_ss.add(when: 'CONFIG_XILINX_SPIPS', if_true: files('xilinx_spips.c'))
|
||||
system_ss.add(when: 'CONFIG_XLNX_VERSAL', if_true: files('xlnx-versal-ospi.c'))
|
||||
system_ss.add(when: 'CONFIG_IMX', if_true: files('imx_spi.c'))
|
||||
system_ss.add(when: 'CONFIG_XTENSA_ESP32', if_true: files('esp32_spi.c'))
|
||||
-system_ss.add(when: 'CONFIG_RISCV_ESP32C3', if_true: files('esp32c3_spi.c'))
|
||||
+system_ss.add(when: 'CONFIG_RISCV_ESP32C3', if_true: files('esp32c3_spi.c', 'esp32c3_spi2.c'))
|
||||
system_ss.add(when: 'CONFIG_XTENSA_ESP32S3', if_true: files('esp32s3_spi.c'))
|
||||
system_ss.add(when: 'CONFIG_IBEX', if_true: files('ibex_spi_host.c'))
|
||||
system_ss.add(when: 'CONFIG_BCM2835_SPI', if_true: files('bcm2835_spi.c'))
|
||||
diff --git i/include/hw/adc/esp32c3_adc.h w/include/hw/adc/esp32c3_adc.h
|
||||
new file mode 100644
|
||||
index 0000000..1b914e1
|
||||
--- /dev/null
|
||||
+++ w/include/hw/adc/esp32c3_adc.h
|
||||
@@ -0,0 +1,17 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "hw/sysbus.h"
|
||||
+
|
||||
+#define TYPE_ESP32C3_ADC "esp32c3.adc"
|
||||
+OBJECT_DECLARE_SIMPLE_TYPE(ESP32C3AdcState, ESP32C3_ADC)
|
||||
+
|
||||
+#define ESP32C3_ADC_CHANNELS 10
|
||||
+#define ESP32C3_ADC_IO_SIZE 0x1000
|
||||
+
|
||||
+struct ESP32C3AdcState {
|
||||
+ SysBusDevice parent_obj;
|
||||
+ MemoryRegion iomem;
|
||||
+ uint32_t regs[ESP32C3_ADC_IO_SIZE / sizeof(uint32_t)];
|
||||
+ uint32_t input[ESP32C3_ADC_CHANNELS];
|
||||
+ uint8_t channel;
|
||||
+};
|
||||
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
|
||||
--- /dev/null
|
||||
+++ w/include/hw/display/xteink_x3_eink.h
|
||||
@@ -0,0 +1,29 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "hw/ssi/ssi.h"
|
||||
+#include "qemu/timer.h"
|
||||
+#include "ui/console.h"
|
||||
+
|
||||
+#define TYPE_XTEINK_X3_EINK "xteink-x3-eink"
|
||||
+OBJECT_DECLARE_SIMPLE_TYPE(XteinkX3EinkState, XTEINK_X3_EINK)
|
||||
+
|
||||
+#define XTEINK_X3_EINK_DC "dc"
|
||||
+#define XTEINK_X3_EINK_RESET "reset"
|
||||
+#define XTEINK_X3_EINK_BUSY "busy"
|
||||
+
|
||||
+#define XTEINK_X3_WIDTH 792
|
||||
+#define XTEINK_X3_HEIGHT 528
|
||||
+#define XTEINK_X3_WIDTH_BYTES (XTEINK_X3_WIDTH / 8)
|
||||
+#define XTEINK_X3_PLANE_SIZE (XTEINK_X3_WIDTH_BYTES * XTEINK_X3_HEIGHT)
|
||||
+
|
||||
+struct XteinkX3EinkState {
|
||||
+ SSIPeripheral parent_obj;
|
||||
+ QemuConsole *console;
|
||||
+ QEMUTimer *busy_timer;
|
||||
+ qemu_irq busy;
|
||||
+ bool dc;
|
||||
+ uint8_t command;
|
||||
+ uint32_t plane_pos;
|
||||
+ uint8_t old_plane[XTEINK_X3_PLANE_SIZE];
|
||||
+ uint8_t new_plane[XTEINK_X3_PLANE_SIZE];
|
||||
+};
|
||||
diff --git i/include/hw/gpio/esp32_gpio.h w/include/hw/gpio/esp32_gpio.h
|
||||
index 163b41a..e2f52ad 100644
|
||||
--- i/include/hw/gpio/esp32_gpio.h
|
||||
+++ w/include/hw/gpio/esp32_gpio.h
|
||||
@@ -14,12 +14,20 @@ REG32(GPIO_STRAP, 0x0038)
|
||||
#define ESP32_STRAP_MODE_FLASH_BOOT 0x12
|
||||
#define ESP32_STRAP_MODE_UART_BOOT 0x0f
|
||||
|
||||
+#define ESP32_GPIO_COUNT 32
|
||||
+#define ESP32_GPIO_INPUT "gpio-in"
|
||||
+#define ESP32_GPIO_OUTPUT "gpio-out"
|
||||
+
|
||||
typedef struct Esp32GpioState {
|
||||
SysBusDevice parent_obj;
|
||||
|
||||
MemoryRegion iomem;
|
||||
qemu_irq irq;
|
||||
+ qemu_irq output_lines[ESP32_GPIO_COUNT];
|
||||
uint32_t strap_mode;
|
||||
+ uint32_t input_level;
|
||||
+ uint32_t output_level;
|
||||
+ uint32_t output_enable;
|
||||
} Esp32GpioState;
|
||||
|
||||
typedef struct Esp32GpioClass {
|
||||
diff --git i/include/hw/ssi/esp32c3_spi2.h w/include/hw/ssi/esp32c3_spi2.h
|
||||
new file mode 100644
|
||||
index 0000000..2155c36
|
||||
--- /dev/null
|
||||
+++ w/include/hw/ssi/esp32c3_spi2.h
|
||||
@@ -0,0 +1,17 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "hw/ssi/ssi.h"
|
||||
+#include "hw/sysbus.h"
|
||||
+
|
||||
+#define TYPE_ESP32C3_SPI2 "ssi.esp32c3.spi2"
|
||||
+OBJECT_DECLARE_SIMPLE_TYPE(ESP32C3Spi2State, ESP32C3_SPI2)
|
||||
+
|
||||
+#define ESP32C3_SPI2_IO_SIZE 0x1000
|
||||
+#define ESP32C3_SPI2_REG_WORDS (ESP32C3_SPI2_IO_SIZE / sizeof(uint32_t))
|
||||
+
|
||||
+struct ESP32C3Spi2State {
|
||||
+ SysBusDevice parent_obj;
|
||||
+ MemoryRegion iomem;
|
||||
+ SSIBus *bus;
|
||||
+ uint32_t regs[ESP32C3_SPI2_REG_WORDS];
|
||||
+};
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
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"
|
||||
JOBS="${JOBS:-2}"
|
||||
|
||||
if [ ! -d "$SRC/.git" ]; then
|
||||
git clone --depth 1 --branch "$TAG" https://github.com/espressif/qemu "$SRC"
|
||||
fi
|
||||
|
||||
cd "$SRC"
|
||||
if git apply --check "$PATCH" 2>/dev/null; then
|
||||
git apply "$PATCH"
|
||||
elif ! git apply --reverse --check "$PATCH" 2>/dev/null; then
|
||||
echo "QEMU source has changes that conflict with $PATCH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f build/build.ninja ]; then
|
||||
./configure \
|
||||
--target-list=riscv32-softmmu \
|
||||
--enable-gcrypt \
|
||||
--enable-slirp \
|
||||
--disable-werror \
|
||||
--disable-docs \
|
||||
--disable-tools
|
||||
fi
|
||||
|
||||
ninja -C build -j"$JOBS" qemu-system-riscv32
|
||||
printf '%s\n' "$SRC/build/qemu-system-riscv32"
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a FAT32 SD-card image from a local directory — the local-disk backing
|
||||
# for the emulated SD. QEMU's block layer is the interface: this same image is
|
||||
# swapped for a browser-supplied blockdev (File/OPFS) in the WASM build.
|
||||
#
|
||||
# scripts/mksd.sh [src-dir] [out-img] (SIZE_MB env overrides size)
|
||||
set -euo pipefail
|
||||
|
||||
SRC="${1:-../crosspoint-reader-lua/sdcard}"
|
||||
OUT="${2:-sd.img}"
|
||||
SIZE_MB="${SIZE_MB:-64}"
|
||||
|
||||
[ -d "$SRC" ] || { echo "no such dir: $SRC" >&2; exit 1; }
|
||||
OUT_ABS="$(realpath -m "$OUT")"
|
||||
|
||||
rm -f "$OUT_ABS"
|
||||
truncate -s "${SIZE_MB}M" "$OUT_ABS"
|
||||
mkfs.vfat -F 32 -n XTEINK "$OUT_ABS" >/dev/null
|
||||
|
||||
# mcopy the tree in (recursive, no per-file prompts).
|
||||
( cd "$SRC" && for e in *; do [ -e "$e" ] && mcopy -i "$OUT_ABS" -s -Q -o "$e" ::; done )
|
||||
|
||||
echo "wrote $OUT (${SIZE_MB}M FAT32) from $SRC"
|
||||
mdir -i "$OUT_ABS" :: | sed 's/^/ /'
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
# Exercise the button/ADC QOM interface: qmp-qom.py <sock> get|set <path> <prop> [value]
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX)
|
||||
sock.connect(sys.argv[1])
|
||||
f = sock.makefile("rw")
|
||||
json.loads(f.readline())
|
||||
|
||||
|
||||
def cmd(command):
|
||||
f.write(json.dumps(command) + "\n")
|
||||
f.flush()
|
||||
return json.loads(f.readline())
|
||||
|
||||
|
||||
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])}}))
|
||||
else:
|
||||
print(cmd({"execute": "qom-get", "arguments": {
|
||||
"path": path, "property": prop}}))
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX)
|
||||
sock.connect(sys.argv[1])
|
||||
f = sock.makefile("rw")
|
||||
json.loads(f.readline())
|
||||
for command in (
|
||||
{"execute": "qmp_capabilities"},
|
||||
{"execute": "screendump", "arguments": {"filename": sys.argv[2]}},
|
||||
):
|
||||
f.write(json.dumps(command) + "\n")
|
||||
f.flush()
|
||||
response = json.loads(f.readline())
|
||||
if "error" in response:
|
||||
raise RuntimeError(response["error"])
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# Point `firmware`/`elf` at any xteink-X3 build. `make firmware` produces
|
||||
# flash.bin + firmware.elf from a local crosspoint-reader-lua PlatformIO build.
|
||||
[wokwi]
|
||||
version = 1
|
||||
firmware = 'flash.bin'
|
||||
elf = 'firmware.elf'
|
||||
|
||||
[[chip]]
|
||||
name = 'eink-x3'
|
||||
binary = 'chip/dist/eink-x3.chip.wasm'
|
||||
Reference in New Issue
Block a user