Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9946dda059 | |||
| bfdbe91515 | |||
| d45d630223 | |||
| 0275620620 | |||
| 7b9b73c440 | |||
| c8654eda59 | |||
| c45800cdf8 | |||
| f15a7e85d3 | |||
| 7f55ce0138 | |||
| dbe79afa73 | |||
| 6a34276230 | |||
| d70a23f108 |
@@ -0,0 +1 @@
|
||||
*
|
||||
@@ -0,0 +1 @@
|
||||
/home/evanreichard/Development/qemu-xteink
|
||||
@@ -9,7 +9,7 @@ description: "Build and interactively test Xteink ESP32-C3 firmware in the nativ
|
||||
|
||||
Build firmware for an Xteink device, boot it in the native emulator, and validate behavior through serial logs, physical controls, screenshots, SD state, and network access.
|
||||
|
||||
Resolve script paths relative to this skill directory. `scripts/xteink-emu.sh` stores the qemu-xteink checkout location and runs the globally installed `xteink-emu` (or its uv project fallback); the CLI builds native QEMU on first boot when missing. If its location is unset, run the `variable.sh --set` command printed by the wrapper after asking the user for the checkout path.
|
||||
Resolve script paths relative to this skill directory. `scripts/esp-emu.sh` stores the qemu-esp-boards checkout location and runs the globally installed `esp-emu --board xteink` (or its uv project fallback); the CLI builds native QEMU on first boot when missing. If its location is unset, run the `variable.sh --set` command printed by the wrapper after asking the user for the checkout path.
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -24,23 +24,23 @@ Resolve script paths relative to this skill directory. `scripts/xteink-emu.sh` s
|
||||
2. Boot it. `boot` replaces an emulator already using the state and simulates a two-second cold-boot power hold:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" boot "$XTEINK_FIRMWARE"
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" boot "$XTEINK_FIRMWARE"
|
||||
```
|
||||
|
||||
Without an SD argument, the emulator creates or reuses `$XTEINK_EMU_STATE/sdcard.img`. An existing image can be used directly. A directory is merged into the state image before every boot; host files overwrite conflicts, firmware-created files remain, and the source directory is unchanged:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" boot "$XTEINK_FIRMWARE" --sdcard card.img
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" boot "$XTEINK_FIRMWARE" --sdcard test-library/
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" boot "$XTEINK_FIRMWARE" --sdcard test-library/ --fresh-sd
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" boot "$XTEINK_FIRMWARE" --sdcard card.img
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" boot "$XTEINK_FIRMWARE" --sdcard test-library/
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" boot "$XTEINK_FIRMWARE" --sdcard test-library/ --fresh-sd
|
||||
```
|
||||
|
||||
3. Synchronize on observable firmware behavior rather than sleeping:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" wait-log 'Entering activity: Home' --timeout 60
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" wait-idle 2 --timeout 30
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" wait-frame 2 --timeout 30
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" wait-log 'Entering activity: Home' --timeout 60
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" wait-idle 2 --timeout 30
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" wait-frame 2 --timeout 30
|
||||
tail -f "$XTEINK_EMU_STATE/serial.log"
|
||||
```
|
||||
|
||||
@@ -49,9 +49,9 @@ Resolve script paths relative to this skill directory. `scripts/xteink-emu.sh` s
|
||||
4. Drive physical controls. `press` waits for reliable debounced press and release sampling, and accepts a sequence:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" press bottom-4 bottom-4 bottom-2
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" hold power
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" release power
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" press bottom-4 bottom-4 bottom-2
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" hold power
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" release power
|
||||
```
|
||||
|
||||
Buttons are `left`, `right`, `bottom-1` through `bottom-4`, and `power`. Use labels rendered by the current screen as the authoritative mapping.
|
||||
@@ -59,14 +59,14 @@ Resolve script paths relative to this skill directory. `scripts/xteink-emu.sh` s
|
||||
5. Put repeatable flows in a plain command file, or pipe them over stdin. `--state` is inherited by every line:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" run flow.xteink
|
||||
printf '%s\n' 'wait-log "ready"' 'press bottom-2' | scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" run
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" run flow.xteink
|
||||
printf '%s\n' 'wait-log "ready"' 'press bottom-2' | scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" run
|
||||
```
|
||||
|
||||
6. Capture and inspect meaningful states without a static settle delay:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" capture /tmp/xteink-screen.png
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" capture /tmp/xteink-screen.png
|
||||
```
|
||||
|
||||
7. For network tests, connect firmware to the single open Wi-Fi network named `qemu`, then validate the observable result.
|
||||
@@ -81,5 +81,5 @@ Resolve script paths relative to this skill directory. `scripts/xteink-emu.sh` s
|
||||
## Cleanup
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh --state "$XTEINK_EMU_STATE" stop
|
||||
scripts/esp-emu.sh --board xteink --state "$XTEINK_EMU_STATE" stop
|
||||
```
|
||||
|
||||
+5
-5
@@ -3,15 +3,15 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
QEMU_REPO="$("$SCRIPT_DIR/variable.sh" --get XTEINK_QEMU_REPO)"
|
||||
TOOL="$QEMU_REPO/tools/xteink-emu-cli"
|
||||
TOOL="$QEMU_REPO/tools/esp-emu-cli"
|
||||
|
||||
if [[ ! -f "$TOOL/pyproject.toml" ]]; then
|
||||
echo >&2 "$QEMU_REPO is not a qemu-xteink checkout (missing tools/xteink-emu-cli)"
|
||||
echo >&2 "$QEMU_REPO is not a qemu-esp-boards checkout (missing tools/esp-emu-cli)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
export XTEINK_QEMU_REPO="$QEMU_REPO"
|
||||
if command -v xteink-emu >/dev/null; then
|
||||
exec xteink-emu "$@"
|
||||
if command -v esp-emu >/dev/null; then
|
||||
exec esp-emu "$@"
|
||||
fi
|
||||
exec uv run --project "$TOOL" xteink-emu "$@"
|
||||
exec uv run --project "$TOOL" esp-emu "$@"
|
||||
@@ -0,0 +1,154 @@
|
||||
# qemu-esp-boards
|
||||
|
||||
Espressif's QEMU fork carrying custom ESP32 machine models — `xteink` (X3/X4,
|
||||
ESP32-C3, e-ink) and `e32r40t` (ESP32, ST7796 LCD) — plus a wasm32 TCG backend (ported from [ktock/qemu-wasm](https://github.com/ktock/qemu-wasm)),
|
||||
used by [xteink-web-emulator](../xteink-web-emulator) to run CrossPoint firmware
|
||||
in the browser.
|
||||
|
||||
## Branch
|
||||
|
||||
- `main` — the working branch. Base is Espressif's `esp-develop-9.2.2-20260417`
|
||||
(`40edccac`).
|
||||
|
||||
## Remotes
|
||||
|
||||
- `origin` — https://ssh.gitea.va.reichard.io/evan/qemu-esp-boards
|
||||
- `upstream` — https://github.com/espressif/qemu
|
||||
- `qemu-wasm` — https://github.com/ktock/qemu-wasm (source of the wasm32 backend)
|
||||
|
||||
## Building
|
||||
|
||||
### Native (parity check — authoritative)
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
./configure --target-list=riscv32-softmmu --with-devices-riscv32=xteink --enable-gcrypt
|
||||
ninja -C build qemu-system-riscv32
|
||||
```
|
||||
|
||||
Install the emulator controller as an editable global tool:
|
||||
|
||||
```sh
|
||||
uv tool install -e tools/esp-emu-cli
|
||||
esp-emu --help
|
||||
```
|
||||
|
||||
`esp-emu --board {xteink,e32r40t}` selects the machine and defaults to `xteink`. Flow scripts
|
||||
select it in the shebang, which needs `env -S`: `#!/usr/bin/env -S esp-emu --board e32r40t`.
|
||||
|
||||
Its first `boot` builds `.#qemu-native-xteink` with Nix when `dist/qemu-native` is missing. Run `make xteink` when you want to build ahead of time.
|
||||
|
||||
### Agent emulator
|
||||
|
||||
```sh
|
||||
nix run .#esp-emu -- --board xteink boot firmware.bin
|
||||
nix run .#esp-emu -- --board xteink boot firmware.bin --sdcard sdcard.img
|
||||
nix run .#esp-emu -- --board xteink boot firmware.bin --sdcard test-library/ --fresh-sd
|
||||
nix run .#esp-emu -- --board xteink wait-log 'ready' --timeout 30
|
||||
nix run .#esp-emu -- --board xteink press bottom-4 bottom-4 bottom-2
|
||||
nix run .#esp-emu -- --board xteink wait-frame 2
|
||||
nix run .#esp-emu -- --board xteink capture screen.png
|
||||
nix run .#esp-emu -- --board xteink memory
|
||||
nix run .#esp-emu -- --board xteink stop
|
||||
```
|
||||
|
||||
Use one state for a complete flow:
|
||||
|
||||
```sh
|
||||
nix run .#esp-emu -- --board xteink --state /tmp/reader-test run flow.xteink
|
||||
# Omit flow.xteink to read command lines from stdin.
|
||||
```
|
||||
|
||||
`boot` replaces the emulator already running in that state. Without `--sdcard`, it creates or reuses `$STATE/sdcard.img`. An image path is used directly; a directory is merged into the state image before every boot, with host files winning conflicts and other image files preserved. `--fresh-sd` recreates a state-owned image before importing the directory. The source directory is never modified.
|
||||
|
||||
The installable application lives in `tools/esp-emu-cli/`; `uv tool install -e tools/esp-emu-cli` exposes the global `esp-emu`, `esp-emu --board xteink`, and `esp-emu --board e32r40t` commands and reflects checkout edits immediately. `press` waits for debounced guest ADC reads, so multiple presses do not need display waits. `wait-log` advances a persistent cursor and fails on timeout; use `wait-frame` only when visible rendering is itself under test. `capture` records the current panel without a static delay.
|
||||
|
||||
`web` serves a browser UI for firmware, SD images, controls, screen, logs, and SD files. It
|
||||
adapts to the board it was started for: `esp-emu --board xteink web` renders the button chrome, while
|
||||
`esp-emu --board e32r40t web` renders a touch panel you click and drag directly on the screen, and asks
|
||||
for the `bootloader.bin` and `partitions.bin` that sit beside a PlatformIO `firmware.bin`.
|
||||
A click is held for at least 250 ms, because the guest only samples touch between redraws. `memory` reports RAM high-water marks; QEMU zeroes RAM at reset, so firmware heap logs remain authoritative for live usage. State defaults to `/tmp/esp-emu-xteink-$UID`. `interactive` opens a native window and maps Left/Right, 1–4, and P to device buttons. Both emulated boards expose the open Wi-Fi network `qemu` through QEMU user-mode
|
||||
NAT (`192.168.4.0/24`). Xteink uses the ESP32-C3 Wi-Fi front end; E32R40T uses the
|
||||
classic Xtensa ESP32 front end. They share the synthetic AP and network backend,
|
||||
so DHCP and outbound internet access behave the same.
|
||||
|
||||
### WebAssembly
|
||||
|
||||
Driven by the parent repo, not here:
|
||||
|
||||
```sh
|
||||
cd ../xteink-web-emulator
|
||||
make web-dev # incremental build straight from this tree
|
||||
```
|
||||
|
||||
The parent's `scripts/build-qemu-wasm.sh` compiles this checkout inside the
|
||||
emsdk Docker image defined in `xteink-web-emulator/qemu/wasm/Dockerfile`.
|
||||
|
||||
## E32R40T (4.0" ESP32-32E display module)
|
||||
|
||||
A separate `e32r40t` machine models the E32R40T board (ESP32-WROOM-32E,
|
||||
ST7796S 320x480 panel + XPT2046 resistive touch, both on SPI2). It is a
|
||||
subclass of the upstream `esp32` machine; the panel and touch share one
|
||||
emulated board state. The GPIO model was widened to 40 pins so the firmware's
|
||||
GPIO 33 (touch CS) and GPIO 36 (touch IRQ) lines resolve. The classic ESP32 Wi-Fi
|
||||
MAC/DMA and required PHY register windows are modeled for the same open `qemu`
|
||||
hotspot and user-mode NAT used by Xteink.
|
||||
|
||||
```sh
|
||||
./configure --target-list=xtensa-softmmu --enable-gcrypt
|
||||
ninja -C build qemu-system-xtensa
|
||||
./build/qemu-system-xtensa -machine e32r40t -m 4M \
|
||||
-global driver=timer.esp32.timg,property=wdt_disable,value=true \
|
||||
-drive file=flash.bin,if=mtd,format=raw \
|
||||
-drive file=sdcard.img,if=sd,format=raw \
|
||||
-display none -serial stdio
|
||||
```
|
||||
|
||||
The `wdt_disable` global is worth keeping for this board: TCG is slow enough that
|
||||
IDF startup trips the timer-group watchdogs repeatedly, and each reset restarts
|
||||
SD initialisation. With it, boot is a single reset and mounting is deterministic.
|
||||
|
||||
`frame-generation` is exposed on the machine (same as `xteink`) so the CLI
|
||||
can capture only on a real refresh.
|
||||
|
||||
The console stays 320x480 whatever MADCTL says, matching the physical glass:
|
||||
a rotated guest draws sideways, and touch coordinates are physical, so
|
||||
rotating the guest never moves where a tap lands.
|
||||
|
||||
### esp-emu --board e32r40t
|
||||
|
||||
The same controller drives this board under a second entry point, so `boot`,
|
||||
`wait-log`, `wait-frame`, `capture`, `memory` and `run` behave identically. It
|
||||
keeps its own state directory, and board-specific commands are gated: `tap` is
|
||||
rejected on `xteink`, `press`/`hold`/`release` on `e32r40t`.
|
||||
|
||||
```sh
|
||||
nix run .#esp-emu -- --board e32r40t boot .pio/build/esp32-32e/firmware.bin --sdcard sdcard/ --fresh-sd
|
||||
nix run .#esp-emu -- --board e32r40t tap 60 95 # panel pixels, 320x480
|
||||
nix run .#esp-emu -- --board e32r40t drag 60 400 60 180 # press, interpolate, release
|
||||
nix run .#esp-emu -- --board e32r40t touch-hold 60 95 # manual gesture: hold, move, release
|
||||
nix run .#esp-emu -- --board e32r40t touch-release 200 300
|
||||
nix run .#esp-emu -- --board e32r40t capture screen.png
|
||||
nix run .#esp-emu -- --board e32r40t stop
|
||||
```
|
||||
|
||||
Use `drag` for scrolling rather than a run of `touch-hold`s: the guest polls the touch
|
||||
controller between redraws, so back-to-back positions collapse into a tap at the last one.
|
||||
A guest that repaints slowly can outrun even the interpolated steps, which looks like a
|
||||
flick that scrolls a fraction of the distance; `--steps` and `--step-ms` slow it down.
|
||||
|
||||
`bootloader.bin` and `partitions.bin` are read from the PlatformIO build
|
||||
directory next to `firmware.bin`; a full 4 MiB flash image is also accepted.
|
||||
The watchdog global above is applied automatically.
|
||||
|
||||
The panel console is bound to the device id `lcd`, so it can be targeted
|
||||
directly instead of relying on console ordering:
|
||||
|
||||
```
|
||||
{ "execute": "screendump", "arguments": { "filename": "shot.ppm", "device": "lcd" } }
|
||||
```
|
||||
|
||||
Interactive displays (`-display gtk` / `sdl`) drive the touch panel: pointer
|
||||
motion sets the XPT2046 position and the left button acts as pen-down.
|
||||
Touches can also be injected over QMP with `input-send-event` (`abs` x/y in
|
||||
the 0-0x7fff range, then a `btn` press/release).
|
||||
@@ -1,75 +0,0 @@
|
||||
# xteink QEMU fork
|
||||
|
||||
Espressif's QEMU fork carrying the xteink X3/X4 (ESP32-C3) machine model and a
|
||||
wasm32 TCG backend (ported from [ktock/qemu-wasm](https://github.com/ktock/qemu-wasm)),
|
||||
used by [xteink-web-emulator](../xteink-web-emulator) to run CrossPoint firmware
|
||||
in the browser.
|
||||
|
||||
## Branch
|
||||
|
||||
- `main` — the working branch. Base is Espressif's `esp-develop-9.2.2-20260417`
|
||||
(`40edccac`).
|
||||
|
||||
## Remotes
|
||||
|
||||
- `origin` — https://ssh.gitea.va.reichard.io/evan/qemu-xteink
|
||||
- `upstream` — https://github.com/espressif/qemu
|
||||
- `qemu-wasm` — https://github.com/ktock/qemu-wasm (source of the wasm32 backend)
|
||||
|
||||
## Building
|
||||
|
||||
### Native (parity check — authoritative)
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
./configure --target-list=riscv32-softmmu --with-devices-riscv32=xteink --enable-gcrypt
|
||||
ninja -C build qemu-system-riscv32
|
||||
```
|
||||
|
||||
Install the emulator controller as an editable global tool:
|
||||
|
||||
```sh
|
||||
uv tool install -e tools/xteink-emu-cli
|
||||
xteink-emu --help
|
||||
```
|
||||
|
||||
Its first `boot` builds `.#qemu-native-xteink` with Nix when `dist/qemu-native` is missing. Run `make xteink` when you want to build ahead of time.
|
||||
|
||||
### Agent emulator
|
||||
|
||||
```sh
|
||||
nix run .#xteink-emu -- boot firmware.bin
|
||||
nix run .#xteink-emu -- boot firmware.bin --sdcard sdcard.img
|
||||
nix run .#xteink-emu -- boot firmware.bin --sdcard test-library/ --fresh-sd
|
||||
nix run .#xteink-emu -- wait-log 'ready' --timeout 30
|
||||
nix run .#xteink-emu -- press bottom-4 bottom-4 bottom-2
|
||||
nix run .#xteink-emu -- wait-frame 2
|
||||
nix run .#xteink-emu -- capture screen.png
|
||||
nix run .#xteink-emu -- memory
|
||||
nix run .#xteink-emu -- stop
|
||||
```
|
||||
|
||||
Use one state for a complete flow:
|
||||
|
||||
```sh
|
||||
nix run .#xteink-emu -- --state /tmp/reader-test run flow.xteink
|
||||
# Omit flow.xteink to read command lines from stdin.
|
||||
```
|
||||
|
||||
`boot` replaces the emulator already running in that state. Without `--sdcard`, it creates or reuses `$STATE/sdcard.img`. An image path is used directly; a directory is merged into the state image before every boot, with host files winning conflicts and other image files preserved. `--fresh-sd` recreates a state-owned image before importing the directory. The source directory is never modified.
|
||||
|
||||
The installable application lives in `tools/xteink-emu-cli/`; `uv tool install -e tools/xteink-emu-cli` exposes the global `xteink-emu` command and reflects checkout edits immediately. `press` waits for debounced guest ADC reads, so multiple presses do not need display waits. `wait-log` advances a persistent cursor and fails on timeout; use `wait-frame` only when visible rendering is itself under test. `capture` records the current panel without a static delay.
|
||||
|
||||
`web` serves a browser UI for firmware, SD images, controls, screen, logs, and SD files. `memory` reports RAM high-water marks; QEMU zeroes RAM at reset, so firmware heap logs remain authoritative for live usage. State defaults to `/tmp/xteink-emu-$UID`. `interactive` opens a native window and maps Left/Right, 1–4, and P to device buttons. The emulator exposes the open Wi-Fi network `qemu` through QEMU user-mode NAT.
|
||||
|
||||
### WebAssembly
|
||||
|
||||
Driven by the parent repo, not here:
|
||||
|
||||
```sh
|
||||
cd ../xteink-web-emulator
|
||||
make web-dev # incremental build straight from this tree
|
||||
```
|
||||
|
||||
The parent's `scripts/build-qemu-wasm.sh` compiles this checkout inside the
|
||||
emsdk Docker image defined in `xteink-web-emulator/qemu/wasm/Dockerfile`.
|
||||
@@ -61,7 +61,7 @@
|
||||
export PKG_CONFIG_PATH=${pkgs.libslirp}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}
|
||||
./configure \
|
||||
--prefix=$out \
|
||||
--target-list=riscv32-softmmu \
|
||||
--target-list=riscv32-softmmu,xtensa-softmmu \
|
||||
--with-devices-riscv32=xteink \
|
||||
--with-coroutine=ucontext \
|
||||
--without-default-features \
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
ninja -C build qemu-system-riscv32
|
||||
ninja -C build qemu-system-riscv32 qemu-system-xtensa
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
@@ -89,31 +89,32 @@
|
||||
runHook postInstall
|
||||
'';
|
||||
};
|
||||
xteink-emu-cli = pkgs.python3Packages.buildPythonApplication {
|
||||
pname = "xteink-emu-cli";
|
||||
esp-emu-cli = pkgs.python3Packages.buildPythonApplication {
|
||||
pname = "esp-emu-cli";
|
||||
version = "0.1.0";
|
||||
pyproject = true;
|
||||
src = ./tools/xteink-emu-cli;
|
||||
src = ./tools/esp-emu-cli;
|
||||
build-system = [ pkgs.python3Packages.hatchling ];
|
||||
};
|
||||
xteink-emu = pkgs.writeShellApplication {
|
||||
name = "xteink-emu";
|
||||
esp-emu = pkgs.writeShellApplication {
|
||||
name = "esp-emu";
|
||||
runtimeInputs = [ pkgs.mtools pkgs.dosfstools ];
|
||||
text = ''
|
||||
export XTEINK_QEMU=${qemu-native-xteink}/bin/qemu-system-riscv32
|
||||
export E32R40T_QEMU=${qemu-native-xteink}/bin/qemu-system-xtensa
|
||||
export XTEINK_BIOS=${qemu-native-xteink}/share/qemu
|
||||
export XTEINK_BOOTLOADER=${./scripts/assets/esp32c3-bootloader.bin}
|
||||
export XTEINK_PARTITIONS=${./scripts/assets/xteink-partitions.bin}
|
||||
exec ${xteink-emu-cli}/bin/xteink-emu "$@"
|
||||
exec ${esp-emu-cli}/bin/esp-emu "$@"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.qemu-native-xteink = qemu-native-xteink;
|
||||
packages.xteink-emu = xteink-emu;
|
||||
apps.xteink-emu = {
|
||||
packages.esp-emu = esp-emu;
|
||||
apps.esp-emu = {
|
||||
type = "app";
|
||||
program = "${xteink-emu}/bin/xteink-emu";
|
||||
program = "${esp-emu}/bin/esp-emu";
|
||||
};
|
||||
|
||||
# Native build/test shell. The wasm target is built by the parent
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
* E32R40T 4.0" display module: ST7796S TFT panel (CS=GPIO15, DC=GPIO2) and
|
||||
* XPT2046 resistive touch (CS=GPIO33), both on ESP32 SPI2. Two thin SSI
|
||||
* peripherals share one Esp32LcdBoard state since SSI models one CS each.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "qemu/osdep.h"
|
||||
#include "qemu/module.h"
|
||||
#include "qemu/timer.h"
|
||||
#include "hw/irq.h"
|
||||
#include "hw/qdev-properties.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/ssi/ssi.h"
|
||||
#include "ui/console.h"
|
||||
#include "ui/input.h"
|
||||
#include "hw/display/esp32_lcd_st7796.h"
|
||||
|
||||
#define WIDTH 320
|
||||
#define HEIGHT 480
|
||||
|
||||
#define ST7796_CMD_CASET 0x2A
|
||||
#define ST7796_CMD_RASET 0x2B
|
||||
#define ST7796_CMD_RAMWR 0x2C
|
||||
#define ST7796_CMD_MADCTL 0x36
|
||||
|
||||
#define MADCTL_MY 0x80
|
||||
#define MADCTL_MX 0x40
|
||||
#define MADCTL_MV 0x20
|
||||
|
||||
#define XPT2046_CMD_X 0x90
|
||||
#define XPT2046_CMD_Y 0xD0
|
||||
#define XPT2046_CMD_Z1 0xB0
|
||||
#define XPT2046_CMD_Z2 0xC0
|
||||
|
||||
/* Show the settled framebuffer after a frame of inactivity instead of
|
||||
* redrawing the console on every RAMWR pixel */
|
||||
#define REFRESH_IDLE_NS (20 * SCALE_MS)
|
||||
|
||||
static uint32_t lcd_frame_generation;
|
||||
|
||||
struct Esp32LcdBoard {
|
||||
SysBusDevice parent_obj;
|
||||
QemuConsole *console;
|
||||
QEMUTimer *refresh_timer;
|
||||
QemuInputHandlerState *input;
|
||||
qemu_irq touch_irq;
|
||||
bool dc;
|
||||
bool touched;
|
||||
uint16_t touch_x;
|
||||
uint16_t touch_y;
|
||||
uint8_t command;
|
||||
uint8_t madctl;
|
||||
uint8_t touch_phase;
|
||||
uint16_t be16;
|
||||
uint8_t addr_pos;
|
||||
uint8_t addr_buf[4];
|
||||
uint32_t pixel;
|
||||
uint8_t pixel_byte;
|
||||
uint16_t col_start, col_end, row_start, row_end;
|
||||
uint16_t cur_col, cur_row;
|
||||
bool dirty;
|
||||
uint16_t fb[WIDTH * HEIGHT];
|
||||
};
|
||||
|
||||
typedef struct Esp32LcdPanelState {
|
||||
SSIPeripheral parent_obj;
|
||||
Esp32LcdBoard *board;
|
||||
} Esp32LcdPanelState;
|
||||
|
||||
typedef struct Esp32LcdTouchState {
|
||||
SSIPeripheral parent_obj;
|
||||
Esp32LcdBoard *board;
|
||||
} Esp32LcdTouchState;
|
||||
|
||||
OBJECT_DECLARE_SIMPLE_TYPE(Esp32LcdPanelState, ESP32_LCD_ST7796)
|
||||
OBJECT_DECLARE_SIMPLE_TYPE(Esp32LcdTouchState, ESP32_LCD_XPT2046)
|
||||
|
||||
/* MADCTL maps the guest's address window onto the glass: MV swaps the axes so a
|
||||
* landscape guest addresses 480 columns, MX and MY mirror them. Rotation 0 in
|
||||
* the vendor init sets MX, so that is the identity baseline here. */
|
||||
static void lcd_gram_to_panel(Esp32LcdBoard *b, int col, int row, int *px, int *py)
|
||||
{
|
||||
if (b->madctl & MADCTL_MV) {
|
||||
*px = (b->madctl & MADCTL_MY) ? row : WIDTH - 1 - row;
|
||||
*py = (b->madctl & MADCTL_MX) ? HEIGHT - 1 - col : col;
|
||||
} else {
|
||||
*px = (b->madctl & MADCTL_MX) ? col : WIDTH - 1 - col;
|
||||
*py = (b->madctl & MADCTL_MY) ? HEIGHT - 1 - row : row;
|
||||
}
|
||||
}
|
||||
|
||||
static void lcd_render(Esp32LcdBoard *b)
|
||||
{
|
||||
DisplaySurface *surface = qemu_console_surface(b->console);
|
||||
if (!surface || !surface_data(surface)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t *dest = surface_data(surface);
|
||||
int stride = surface_stride(surface) / 4;
|
||||
|
||||
/* The glass is physically 320x480 whatever MADCTL says, so a rotated guest
|
||||
* simply draws sideways here, exactly as it looks on the real board. */
|
||||
for (int y = 0; y < HEIGHT; y++) {
|
||||
for (int x = 0; x < WIDTH; x++) {
|
||||
uint16_t color = b->fb[y * WIDTH + x];
|
||||
dest[y * stride + x] = ((color & 0xf800) << 8) |
|
||||
((color & 0x07e0) << 5) |
|
||||
((color & 0x001f) << 3);
|
||||
}
|
||||
}
|
||||
|
||||
qatomic_inc(&lcd_frame_generation);
|
||||
dpy_gfx_update(b->console, 0, 0, WIDTH, HEIGHT);
|
||||
b->dirty = false;
|
||||
}
|
||||
|
||||
static void lcd_refresh_idle(void *opaque)
|
||||
{
|
||||
Esp32LcdBoard *b = opaque;
|
||||
if (b->dirty) {
|
||||
lcd_render(b);
|
||||
}
|
||||
}
|
||||
|
||||
static void lcd_reset_board(Esp32LcdBoard *b)
|
||||
{
|
||||
b->dc = false;
|
||||
b->touched = false;
|
||||
b->command = 0;
|
||||
b->madctl = 0;
|
||||
b->touch_phase = 0;
|
||||
b->addr_pos = 0;
|
||||
b->dirty = false;
|
||||
memset(b->fb, 0xff, sizeof(b->fb));
|
||||
lcd_render(b);
|
||||
}
|
||||
|
||||
static void lcd_set_dc(void *opaque, int n, int level)
|
||||
{
|
||||
Esp32LcdPanelState *s = ESP32_LCD_ST7796(opaque);
|
||||
s->board->dc = level;
|
||||
}
|
||||
|
||||
static void lcd_touch_event(DeviceState *dev, QemuConsole *src,
|
||||
InputEvent *evt)
|
||||
{
|
||||
Esp32LcdPanelState *s = ESP32_LCD_ST7796(dev);
|
||||
Esp32LcdBoard *b = s->board;
|
||||
|
||||
switch (evt->type) {
|
||||
/* Touch coordinates are physical: the resistive panel knows nothing about
|
||||
* MADCTL, so rotating the guest must not move where a tap lands. */
|
||||
case INPUT_EVENT_KIND_ABS:
|
||||
if (evt->u.abs.data->axis == INPUT_AXIS_X) {
|
||||
b->touch_x = qemu_input_scale_axis(evt->u.abs.data->value,
|
||||
INPUT_EVENT_ABS_MIN, INPUT_EVENT_ABS_MAX, 0, WIDTH - 1);
|
||||
} else if (evt->u.abs.data->axis == INPUT_AXIS_Y) {
|
||||
b->touch_y = qemu_input_scale_axis(evt->u.abs.data->value,
|
||||
INPUT_EVENT_ABS_MIN, INPUT_EVENT_ABS_MAX, 0, HEIGHT - 1);
|
||||
}
|
||||
break;
|
||||
case INPUT_EVENT_KIND_BTN:
|
||||
if (evt->u.btn.data->button == INPUT_BUTTON_LEFT) {
|
||||
b->touched = evt->u.btn.data->down;
|
||||
qemu_set_irq(b->touch_irq, b->touched ? 0 : 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void lcd_touch_sync(DeviceState *dev)
|
||||
{
|
||||
}
|
||||
|
||||
static QemuInputHandler lcd_touch_handler = {
|
||||
.name = "e32r40t touch",
|
||||
.mask = INPUT_EVENT_MASK_BTN | INPUT_EVENT_MASK_ABS,
|
||||
.event = lcd_touch_event,
|
||||
.sync = lcd_touch_sync,
|
||||
};
|
||||
|
||||
static uint32_t lcd_panel_transfer(SSIPeripheral *peripheral, uint32_t value)
|
||||
{
|
||||
Esp32LcdBoard *b = ESP32_LCD_ST7796(peripheral)->board;
|
||||
uint8_t byte = value;
|
||||
|
||||
if (!b->dc) {
|
||||
b->command = byte;
|
||||
b->addr_pos = 0;
|
||||
b->pixel_byte = 0;
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
switch (b->command) {
|
||||
case ST7796_CMD_CASET:
|
||||
case ST7796_CMD_RASET:
|
||||
if (b->addr_pos < sizeof(b->addr_buf)) {
|
||||
b->addr_buf[b->addr_pos++] = byte;
|
||||
}
|
||||
if (b->addr_pos == sizeof(b->addr_buf)) {
|
||||
uint16_t start = (b->addr_buf[0] << 8) | b->addr_buf[1];
|
||||
uint16_t end = (b->addr_buf[2] << 8) | b->addr_buf[3];
|
||||
if (b->command == ST7796_CMD_CASET) {
|
||||
b->col_start = start;
|
||||
b->col_end = end;
|
||||
b->cur_col = start;
|
||||
} else {
|
||||
b->row_start = start;
|
||||
b->row_end = end;
|
||||
b->cur_row = start;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ST7796_CMD_MADCTL:
|
||||
b->madctl = byte;
|
||||
break;
|
||||
case ST7796_CMD_RAMWR:
|
||||
if (b->pixel_byte == 0) {
|
||||
b->pixel = byte << 8;
|
||||
b->pixel_byte = 1;
|
||||
} else {
|
||||
b->pixel |= byte;
|
||||
b->pixel_byte = 0;
|
||||
if (b->cur_col <= b->col_end && b->cur_row <= b->row_end) {
|
||||
int px, py;
|
||||
lcd_gram_to_panel(b, b->cur_col, b->cur_row, &px, &py);
|
||||
if (px >= 0 && px < WIDTH && py >= 0 && py < HEIGHT) {
|
||||
b->fb[py * WIDTH + px] = b->pixel;
|
||||
}
|
||||
}
|
||||
if (++b->cur_col > b->col_end) {
|
||||
b->cur_col = b->col_start;
|
||||
if (++b->cur_row > b->row_end) {
|
||||
b->cur_row = b->row_start;
|
||||
}
|
||||
}
|
||||
b->dirty = true;
|
||||
timer_mod(b->refresh_timer,
|
||||
qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + REFRESH_IDLE_NS);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
static uint16_t lcd_touch_adc(Esp32LcdBoard *b, uint8_t cmd)
|
||||
{
|
||||
switch (cmd & 0xF0) {
|
||||
/* The controller's axes are swapped relative to the panel, as measured on real
|
||||
* E32R40T hardware: X reads the long axis inverted, Y reads the short axis. */
|
||||
case XPT2046_CMD_X:
|
||||
return 3800 - b->touch_y * 3600 / (HEIGHT - 1);
|
||||
case XPT2046_CMD_Y:
|
||||
return 240 + b->touch_x * 3560 / (WIDTH - 1);
|
||||
case XPT2046_CMD_Z1:
|
||||
return b->touched ? 500 : 0;
|
||||
case XPT2046_CMD_Z2:
|
||||
return b->touched ? 3500 : 4095;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t lcd_touch_transfer(SSIPeripheral *peripheral, uint32_t value)
|
||||
{
|
||||
Esp32LcdBoard *b = ESP32_LCD_XPT2046(peripheral)->board;
|
||||
|
||||
if (b->touch_phase == 0) {
|
||||
b->be16 = lcd_touch_adc(b, value) << 3;
|
||||
b->touch_phase = 1;
|
||||
return 0;
|
||||
}
|
||||
if (b->touch_phase == 1) {
|
||||
b->touch_phase = 2;
|
||||
return b->be16 >> 8;
|
||||
}
|
||||
uint8_t result = b->be16;
|
||||
b->be16 = lcd_touch_adc(b, value) << 3;
|
||||
b->touch_phase = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
static void lcd_invalidate(void *opaque)
|
||||
{
|
||||
Esp32LcdBoard *b = opaque;
|
||||
lcd_render(b);
|
||||
}
|
||||
|
||||
static void lcd_update(void *opaque)
|
||||
{
|
||||
}
|
||||
|
||||
static const GraphicHwOps lcd_graphics_ops = {
|
||||
.invalidate = lcd_invalidate,
|
||||
.gfx_update = lcd_update,
|
||||
};
|
||||
|
||||
static Esp32LcdBoard *lcd_get_board(DeviceState *dev)
|
||||
{
|
||||
return ESP32_LCD_BOARD(
|
||||
object_property_get_link(OBJECT(dev), "board", &error_abort));
|
||||
}
|
||||
|
||||
static void lcd_panel_init(Object *obj)
|
||||
{
|
||||
qdev_init_gpio_in_named(DEVICE(obj), lcd_set_dc, ESP32_LCD_ST7796_DC, 1);
|
||||
}
|
||||
|
||||
static void lcd_panel_realize(SSIPeripheral *peripheral, Error **errp)
|
||||
{
|
||||
Esp32LcdPanelState *s = ESP32_LCD_ST7796(peripheral);
|
||||
s->board = lcd_get_board(DEVICE(peripheral));
|
||||
s->board->input = qemu_input_handler_register(DEVICE(s),
|
||||
&lcd_touch_handler);
|
||||
qemu_input_handler_activate(s->board->input);
|
||||
}
|
||||
|
||||
/* Each CS assertion starts a fresh command; otherwise the 16-bit reads that
|
||||
* follow drift one byte out of phase for the rest of the session. */
|
||||
static int lcd_touch_set_cs(SSIPeripheral *peripheral, bool level)
|
||||
{
|
||||
ESP32_LCD_XPT2046(peripheral)->board->touch_phase = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void lcd_touch_realize(SSIPeripheral *peripheral, Error **errp)
|
||||
{
|
||||
Esp32LcdTouchState *s = ESP32_LCD_XPT2046(peripheral);
|
||||
s->board = lcd_get_board(DEVICE(peripheral));
|
||||
}
|
||||
|
||||
static void lcd_board_reset(DeviceState *dev)
|
||||
{
|
||||
lcd_reset_board(ESP32_LCD_BOARD(dev));
|
||||
}
|
||||
|
||||
static void lcd_board_init(Object *obj)
|
||||
{
|
||||
Esp32LcdBoard *b = ESP32_LCD_BOARD(obj);
|
||||
|
||||
object_property_add_uint32_ptr(obj, "frame-generation",
|
||||
&lcd_frame_generation, OBJ_PROP_FLAG_READ);
|
||||
b->refresh_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, lcd_refresh_idle, b);
|
||||
qdev_init_gpio_out_named(DEVICE(obj), &b->touch_irq,
|
||||
ESP32_LCD_XPT2046_IRQ, 1);
|
||||
qemu_set_irq(b->touch_irq, 1);
|
||||
}
|
||||
|
||||
/* Deferred to realize: an unparented device has no canonical path, so the
|
||||
* console's "device" link would silently stay NULL and QMP could not find it. */
|
||||
static void lcd_board_realize(DeviceState *dev, Error **errp)
|
||||
{
|
||||
Esp32LcdBoard *b = ESP32_LCD_BOARD(dev);
|
||||
|
||||
b->console = graphic_console_init(dev, 0, &lcd_graphics_ops, b);
|
||||
dpy_gfx_replace_surface(b->console,
|
||||
qemu_create_displaysurface(WIDTH, HEIGHT));
|
||||
}
|
||||
|
||||
static void lcd_board_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
dc->realize = lcd_board_realize;
|
||||
device_class_set_legacy_reset(dc, lcd_board_reset);
|
||||
dc->user_creatable = false;
|
||||
}
|
||||
|
||||
static const Property lcd_link_properties[] = {
|
||||
DEFINE_PROP_LINK("board", Esp32LcdPanelState, board,
|
||||
TYPE_ESP32_LCD_BOARD, Esp32LcdBoard *),
|
||||
DEFINE_PROP_END_OF_LIST(),
|
||||
};
|
||||
|
||||
static const Property lcd_touch_link_properties[] = {
|
||||
DEFINE_PROP_LINK("board", Esp32LcdTouchState, board,
|
||||
TYPE_ESP32_LCD_BOARD, Esp32LcdBoard *),
|
||||
DEFINE_PROP_END_OF_LIST(),
|
||||
};
|
||||
|
||||
static void lcd_panel_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
SSIPeripheralClass *ssi = SSI_PERIPHERAL_CLASS(klass);
|
||||
|
||||
ssi->realize = lcd_panel_realize;
|
||||
ssi->transfer = lcd_panel_transfer;
|
||||
ssi->cs_polarity = SSI_CS_LOW;
|
||||
device_class_set_props(dc, lcd_link_properties);
|
||||
dc->user_creatable = false;
|
||||
}
|
||||
|
||||
static void lcd_touch_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
SSIPeripheralClass *ssi = SSI_PERIPHERAL_CLASS(klass);
|
||||
|
||||
ssi->realize = lcd_touch_realize;
|
||||
ssi->transfer = lcd_touch_transfer;
|
||||
ssi->set_cs = lcd_touch_set_cs;
|
||||
ssi->cs_polarity = SSI_CS_LOW;
|
||||
device_class_set_props(dc, lcd_touch_link_properties);
|
||||
dc->user_creatable = false;
|
||||
}
|
||||
|
||||
static const TypeInfo lcd_board_info = {
|
||||
.name = TYPE_ESP32_LCD_BOARD,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(Esp32LcdBoard),
|
||||
.instance_init = lcd_board_init,
|
||||
.class_init = lcd_board_class_init,
|
||||
};
|
||||
|
||||
static const TypeInfo lcd_panel_info = {
|
||||
.name = TYPE_ESP32_LCD_ST7796,
|
||||
.parent = TYPE_SSI_PERIPHERAL,
|
||||
.instance_size = sizeof(Esp32LcdPanelState),
|
||||
.instance_init = lcd_panel_init,
|
||||
.class_init = lcd_panel_class_init,
|
||||
};
|
||||
|
||||
static const TypeInfo lcd_touch_info = {
|
||||
.name = TYPE_ESP32_LCD_XPT2046,
|
||||
.parent = TYPE_SSI_PERIPHERAL,
|
||||
.instance_size = sizeof(Esp32LcdTouchState),
|
||||
.class_init = lcd_touch_class_init,
|
||||
};
|
||||
|
||||
static void lcd_register_types(void)
|
||||
{
|
||||
type_register_static(&lcd_board_info);
|
||||
type_register_static(&lcd_panel_info);
|
||||
type_register_static(&lcd_touch_info);
|
||||
}
|
||||
|
||||
type_init(lcd_register_types)
|
||||
@@ -33,6 +33,7 @@ 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_XTENSA_ESP32', if_true: files('esp32_lcd_st7796.c'))
|
||||
|
||||
system_ss.add(when: 'CONFIG_VGA', if_true: files('vga.c'))
|
||||
system_ss.add(when: 'CONFIG_VIRTIO', if_true: files('virtio-dmabuf.c'))
|
||||
|
||||
+80
-25
@@ -23,14 +23,28 @@
|
||||
#define GPIO_OUT 0x04
|
||||
#define GPIO_OUT_W1TS 0x08
|
||||
#define GPIO_OUT_W1TC 0x0c
|
||||
/* pins 32-39 */
|
||||
#define GPIO_OUT1 0x10
|
||||
#define GPIO_OUT1_W1TS 0x14
|
||||
#define GPIO_OUT1_W1TC 0x18
|
||||
#define GPIO_ENABLE 0x20
|
||||
#define GPIO_ENABLE_W1TS 0x24
|
||||
#define GPIO_ENABLE_W1TC 0x28
|
||||
#define GPIO_ENABLE1 0x2c
|
||||
#define GPIO_ENABLE1_W1TS 0x30
|
||||
#define GPIO_ENABLE1_W1TC 0x34
|
||||
#define GPIO_IN 0x3c
|
||||
#define GPIO_IN1 0x40
|
||||
#define GPIO_STATUS 0x44
|
||||
#define GPIO_STATUS_W1TS 0x48
|
||||
#define GPIO_STATUS_W1TC 0x4c
|
||||
#define GPIO_STATUS1 0x50
|
||||
#define GPIO_STATUS1_W1TS 0x54
|
||||
#define GPIO_STATUS1_W1TC 0x58
|
||||
#define GPIO_PCPU_INT 0x5c
|
||||
#define GPIO_PCPU_INT1 0x60
|
||||
|
||||
#define HI(x) ((uint32_t)((x) >> 32)) /* pins 32-39 */
|
||||
|
||||
#define GPIO_PIN_INT_TYPE_SHIFT 7
|
||||
#define GPIO_PIN_INT_TYPE_MASK 0x7
|
||||
@@ -47,21 +61,21 @@
|
||||
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))
|
||||
int level = (s->output_enable & (1ULL << i))
|
||||
? !!(s->output_level & (1ULL << i))
|
||||
: 1;
|
||||
qemu_set_irq(s->output_lines[i], level);
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t esp32_gpio_enabled_interrupts(Esp32GpioState *s)
|
||||
static uint64_t esp32_gpio_enabled_interrupts(Esp32GpioState *s)
|
||||
{
|
||||
uint32_t enabled = 0;
|
||||
uint64_t enabled = 0;
|
||||
|
||||
for (int pin = 0; pin < ESP32_GPIO_COUNT; pin++) {
|
||||
uint32_t interrupt_enable = s->pin_config[pin] >> GPIO_PIN_INT_ENA_SHIFT;
|
||||
if (interrupt_enable & GPIO_PIN_INT_ENA_CPU0) {
|
||||
enabled |= BIT(pin);
|
||||
enabled |= 1ULL << pin;
|
||||
}
|
||||
}
|
||||
return enabled;
|
||||
@@ -75,31 +89,31 @@ static void esp32_gpio_update_irq(Esp32GpioState *s)
|
||||
|
||||
static void esp32_gpio_latch_level_interrupts(Esp32GpioState *s)
|
||||
{
|
||||
uint32_t levels = qatomic_read(&s->input_level);
|
||||
uint64_t levels = qatomic_read(&s->input_level);
|
||||
|
||||
for (int pin = 0; pin < ESP32_GPIO_COUNT; pin++) {
|
||||
uint32_t type = extract32(s->pin_config[pin],
|
||||
GPIO_PIN_INT_TYPE_SHIFT,
|
||||
3);
|
||||
bool level = !!(levels & BIT(pin));
|
||||
bool level = !!(levels & (1ULL << pin));
|
||||
if ((type == GPIO_INTR_LOW_LEVEL && !level) ||
|
||||
(type == GPIO_INTR_HIGH_LEVEL && level)) {
|
||||
s->interrupt_status |= BIT(pin);
|
||||
s->interrupt_status |= 1ULL << pin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void esp32_gpio_set_input_level(Esp32GpioState *s, int pin, bool level)
|
||||
{
|
||||
uint32_t old_level;
|
||||
uint32_t new_level;
|
||||
uint64_t old_level;
|
||||
uint64_t new_level;
|
||||
|
||||
do {
|
||||
old_level = qatomic_read(&s->input_level);
|
||||
new_level = deposit32(old_level, pin, 1, level);
|
||||
new_level = deposit64(old_level, pin, 1, level);
|
||||
} while (qatomic_cmpxchg(&s->input_level, old_level, new_level) != old_level);
|
||||
|
||||
bool old_pin_level = !!(old_level & BIT(pin));
|
||||
bool old_pin_level = !!(old_level & (1ULL << pin));
|
||||
uint32_t type = extract32(s->pin_config[pin],
|
||||
GPIO_PIN_INT_TYPE_SHIFT, 3);
|
||||
if ((!old_pin_level && level && type == GPIO_INTR_POSEDGE) ||
|
||||
@@ -107,7 +121,7 @@ void esp32_gpio_set_input_level(Esp32GpioState *s, int pin, bool level)
|
||||
(old_pin_level != level && type == GPIO_INTR_ANYEDGE) ||
|
||||
(!level && type == GPIO_INTR_LOW_LEVEL) ||
|
||||
(level && type == GPIO_INTR_HIGH_LEVEL)) {
|
||||
s->interrupt_status |= BIT(pin);
|
||||
s->interrupt_status |= 1ULL << pin;
|
||||
}
|
||||
esp32_gpio_update_irq(s);
|
||||
}
|
||||
@@ -122,7 +136,7 @@ static void esp32_gpio_get_input(Object *obj, Visitor *v, const char *name,
|
||||
{
|
||||
Esp32GpioState *s = ESP32_GPIO(obj);
|
||||
int pin = GPOINTER_TO_INT(opaque);
|
||||
bool level = extract32(qatomic_read(&s->input_level), pin, 1);
|
||||
bool level = extract64(qatomic_read(&s->input_level), pin, 1);
|
||||
visit_type_bool(v, name, &level, errp);
|
||||
}
|
||||
|
||||
@@ -145,17 +159,27 @@ static uint64_t esp32_gpio_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
|
||||
switch (addr) {
|
||||
case GPIO_OUT:
|
||||
return s->output_level;
|
||||
return (uint32_t)s->output_level;
|
||||
case GPIO_OUT1:
|
||||
return HI(s->output_level);
|
||||
case GPIO_ENABLE:
|
||||
return s->output_enable;
|
||||
return (uint32_t)s->output_enable;
|
||||
case GPIO_ENABLE1:
|
||||
return HI(s->output_enable);
|
||||
case A_GPIO_STRAP:
|
||||
return s->strap_mode;
|
||||
case GPIO_IN:
|
||||
return qatomic_read(&s->input_level);
|
||||
return (uint32_t)qatomic_read(&s->input_level);
|
||||
case GPIO_IN1:
|
||||
return HI(qatomic_read(&s->input_level));
|
||||
case GPIO_STATUS:
|
||||
return s->interrupt_status;
|
||||
return (uint32_t)s->interrupt_status;
|
||||
case GPIO_STATUS1:
|
||||
return HI(s->interrupt_status);
|
||||
case GPIO_PCPU_INT:
|
||||
return s->interrupt_status & esp32_gpio_enabled_interrupts(s);
|
||||
return (uint32_t)(s->interrupt_status & esp32_gpio_enabled_interrupts(s));
|
||||
case GPIO_PCPU_INT1:
|
||||
return HI(s->interrupt_status & esp32_gpio_enabled_interrupts(s));
|
||||
default:
|
||||
if (addr >= s->pin_config_base &&
|
||||
addr < s->pin_config_base + sizeof(s->pin_config)) {
|
||||
@@ -180,25 +204,43 @@ static void esp32_gpio_write(void *opaque, hwaddr addr,
|
||||
|
||||
switch (addr) {
|
||||
case GPIO_OUT:
|
||||
s->output_level = value;
|
||||
s->output_level = (s->output_level & ~0xffffffffULL) | value;
|
||||
break;
|
||||
case GPIO_OUT_W1TS:
|
||||
s->output_level |= value;
|
||||
break;
|
||||
case GPIO_OUT_W1TC:
|
||||
s->output_level &= ~value;
|
||||
s->output_level &= ~(uint64_t)value;
|
||||
break;
|
||||
case GPIO_OUT1:
|
||||
s->output_level = (s->output_level & 0xffffffffULL) | (value << 32);
|
||||
break;
|
||||
case GPIO_OUT1_W1TS:
|
||||
s->output_level |= (uint64_t)value << 32;
|
||||
break;
|
||||
case GPIO_OUT1_W1TC:
|
||||
s->output_level &= ~((uint64_t)value << 32);
|
||||
break;
|
||||
case GPIO_ENABLE:
|
||||
s->output_enable = value;
|
||||
s->output_enable = (s->output_enable & ~0xffffffffULL) | value;
|
||||
break;
|
||||
case GPIO_ENABLE_W1TS:
|
||||
s->output_enable |= value;
|
||||
break;
|
||||
case GPIO_ENABLE_W1TC:
|
||||
s->output_enable &= ~value;
|
||||
s->output_enable &= ~(uint64_t)value;
|
||||
break;
|
||||
case GPIO_ENABLE1:
|
||||
s->output_enable = (s->output_enable & 0xffffffffULL) | (value << 32);
|
||||
break;
|
||||
case GPIO_ENABLE1_W1TS:
|
||||
s->output_enable |= (uint64_t)value << 32;
|
||||
break;
|
||||
case GPIO_ENABLE1_W1TC:
|
||||
s->output_enable &= ~((uint64_t)value << 32);
|
||||
break;
|
||||
case GPIO_STATUS:
|
||||
s->interrupt_status = value;
|
||||
s->interrupt_status = (s->interrupt_status & ~0xffffffffULL) | value;
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
case GPIO_STATUS_W1TS:
|
||||
@@ -206,7 +248,20 @@ static void esp32_gpio_write(void *opaque, hwaddr addr,
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
case GPIO_STATUS_W1TC:
|
||||
s->interrupt_status &= ~value;
|
||||
s->interrupt_status &= ~(uint64_t)value;
|
||||
esp32_gpio_latch_level_interrupts(s);
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
case GPIO_STATUS1:
|
||||
s->interrupt_status = (s->interrupt_status & 0xffffffffULL) | (value << 32);
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
case GPIO_STATUS1_W1TS:
|
||||
s->interrupt_status |= (uint64_t)value << 32;
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
case GPIO_STATUS1_W1TC:
|
||||
s->interrupt_status &= ~((uint64_t)value << 32);
|
||||
esp32_gpio_latch_level_interrupts(s);
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "qemu/osdep.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/misc/esp32_ana.h"
|
||||
#include "esp32_wlan.h"
|
||||
|
||||
int esp32_wifi_channel;
|
||||
|
||||
static uint64_t esp32_ana_read(void *opaque, hwaddr addr, unsigned size)
|
||||
{
|
||||
Esp32AnaState *s = ESP32_ANA(opaque);
|
||||
|
||||
switch (addr) {
|
||||
case 0x04:
|
||||
return 0xfdffffff;
|
||||
case 0x44:
|
||||
case 0x4c:
|
||||
case 0xc4:
|
||||
return 0xffffffff;
|
||||
default:
|
||||
return s->mem[addr / 4];
|
||||
}
|
||||
}
|
||||
|
||||
static void esp32_ana_write(void *opaque, hwaddr addr, uint64_t value,
|
||||
unsigned size)
|
||||
{
|
||||
Esp32AnaState *s = ESP32_ANA(opaque);
|
||||
|
||||
if (addr == 0xc4 && (value & 0xff) != 0xff) {
|
||||
/* The emulator exposes one AP on channel 1; proprietary PHY tuning values
|
||||
* vary between IDF releases, so no wider channel decoder is needed. */
|
||||
esp32_wifi_channel = 1;
|
||||
}
|
||||
s->mem[addr / 4] = value;
|
||||
}
|
||||
|
||||
static const MemoryRegionOps esp32_ana_ops = {
|
||||
.read = esp32_ana_read,
|
||||
.write = esp32_ana_write,
|
||||
.endianness = DEVICE_LITTLE_ENDIAN,
|
||||
};
|
||||
|
||||
static void esp32_ana_init(Object *obj)
|
||||
{
|
||||
Esp32AnaState *s = ESP32_ANA(obj);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
|
||||
|
||||
memory_region_init_io(&s->iomem, obj, &esp32_ana_ops, s,
|
||||
TYPE_ESP32_ANA, 0x1000);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
}
|
||||
|
||||
static const TypeInfo esp32_ana_info = {
|
||||
.name = TYPE_ESP32_ANA,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(Esp32AnaState),
|
||||
.instance_init = esp32_ana_init,
|
||||
};
|
||||
|
||||
static void esp32_ana_register_types(void)
|
||||
{
|
||||
type_register_static(&esp32_ana_info);
|
||||
}
|
||||
|
||||
type_init(esp32_ana_register_types)
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "qemu/osdep.h"
|
||||
#include "hw/irq.h"
|
||||
#include "hw/misc/esp32_wifi.h"
|
||||
#include "hw/qdev-properties.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "exec/address-spaces.h"
|
||||
#include "esp32_wlan_packet.h"
|
||||
|
||||
extern access_point_info access_points[];
|
||||
extern int nb_aps;
|
||||
|
||||
static uint64_t esp32_wifi_read(void *opaque, hwaddr addr, unsigned size)
|
||||
{
|
||||
Esp32WifiState *s = ESP32_WIFI(opaque);
|
||||
|
||||
switch (addr) {
|
||||
case A_WIFI_DMA_IN_STATUS:
|
||||
return 0;
|
||||
case A_WIFI_DMA_INT_STATUS:
|
||||
case A_WIFI_DMA_INT_CLR:
|
||||
return s->raw_interrupt;
|
||||
case A_WIFI_STATUS:
|
||||
case A_WIFI_DMA_OUT_STATUS:
|
||||
return 1;
|
||||
default:
|
||||
return s->mem[addr / 4];
|
||||
}
|
||||
}
|
||||
|
||||
static void esp32_wifi_raise(Esp32WifiState *s, uint32_t bits)
|
||||
{
|
||||
s->raw_interrupt |= bits;
|
||||
qemu_set_irq(s->irq, 1);
|
||||
}
|
||||
|
||||
void Esp32_WLAN_frame_delivered(Esp32WifiState *s)
|
||||
{
|
||||
esp32_wifi_raise(s, 0x80);
|
||||
}
|
||||
|
||||
static void esp32_wifi_write(void *opaque, hwaddr addr, uint64_t value,
|
||||
unsigned size)
|
||||
{
|
||||
Esp32WifiState *s = ESP32_WIFI(opaque);
|
||||
|
||||
switch (addr) {
|
||||
case A_WIFI_DMA_INLINK: {
|
||||
uint32_t offset = value & 0xfffff;
|
||||
s->dma_inlink_address = offset ? 0x3ff00000 | offset : 0;
|
||||
break;
|
||||
}
|
||||
case A_WIFI_DMA_INT_CLR:
|
||||
s->raw_interrupt &= ~value;
|
||||
if (!s->raw_interrupt) {
|
||||
qemu_set_irq(s->irq, 0);
|
||||
}
|
||||
break;
|
||||
case A_WIFI_DMA_OUTLINK:
|
||||
if ((value & 0xc0000000) && (value & 0xfffff)) {
|
||||
mac80211_frame frame;
|
||||
dma_list_item item;
|
||||
hwaddr descriptor = 0x3ff00000 | (value & 0xfffff);
|
||||
|
||||
address_space_read(&address_space_memory, descriptor,
|
||||
MEMTXATTRS_UNSPECIFIED, &item, sizeof(item));
|
||||
if (item.length > sizeof(frame)) {
|
||||
break;
|
||||
}
|
||||
address_space_read(&address_space_memory, item.address,
|
||||
MEMTXATTRS_UNSPECIFIED, &frame, item.length);
|
||||
frame.frame_length = item.length;
|
||||
frame.next_frame = NULL;
|
||||
Esp32_WLAN_handle_frame(s, &frame);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
s->mem[addr / 4] = value;
|
||||
}
|
||||
|
||||
static bool esp32_wifi_mac_matches(const uint8_t *actual, const uint8_t *filter)
|
||||
{
|
||||
return !memcmp(actual, filter, 6) || !memcmp(actual, BROADCAST, 6);
|
||||
}
|
||||
|
||||
void Esp32_sendFrame(Esp32WifiState *s, mac80211_frame *frame, int length,
|
||||
int signal_strength)
|
||||
{
|
||||
int packet_channel = esp32_wifi_channel;
|
||||
|
||||
if (!s->dma_inlink_address) {
|
||||
return;
|
||||
}
|
||||
if (frame->frame_control.type == IEEE80211_TYPE_MGT &&
|
||||
frame->frame_control.sub_type == IEEE80211_TYPE_MGT_SUBTYPE_BEACON) {
|
||||
int pos = 12;
|
||||
int data_length = length - IEEE80211_HEADER_SIZE;
|
||||
|
||||
while (pos + 2 <= data_length) {
|
||||
uint8_t tag = frame->data_and_fcs[pos];
|
||||
uint8_t tag_length = frame->data_and_fcs[pos + 1];
|
||||
if (tag == IEEE80211_BEACON_PARAM_CHANNEL && tag_length) {
|
||||
packet_channel = frame->data_and_fcs[pos + 2];
|
||||
break;
|
||||
}
|
||||
pos += 2 + tag_length;
|
||||
}
|
||||
}
|
||||
if (!packet_channel) {
|
||||
const uint8_t *address = frame->frame_control.type == IEEE80211_TYPE_DATA ?
|
||||
frame->bssid_address : frame->source_address;
|
||||
for (int i = 0; i < nb_aps; i++) {
|
||||
if (!memcmp(access_points[i].mac_address, address, 6)) {
|
||||
packet_channel = access_points[i].channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_autofree uint8_t *buffer = g_malloc0(sizeof(wifi_pkt_rx_ctrl_t) + length);
|
||||
wifi_pkt_rx_ctrl_t *pkt = (wifi_pkt_rx_ctrl_t *)buffer;
|
||||
*pkt = (wifi_pkt_rx_ctrl_t) {
|
||||
.rssi = signal_strength + (rand() % 10) + 96,
|
||||
.rate = 11,
|
||||
.legacy_length = length,
|
||||
.noise_floor = -97,
|
||||
.channel = packet_channel,
|
||||
.timestamp = qemu_clock_get_ns(QEMU_CLOCK_REALTIME) / 1000,
|
||||
.sig_len = length,
|
||||
.sig_len_copy = length,
|
||||
};
|
||||
if (esp32_wifi_mac_matches(frame->destination_address,
|
||||
(uint8_t *)s->mem + 0x40)) {
|
||||
pkt->damatch0 = 1;
|
||||
}
|
||||
if (esp32_wifi_mac_matches(frame->destination_address,
|
||||
(uint8_t *)s->mem + 0x48)) {
|
||||
pkt->damatch1 = 1;
|
||||
}
|
||||
if (esp32_wifi_mac_matches(frame->bssid_address,
|
||||
(uint8_t *)s->mem + 0x40)) {
|
||||
pkt->bssidmatch0 = 1;
|
||||
}
|
||||
if (esp32_wifi_mac_matches(frame->bssid_address,
|
||||
(uint8_t *)s->mem + 0x48)) {
|
||||
pkt->bssidmatch1 = 1;
|
||||
}
|
||||
pkt->damatch0 = 1;
|
||||
pkt->bssidmatch0 = 1;
|
||||
|
||||
memcpy(buffer + sizeof(*pkt), frame, length);
|
||||
length += sizeof(*pkt);
|
||||
|
||||
dma_list_item item;
|
||||
address_space_read(&address_space_memory, s->dma_inlink_address,
|
||||
MEMTXATTRS_UNSPECIFIED, &item, sizeof(item));
|
||||
if (length > item.size) {
|
||||
return;
|
||||
}
|
||||
address_space_write(&address_space_memory, item.address,
|
||||
MEMTXATTRS_UNSPECIFIED, buffer, length);
|
||||
item.length = length;
|
||||
item.eof = 1;
|
||||
item.owner = 0;
|
||||
address_space_write(&address_space_memory, s->dma_inlink_address,
|
||||
MEMTXATTRS_UNSPECIFIED, &item, sizeof(uint32_t));
|
||||
s->dma_inlink_address = item.next;
|
||||
esp32_wifi_raise(s, 0x1000024);
|
||||
}
|
||||
|
||||
static const MemoryRegionOps esp32_wifi_ops = {
|
||||
.read = esp32_wifi_read,
|
||||
.write = esp32_wifi_write,
|
||||
.endianness = DEVICE_LITTLE_ENDIAN,
|
||||
};
|
||||
|
||||
static void esp32_wifi_reset(DeviceState *dev)
|
||||
{
|
||||
Esp32WifiState *s = ESP32_WIFI(dev);
|
||||
|
||||
s->dma_inlink_address = 0;
|
||||
s->raw_interrupt = 0;
|
||||
qemu_set_irq(s->irq, 0);
|
||||
memset(s->mem, 0, sizeof(s->mem));
|
||||
Esp32_WLAN_reset_ap(s);
|
||||
}
|
||||
|
||||
static void esp32_wifi_realize(DeviceState *dev, Error **errp)
|
||||
{
|
||||
Esp32WifiState *s = ESP32_WIFI(dev);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
|
||||
|
||||
memory_region_init_io(&s->iomem, OBJECT(dev), &esp32_wifi_ops, s,
|
||||
TYPE_ESP32_WIFI, 0x1000);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
sysbus_init_irq(sbd, &s->irq);
|
||||
Esp32_WLAN_setup_ap(dev, s);
|
||||
}
|
||||
|
||||
static Property esp32_wifi_properties[] = {
|
||||
DEFINE_NIC_PROPERTIES(Esp32WifiState, conf),
|
||||
DEFINE_PROP_END_OF_LIST(),
|
||||
};
|
||||
|
||||
static void esp32_wifi_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
|
||||
dc->realize = esp32_wifi_realize;
|
||||
device_class_set_legacy_reset(dc, esp32_wifi_reset);
|
||||
set_bit(DEVICE_CATEGORY_NETWORK, dc->categories);
|
||||
dc->desc = "ESP32 WiFi";
|
||||
device_class_set_props(dc, esp32_wifi_properties);
|
||||
}
|
||||
|
||||
static const TypeInfo esp32_wifi_info = {
|
||||
.name = TYPE_ESP32_WIFI,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(Esp32WifiState),
|
||||
.class_init = esp32_wifi_class_init,
|
||||
};
|
||||
|
||||
static void esp32_wifi_register_types(void)
|
||||
{
|
||||
type_register_static(&esp32_wifi_info);
|
||||
}
|
||||
|
||||
type_init(esp32_wifi_register_types)
|
||||
@@ -63,7 +63,8 @@
|
||||
#define ANSI_FG_HCOLOR(f) printf("\033[1;%dm", (f) + 30)
|
||||
|
||||
access_point_info access_points[] = {
|
||||
{"qemu", 1, -25, {0x10, 0x01, 0x00, 0xc4, 0x0a, 0x51}},
|
||||
{"qemu", NULL, 1, -25, {0x10, 0x01, 0x00, 0xc4, 0x0a, 0x51}},
|
||||
{"qemu2", "qemuqemu", 1, -30, {0x10, 0x01, 0x00, 0xc4, 0x0a, 0x52}},
|
||||
};
|
||||
|
||||
int nb_aps=sizeof(access_points)/sizeof(access_point_info);
|
||||
@@ -75,17 +76,14 @@ static void Esp32_WLAN_beacon_timer(void *opaque)
|
||||
// only send a beacon if we are an access point
|
||||
if((ENABLE_BEACON)&&(s->mode == Esp32_Mode_Station)){
|
||||
if(s->ap_state!=Esp32_WLAN__STATE_STA_ASSOCIATED) {
|
||||
for(int i=0;i<nb_aps;i++){
|
||||
int ap = (i + s->beacon_ap)%nb_aps;
|
||||
for(int ap=0;ap<nb_aps;ap++){
|
||||
if (access_points[ap].channel==esp32_wifi_channel) {
|
||||
memcpy(s->ap_macaddr,access_points[ap].mac_address,6);
|
||||
frame = Esp32_WLAN_create_beacon_frame(&access_points[ap]);
|
||||
Esp32_WLAN_init_ap_frame(s, frame);
|
||||
Esp32_WLAN_insert_frame(s, frame);
|
||||
break;
|
||||
}
|
||||
}
|
||||
s->beacon_ap=(s->beacon_ap+1)%nb_aps;
|
||||
}
|
||||
}
|
||||
timer_mod(s->beacon_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + BEACON_TIME);
|
||||
|
||||
@@ -117,6 +117,7 @@ typedef struct mac80211_frame {
|
||||
|
||||
typedef struct access_point_info {
|
||||
const char *ssid;
|
||||
const char *password;
|
||||
int channel;
|
||||
int sigstrength;
|
||||
macaddr_t mac_address;
|
||||
|
||||
@@ -100,17 +100,31 @@ static void add_ssid(mac80211_frame *frame, const char *ssid) {
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_SSID,strlen(ssid),(uint8_t *)ssid);
|
||||
}
|
||||
|
||||
static void add_rsn(mac80211_frame *frame, access_point_info *ap) {
|
||||
if (!ap->password) {
|
||||
return;
|
||||
}
|
||||
add_tag(frame, 0x30, 20, (uint8_t[]){
|
||||
0x01, 0x00,
|
||||
0x00, 0x0f, 0xac, 0x04,
|
||||
0x01, 0x00, 0x00, 0x0f, 0xac, 0x04,
|
||||
0x01, 0x00, 0x00, 0x0f, 0xac, 0x02,
|
||||
0x00, 0x00,
|
||||
});
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_beacon_frame(access_point_info *ap) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_BEACON);
|
||||
frame->signal_strength=ap->sigstrength;
|
||||
memcpy(frame->destination_address,BROADCAST,6);
|
||||
frame->beacon_info.timestamp=qemu_clock_get_ns(QEMU_CLOCK_REALTIME)/1000;
|
||||
frame->beacon_info.interval=1000;
|
||||
frame->beacon_info.capability=1;
|
||||
frame->beacon_info.capability=ap->password ? 0x11 : 1;
|
||||
frame->pos=12;
|
||||
add_ssid(frame,ap->ssid);
|
||||
add_rates(frame);
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_CHANNEL,1,(uint8_t[]){ap->channel});
|
||||
add_rsn(frame, ap);
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_TIM,4,(uint8_t[]){4,1,3,0,0});
|
||||
return frame;
|
||||
}
|
||||
@@ -200,11 +214,12 @@ mac80211_frame *Esp32_WLAN_create_probe_response(access_point_info *ap) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_PROBE_RESP);
|
||||
frame->beacon_info.timestamp=qemu_clock_get_ns(QEMU_CLOCK_REALTIME)/1000;
|
||||
frame->beacon_info.interval=1000;
|
||||
frame->beacon_info.capability=1;
|
||||
frame->beacon_info.capability=ap->password ? 0x11 : 1;
|
||||
frame->pos=12;
|
||||
add_ssid(frame,ap->ssid);
|
||||
add_rates(frame);
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_CHANNEL,1,(uint8_t[]){ap->channel});
|
||||
add_rsn(frame, ap);
|
||||
return frame;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ extern int nb_aps;
|
||||
static uint64_t esp32C3_wifi_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
|
||||
Esp32WifiState *s = ESP32_WIFI(opaque);
|
||||
Esp32WifiState *s = ESP32C3_WIFI(opaque);
|
||||
uint32_t r = s->mem[addr/4];
|
||||
|
||||
switch(addr) {
|
||||
@@ -56,7 +56,7 @@ void Esp32_WLAN_frame_delivered(Esp32WifiState *s){
|
||||
|
||||
static void esp32C3_wifi_write(void *opaque, hwaddr addr, uint64_t value,
|
||||
unsigned int size) {
|
||||
Esp32WifiState *s = ESP32_WIFI(opaque);
|
||||
Esp32WifiState *s = ESP32C3_WIFI(opaque);
|
||||
if(DEBUG) printf("esp32C3_wifi_write 0x%04lx= 0x%08lx\n",(unsigned long) addr, (unsigned long) value);
|
||||
|
||||
switch (addr) {
|
||||
@@ -192,7 +192,7 @@ static const MemoryRegionOps esp32C3_wifi_ops = {
|
||||
|
||||
static void esp32c3_wifi_reset(DeviceState *dev)
|
||||
{
|
||||
Esp32WifiState *s = ESP32_WIFI(dev);
|
||||
Esp32WifiState *s = ESP32C3_WIFI(dev);
|
||||
|
||||
s->dma_inlink_address=0;
|
||||
memset(s->mem,0,sizeof(s->mem));
|
||||
@@ -201,12 +201,12 @@ static void esp32c3_wifi_reset(DeviceState *dev)
|
||||
|
||||
static void esp32C3_wifi_realize(DeviceState *dev, Error **errp)
|
||||
{
|
||||
Esp32WifiState *s = ESP32_WIFI(dev);
|
||||
Esp32WifiState *s = ESP32C3_WIFI(dev);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
|
||||
s->dma_inlink_address=0;
|
||||
|
||||
memory_region_init_io(&s->iomem, OBJECT(dev), &esp32C3_wifi_ops, s,
|
||||
TYPE_ESP32_WIFI, 0x1000);
|
||||
TYPE_ESP32C3_WIFI, 0x1000);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
sysbus_init_irq(sbd, &s->irq);
|
||||
memset(s->mem,0,sizeof(s->mem));
|
||||
@@ -231,7 +231,7 @@ static void esp32C3_wifi_class_init(ObjectClass *klass, void *data)
|
||||
|
||||
|
||||
static const TypeInfo esp32C3_wifi_info = {
|
||||
.name = TYPE_ESP32_WIFI,
|
||||
.name = TYPE_ESP32C3_WIFI,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(Esp32WifiState),
|
||||
.class_init = esp32C3_wifi_class_init,
|
||||
|
||||
@@ -145,6 +145,12 @@ system_ss.add(when: 'CONFIG_XTENSA_ESP32', if_true: files(
|
||||
'esp32_aes.c',
|
||||
'esp32_ledc.c',
|
||||
'esp32_flash_enc.c',
|
||||
'esp32_wifi.c',
|
||||
'esp32_wifi_ap.c',
|
||||
'esp32_wlan_packet.c',
|
||||
'esp32_ana.c',
|
||||
'esp32_phya.c',
|
||||
'esp32_fe.c',
|
||||
'ssi_psram.c'
|
||||
))
|
||||
|
||||
|
||||
+2
-2
@@ -1577,7 +1577,7 @@ static sd_rsp_type_t emmc_cmd_SEND_EXT_CSD(SDState *sd, SDRequest req)
|
||||
/* CMD9 */
|
||||
static sd_rsp_type_t spi_cmd_SEND_CSD(SDState *sd, SDRequest req)
|
||||
{
|
||||
if (sd->state != sd_standby_state) {
|
||||
if (sd->state != sd_transfer_state) {
|
||||
return sd_invalid_state_for_cmd(sd, req);
|
||||
}
|
||||
return sd_cmd_to_sendingdata(sd, req, sd_req_get_address(sd, req),
|
||||
@@ -1596,7 +1596,7 @@ static sd_rsp_type_t sd_cmd_SEND_CSD(SDState *sd, SDRequest req)
|
||||
/* CMD10 */
|
||||
static sd_rsp_type_t spi_cmd_SEND_CID(SDState *sd, SDRequest req)
|
||||
{
|
||||
if (sd->state != sd_standby_state) {
|
||||
if (sd->state != sd_transfer_state) {
|
||||
return sd_invalid_state_for_cmd(sd, req);
|
||||
}
|
||||
return sd_cmd_to_sendingdata(sd, req, sd_req_get_address(sd, req),
|
||||
|
||||
@@ -395,6 +395,25 @@ static void ssi_sd_realize(SSIPeripheral *d, Error **errp)
|
||||
qbus_init(&s->sdbus, sizeof(s->sdbus), TYPE_SD_BUS, DEVICE(d), "sd-bus");
|
||||
}
|
||||
|
||||
/* CS frames a command in SPI mode, so releasing it ends whatever was in
|
||||
* flight. Without this a guest that resets mid-transfer (an ESP32 watchdog
|
||||
* reboot, say) leaves the card parked in a data phase, and the next boot's
|
||||
* CMD0 is eaten as payload. */
|
||||
static int ssi_sd_set_cs(SSIPeripheral *dev, bool level)
|
||||
{
|
||||
ssi_sd_state *s = SSI_SD(dev);
|
||||
|
||||
if (level) {
|
||||
s->mode = SSI_SD_CMD;
|
||||
s->arglen = 0;
|
||||
s->response_pos = 0;
|
||||
s->read_bytes = 0;
|
||||
s->write_bytes = 0;
|
||||
s->stopping = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void ssi_sd_reset(DeviceState *dev)
|
||||
{
|
||||
ssi_sd_state *s = SSI_SD(dev);
|
||||
@@ -419,6 +438,7 @@ static void ssi_sd_class_init(ObjectClass *klass, void *data)
|
||||
|
||||
k->realize = ssi_sd_realize;
|
||||
k->transfer = ssi_sd_transfer;
|
||||
k->set_cs = ssi_sd_set_cs;
|
||||
k->cs_polarity = SSI_CS_LOW;
|
||||
dc->vmsd = &vmstate_ssi_sd;
|
||||
device_class_set_legacy_reset(dc, ssi_sd_reset);
|
||||
|
||||
+4
-3
@@ -157,11 +157,11 @@ static void esp32_spi_txrx_buffer(Esp32SpiState *s, void *buf, int tx_bytes, int
|
||||
uint8_t *c_buf = (uint8_t*) buf;
|
||||
for (int i = 0; i < bytes; ++i) {
|
||||
uint8_t byte = 0;
|
||||
if (byte < tx_bytes) {
|
||||
if (i < tx_bytes) {
|
||||
memcpy(&byte, c_buf + i, 1);
|
||||
}
|
||||
uint32_t res = ssi_transfer(s->spi, byte);
|
||||
if (byte < rx_bytes) {
|
||||
if (i < rx_bytes) {
|
||||
memcpy(c_buf + i, &res, 1);
|
||||
}
|
||||
}
|
||||
@@ -275,7 +275,8 @@ static void esp32_spi_do_command(Esp32SpiState* s, uint32_t cmd_reg)
|
||||
|
||||
case R_SPI_CMD_USR_MASK:
|
||||
maybe_encrypt_data(s);
|
||||
if (FIELD_EX32(s->user_reg, SPI_USER, COMMAND) || FIELD_EX32(s->user2_reg, SPI_USER2, COMMAND_BITLEN)) {
|
||||
if (FIELD_EX32(s->user_reg, SPI_USER, COMMAND) &&
|
||||
FIELD_EX32(s->user2_reg, SPI_USER2, COMMAND_BITLEN)) {
|
||||
t.cmd = FIELD_EX32(s->user2_reg, SPI_USER2, COMMAND_VALUE);
|
||||
t.cmd_bytes = bitlen_to_bytes(FIELD_EX32(s->user2_reg, SPI_USER2, COMMAND_BITLEN));
|
||||
} else {
|
||||
|
||||
@@ -26,6 +26,7 @@ config XTENSA_ESP32
|
||||
depends on XTENSA
|
||||
select SSI
|
||||
select SSI_M25P80
|
||||
select SSI_SD
|
||||
select UNIMP
|
||||
select OPENCORES_ETH
|
||||
select DWC_SDMMC
|
||||
|
||||
+135
-5
@@ -15,6 +15,7 @@
|
||||
#include "qapi/error.h"
|
||||
#include "hw/hw.h"
|
||||
#include "hw/boards.h"
|
||||
#include "monitor/qdev.h"
|
||||
#include "hw/loader.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/i2c/esp32_i2c.h"
|
||||
@@ -25,7 +26,12 @@
|
||||
#include "hw/qdev-properties.h"
|
||||
#include "hw/xtensa/esp32.h"
|
||||
#include "hw/misc/ssi_psram.h"
|
||||
#include "hw/misc/esp32_wifi.h"
|
||||
#include "hw/misc/esp32_ana.h"
|
||||
#include "hw/misc/esp32_phya.h"
|
||||
#include "hw/misc/esp32_fe.h"
|
||||
#include "hw/sd/dwc_sdmmc.h"
|
||||
#include "hw/sd/sd.h"
|
||||
#include "core-esp32/core-isa.h"
|
||||
#include "qemu/datadir.h"
|
||||
#include "sysemu/sysemu.h"
|
||||
@@ -487,6 +493,8 @@ static void esp32_soc_realize(DeviceState *dev, Error **errp)
|
||||
sysbus_connect_irq(SYS_BUS_DEVICE(&s->twai), 0,
|
||||
qdev_get_gpio_in(intmatrix_dev, ETS_CAN_INTR_SOURCE));
|
||||
|
||||
/* WiFi PHY setup touches WDEV control words around the RNG register. */
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.wdev", DR_REG_WDEV_BASE, 0x1000);
|
||||
qdev_realize(DEVICE(&s->rng), &s->periph_bus, &error_fatal);
|
||||
esp32_soc_add_periph_device(sys_mem, &s->rng, ESP32_RNG_BASE);
|
||||
|
||||
@@ -516,7 +524,11 @@ static void esp32_soc_realize(DeviceState *dev, Error **errp)
|
||||
esp32_soc_add_periph_device(sys_mem, &s->rgb, DR_REG_FRAMEBUF_BASE);
|
||||
memory_region_add_subregion_overlap(sys_mem, esp32_memmap[ESP32_MEMREGION_FRAMEBUF].base, &s->rgb.vram, 0);
|
||||
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.analog", DR_REG_ANA_BASE, 0x1000);
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.fe2", DR_REG_FE2_BASE, 0x1000);
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.bt", DR_REG_BT_BASE, 0x1000);
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.nrx", 0x3ff5c000, 0x1000);
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.bb", DR_REG_BB_BASE, 0x1000);
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.phy", DR_REG_PHY_BASE, 0x1000);
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.rtcio", DR_REG_RTCIO_BASE, 0x400);
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.rtcio", DR_REG_SENS_BASE, 0x400);
|
||||
esp32_soc_add_unimp_device(sys_mem, "esp32.iomux", DR_REG_IO_MUX_BASE, 0x2000);
|
||||
@@ -690,11 +702,16 @@ static uint64_t translate_phys_addr(void *opaque, uint64_t addr)
|
||||
}
|
||||
|
||||
|
||||
#include "hw/display/esp32_lcd_st7796.h"
|
||||
#include "hw/ssi/ssi.h"
|
||||
|
||||
struct Esp32MachineState {
|
||||
MachineState parent;
|
||||
|
||||
Esp32SocState esp32;
|
||||
DeviceState *flash_dev;
|
||||
bool esp32_realized;
|
||||
bool e32r40t;
|
||||
};
|
||||
#define TYPE_ESP32_MACHINE MACHINE_TYPE_NAME("esp32")
|
||||
|
||||
@@ -754,6 +771,38 @@ static void esp32_machine_init_i2c(Esp32SocState *s)
|
||||
object_property_set_int(OBJECT(tmp105), "temperature", 25 * 1000, &error_fatal);
|
||||
}
|
||||
|
||||
static void esp32_map_radio_device(const char *type, hwaddr address)
|
||||
{
|
||||
DeviceState *device = qdev_new(type);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(device);
|
||||
|
||||
sysbus_realize_and_unref(sbd, &error_fatal);
|
||||
esp32_soc_add_periph_device(get_system_memory(), device, address);
|
||||
}
|
||||
|
||||
static bool esp32_machine_init_wifi(Esp32SocState *ss)
|
||||
{
|
||||
DeviceState *wifi = qemu_create_nic_device(TYPE_ESP32_WIFI, false, NULL);
|
||||
SysBusDevice *sbd;
|
||||
|
||||
if (!wifi) {
|
||||
return false;
|
||||
}
|
||||
|
||||
esp32_map_radio_device(TYPE_ESP32_ANA, DR_REG_ANA_BASE);
|
||||
esp32_map_radio_device(TYPE_ESP32_PHYA, DR_REG_PHYA_BASE);
|
||||
esp32_map_radio_device(TYPE_ESP32_FE, DR_REG_FE_BASE);
|
||||
|
||||
ss->eth = wifi;
|
||||
sbd = SYS_BUS_DEVICE(wifi);
|
||||
sysbus_realize_and_unref(sbd, &error_fatal);
|
||||
esp32_soc_add_periph_device(get_system_memory(), wifi, DR_REG_WIFI_BASE);
|
||||
sysbus_connect_irq(sbd, 0,
|
||||
qdev_get_gpio_in(DEVICE(&ss->intmatrix),
|
||||
ETS_WIFI_MAC_INTR_SOURCE));
|
||||
return true;
|
||||
}
|
||||
|
||||
static void esp32_machine_init_openeth(Esp32SocState *ss)
|
||||
{
|
||||
SysBusDevice *sbd;
|
||||
@@ -803,7 +852,9 @@ static void esp32_machine_init(MachineState *machine)
|
||||
}
|
||||
|
||||
Esp32MachineState *ms = ESP32_MACHINE(machine);
|
||||
object_initialize_child(OBJECT(ms), "soc", &ms->esp32, TYPE_ESP32_SOC);
|
||||
if (!ms->esp32_realized) {
|
||||
object_initialize_child(OBJECT(ms), "soc", &ms->esp32, TYPE_ESP32_SOC);
|
||||
}
|
||||
Esp32SocState *ss = ESP32_SOC(&ms->esp32);
|
||||
|
||||
if (blk) {
|
||||
@@ -816,7 +867,10 @@ static void esp32_machine_init(MachineState *machine)
|
||||
qdev_prop_set_bit(DEVICE(&ss->dport), "has_psram", true);
|
||||
}
|
||||
|
||||
qdev_realize(DEVICE(ss), NULL, &error_fatal);
|
||||
if (!ms->esp32_realized) {
|
||||
qdev_realize(DEVICE(ss), NULL, &error_fatal);
|
||||
ms->esp32_realized = true;
|
||||
}
|
||||
|
||||
if (blk) {
|
||||
esp32_machine_init_spi_flash(ss, blk);
|
||||
@@ -828,9 +882,13 @@ static void esp32_machine_init(MachineState *machine)
|
||||
|
||||
esp32_machine_init_i2c(ss);
|
||||
|
||||
esp32_machine_init_openeth(ss);
|
||||
if (!esp32_machine_init_wifi(ss)) {
|
||||
esp32_machine_init_openeth(ss);
|
||||
}
|
||||
|
||||
esp32_machine_init_sd(ss);
|
||||
if (!ms->e32r40t) {
|
||||
esp32_machine_init_sd(ss);
|
||||
}
|
||||
|
||||
/* Need MMU initialized prior to ELF loading,
|
||||
* so that ELF gets loaded into virtual addresses
|
||||
@@ -940,9 +998,81 @@ static const TypeInfo esp32_info = {
|
||||
.class_init = esp32_machine_class_init,
|
||||
};
|
||||
|
||||
static void e32r40t_machine_init(MachineState *machine)
|
||||
{
|
||||
Esp32MachineState *ms = ESP32_MACHINE(machine);
|
||||
ms->e32r40t = true;
|
||||
esp32_machine_init(machine);
|
||||
Esp32SocState *ss = ESP32_SOC(&ms->esp32);
|
||||
|
||||
/* ST7796 panel (CS=GPIO15, DC=GPIO2) and XPT2046 touch (CS=GPIO33)
|
||||
* share SPI2 and one emulated board state. SSIBus is only defined in
|
||||
* hw/ssi/ssi.c, hence the cast of the SPI2 child bus. */
|
||||
DeviceState *board = qdev_new(TYPE_ESP32_LCD_BOARD);
|
||||
/* Named so QMP screendump/input-send-event can target the LCD console. */
|
||||
qdev_set_id(board, g_strdup("lcd"), &error_fatal);
|
||||
sysbus_realize_and_unref(SYS_BUS_DEVICE(board), &error_fatal);
|
||||
|
||||
BusState *spi2 = qdev_get_child_bus(DEVICE(&ss->spi[2]), "spi");
|
||||
DeviceState *panel = qdev_new(TYPE_ESP32_LCD_ST7796);
|
||||
object_property_set_link(OBJECT(panel), "board", OBJECT(board),
|
||||
&error_fatal);
|
||||
ssi_realize_and_unref(panel, (SSIBus *)spi2, &error_fatal);
|
||||
|
||||
DeviceState *touch = qdev_new(TYPE_ESP32_LCD_XPT2046);
|
||||
object_property_set_link(OBJECT(touch), "board", OBJECT(board),
|
||||
&error_fatal);
|
||||
qdev_prop_set_uint8(touch, "cs", 1);
|
||||
ssi_realize_and_unref(touch, (SSIBus *)spi2, &error_fatal);
|
||||
|
||||
object_property_add_alias(OBJECT(machine), "frame-generation",
|
||||
OBJECT(board), "frame-generation");
|
||||
qdev_connect_gpio_out_named(DEVICE(&ss->gpio), ESP32_GPIO_OUTPUT, 15,
|
||||
qdev_get_gpio_in_named(panel, SSI_GPIO_CS, 0));
|
||||
qdev_connect_gpio_out_named(DEVICE(&ss->gpio), ESP32_GPIO_OUTPUT, 33,
|
||||
qdev_get_gpio_in_named(touch, SSI_GPIO_CS, 0));
|
||||
qdev_connect_gpio_out_named(DEVICE(&ss->gpio), ESP32_GPIO_OUTPUT, 2,
|
||||
qdev_get_gpio_in_named(panel, ESP32_LCD_ST7796_DC, 0));
|
||||
qemu_set_irq(qdev_get_gpio_in_named(panel, SSI_GPIO_CS, 0), 1);
|
||||
qemu_set_irq(qdev_get_gpio_in_named(touch, SSI_GPIO_CS, 0), 1);
|
||||
qdev_connect_gpio_out_named(board, ESP32_LCD_XPT2046_IRQ, 0,
|
||||
qdev_get_gpio_in_named(DEVICE(&ss->gpio), ESP32_GPIO_INPUT, 36));
|
||||
esp32_gpio_set_input_level(&ss->gpio, 36, true);
|
||||
|
||||
DeviceState *sd_adapter = qdev_new("ssi-sd");
|
||||
qdev_realize_and_unref(sd_adapter,
|
||||
qdev_get_child_bus(DEVICE(&ss->spi[3]), "spi"), &error_fatal);
|
||||
qdev_connect_gpio_out_named(DEVICE(&ss->gpio), ESP32_GPIO_OUTPUT, 5,
|
||||
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);
|
||||
}
|
||||
|
||||
static void e32r40t_machine_class_init(ObjectClass *oc, void *data)
|
||||
{
|
||||
MachineClass *mc = MACHINE_CLASS(oc);
|
||||
mc->desc = "E32R40T 4.0\" ESP32-32E display module (ST7796 + XPT2046)";
|
||||
mc->init = e32r40t_machine_init;
|
||||
}
|
||||
|
||||
static const TypeInfo e32r40t_info = {
|
||||
.name = MACHINE_TYPE_NAME("e32r40t"),
|
||||
.parent = TYPE_ESP32_MACHINE,
|
||||
.class_init = e32r40t_machine_class_init,
|
||||
};
|
||||
|
||||
static void esp32_machine_type_init(void)
|
||||
{
|
||||
type_register_static(&esp32_info);
|
||||
type_register_static(&e32r40t_info);
|
||||
}
|
||||
|
||||
type_init(esp32_machine_type_init);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "qom/object.h"
|
||||
|
||||
typedef struct Esp32LcdBoard Esp32LcdBoard;
|
||||
|
||||
#define TYPE_ESP32_LCD_BOARD "e32r40t-board"
|
||||
#define ESP32_LCD_BOARD(obj) \
|
||||
((Esp32LcdBoard *)object_dynamic_cast(OBJECT(obj), TYPE_ESP32_LCD_BOARD))
|
||||
#define ESP32_LCD_BOARD_GET_CLASS(obj) ((void)(obj), NULL)
|
||||
#define ESP32_LCD_BOARD_CLASS(klass) ((void)(klass), NULL)
|
||||
|
||||
#define TYPE_ESP32_LCD_ST7796 "esp32-lcd-st7796"
|
||||
#define TYPE_ESP32_LCD_XPT2046 "esp32-lcd-xpt2046"
|
||||
#define ESP32_LCD_ST7796_DC "dc"
|
||||
#define ESP32_LCD_XPT2046_IRQ "irq"
|
||||
@@ -14,7 +14,7 @@ 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_COUNT 40 /* real ESP32 has GPIO0-39; upper pins use OUT1/IN1 */
|
||||
#define ESP32_GPIO_INPUT "gpio-in"
|
||||
#define ESP32_GPIO_OUTPUT "gpio-out"
|
||||
|
||||
@@ -25,10 +25,10 @@ typedef struct Esp32GpioState {
|
||||
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;
|
||||
uint32_t interrupt_status;
|
||||
uint64_t input_level;
|
||||
uint64_t output_level;
|
||||
uint64_t output_enable;
|
||||
uint64_t interrupt_status;
|
||||
uint32_t pin_config[ESP32_GPIO_COUNT];
|
||||
hwaddr pin_config_base;
|
||||
} Esp32GpioState;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "hw/hw.h"
|
||||
#include "hw/sysbus.h"
|
||||
|
||||
#define TYPE_ESP32_ANA "misc.esp32.ana"
|
||||
#define ESP32_ANA(obj) OBJECT_CHECK(Esp32AnaState, (obj), TYPE_ESP32_ANA)
|
||||
|
||||
typedef struct Esp32AnaState {
|
||||
SysBusDevice parent_obj;
|
||||
MemoryRegion iomem;
|
||||
uint32_t mem[1024];
|
||||
} Esp32AnaState;
|
||||
@@ -59,6 +59,9 @@
|
||||
#define DR_REG_UART2_BASE 0x3ff6E000
|
||||
#define DR_REG_PWM2_BASE 0x3ff6F000
|
||||
#define DR_REG_PWM3_BASE 0x3ff70000
|
||||
#define DR_REG_PHY_BASE 0x3ff71000
|
||||
#define DR_REG_WIFI_BASE 0x3ff73000
|
||||
#define DR_REG_PHYA_BASE 0x3ff74000
|
||||
#define DR_REG_WDEV_BASE 0x3ff75000
|
||||
|
||||
#define APB_REG_BASE 0x60000000
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "sysemu/sysemu.h"
|
||||
#include "net/net.h"
|
||||
|
||||
#define TYPE_ESP32_WIFI "esp32c3_wifi"
|
||||
#define TYPE_ESP32_WIFI "esp32_wifi"
|
||||
#define ESP32_WIFI(obj) OBJECT_CHECK(Esp32WifiState, (obj), TYPE_ESP32_WIFI)
|
||||
|
||||
typedef struct dma_list_item {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
#include "net/net.h"
|
||||
#include "esp32_wifi.h"
|
||||
|
||||
#define TYPE_ESP32C3_WIFI TYPE_ESP32_WIFI
|
||||
#define TYPE_ESP32C3_WIFI "esp32c3_wifi"
|
||||
#define ESP32C3_WIFI(obj) OBJECT_CHECK(Esp32WifiState, (obj), TYPE_ESP32C3_WIFI)
|
||||
|
||||
|
||||
REG32(C3_WIFI_DMA_IN_STATUS, 0x84);
|
||||
|
||||
+3
-1
@@ -484,9 +484,11 @@ ucontext_probe = '''
|
||||
# On Windows the only valid backend is the Windows specific one.
|
||||
# For POSIX prefer ucontext, but it's not always possible. The fallback
|
||||
# is sigcontext.
|
||||
supported_backends = ['fiber']
|
||||
supported_backends = []
|
||||
if host_os == 'windows'
|
||||
supported_backends += ['windows']
|
||||
elif host_arch == 'wasm32'
|
||||
supported_backends += ['fiber']
|
||||
else
|
||||
if host_os != 'darwin' and cc.links(ucontext_probe)
|
||||
supported_backends += ['ucontext']
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# esp-emu
|
||||
|
||||
Scriptable controller for the native ESP32 board emulators (`xteink`, `e32r40t`).
|
||||
|
||||
## Install
|
||||
|
||||
From the qemu-esp-boards checkout:
|
||||
|
||||
```sh
|
||||
uv tool install -e tools/esp-emu-cli
|
||||
```
|
||||
|
||||
The editable install exposes `esp-emu` globally and reflects source changes immediately. On first `boot`, it locates the containing qemu-esp-boards checkout and builds `.#qemu-native-xteink` with Nix if the native emulator is missing. Set `XTEINK_QEMU_REPO` to select a different checkout or `XTEINK_QEMU` to use an explicit binary.
|
||||
|
||||
The build requires Nix. SD helpers require `mtools` and `dosfstools` (`mcopy` and `mkfs.fat`).
|
||||
|
||||
## Basic Use
|
||||
|
||||
```sh
|
||||
esp-emu --board xteink boot firmware.bin
|
||||
esp-emu --board xteink wait-log 'Entering activity: Home' --timeout 60
|
||||
esp-emu --board xteink press bottom-4 bottom-4 bottom-2
|
||||
esp-emu --board xteink capture screen.png
|
||||
esp-emu --board xteink stop
|
||||
```
|
||||
|
||||
State defaults to `/tmp/esp-emu-xteink-$UID`. Use a separate state for each test:
|
||||
|
||||
```sh
|
||||
esp-emu --board xteink --state /tmp/reader-test boot firmware.bin
|
||||
```
|
||||
|
||||
`boot` replaces an emulator already running in that state.
|
||||
|
||||
## SD Cards
|
||||
|
||||
```sh
|
||||
# Create or reuse $STATE/sdcard.img
|
||||
esp-emu --board xteink boot firmware.bin
|
||||
|
||||
# Use an existing image directly
|
||||
esp-emu --board xteink boot firmware.bin --sdcard card.img
|
||||
|
||||
# Merge a directory into the persistent state image before every boot
|
||||
esp-emu --board xteink boot firmware.bin --sdcard test-library/
|
||||
|
||||
# Recreate the state image before merging the directory
|
||||
esp-emu --board xteink boot firmware.bin --sdcard test-library/ --fresh-sd
|
||||
```
|
||||
|
||||
Directory imports overwrite matching files while preserving other image contents. The source directory is never modified.
|
||||
|
||||
## Scripts and Flows
|
||||
|
||||
A script is a newline-delimited list of the same commands, with blank lines and `#` comments allowed:
|
||||
|
||||
```text
|
||||
#!/usr/bin/env -S esp-emu --board xteink
|
||||
# Any board: #!/usr/bin/env -S esp-emu --board e32r40t
|
||||
|
||||
boot firmware.bin
|
||||
wait-log "Entering activity: Home" --timeout 60
|
||||
press bottom-4 bottom-4 bottom-2
|
||||
wait-log "Entering activity: Settings"
|
||||
capture settings.png
|
||||
```
|
||||
|
||||
Make it executable and run it directly:
|
||||
|
||||
```sh
|
||||
chmod +x flow.xteink
|
||||
./flow.xteink
|
||||
```
|
||||
|
||||
A file argument is treated as an implicit `run`. Explicit file, custom-state, and stdin forms also work:
|
||||
|
||||
```sh
|
||||
esp-emu --board xteink flow.xteink
|
||||
esp-emu --board xteink --state /tmp/reader-test run flow.xteink
|
||||
printf '%s\n' 'boot firmware.bin' 'wait-log "ready"' | esp-emu --board xteink run
|
||||
```
|
||||
|
||||
The outer `--state` applies to every command in the script. When the script exits, its emulator is stopped even if a command failed or the script was interrupted.
|
||||
|
||||
## Synchronization
|
||||
|
||||
- `press` waits for debounced guest press and release sampling; display waits are unnecessary between presses.
|
||||
- `wait-log REGEX` waits for new serial output and acts as an assertion.
|
||||
- `wait-frame [COUNT]` waits for actual e-ink refreshes when rendering itself matters.
|
||||
- `wait-idle SECONDS` waits for serial silence.
|
||||
|
||||
Run `esp-emu --board xteink --help` or `esp-emu --board xteink COMMAND --help` for all options.
|
||||
@@ -3,14 +3,14 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "xteink-emu-cli"
|
||||
name = "esp-emu-cli"
|
||||
version = "0.1.0"
|
||||
description = "Scriptable Xteink firmware emulator controller"
|
||||
description = "Scriptable ESP32 board emulator controller"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[project.scripts]
|
||||
xteink-emu = "xteink_emu.cli:run_cli"
|
||||
esp-emu = "esp_emu.cli:run"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/xteink_emu"]
|
||||
packages = ["src/esp_emu"]
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Per-board differences. Everything not listed here is shared by emu.py."""
|
||||
|
||||
RELEASED_ADC = 4095
|
||||
|
||||
XTEINK_BUTTONS = {
|
||||
"left": ("/machine/adc", "adci[2]", 2242, RELEASED_ADC),
|
||||
"right": ("/machine/adc", "adci[2]", 5, RELEASED_ADC),
|
||||
"bottom-1": ("/machine/adc", "adci[1]", 3512, RELEASED_ADC),
|
||||
"bottom-2": ("/machine/adc", "adci[1]", 2694, RELEASED_ADC),
|
||||
"bottom-3": ("/machine/adc", "adci[1]", 1493, RELEASED_ADC),
|
||||
"bottom-4": ("/machine/adc", "adci[1]", 5, RELEASED_ADC),
|
||||
"power": ("/machine/gpio", "input-level[3]", False, True),
|
||||
}
|
||||
|
||||
BOARDS = {
|
||||
"xteink": {
|
||||
"name": "xteink",
|
||||
"qemu_env": "XTEINK_QEMU",
|
||||
"qemu_binary": "qemu-system-riscv32",
|
||||
"machine": "xteink",
|
||||
"serial": "stdio",
|
||||
"variants": ("x3", "x4"),
|
||||
"memory": None, # the machine sizes its own RAM
|
||||
"flash_size": 16 * 1024 * 1024,
|
||||
# ESP32-C3 keeps its second stage bootloader at offset 0; ESP32 at 0x1000.
|
||||
"bootloader_offset": 0x0,
|
||||
# Bootloader and partition table ship with the checkout for this board.
|
||||
"packaged_flash_parts": True,
|
||||
"buttons": XTEINK_BUTTONS,
|
||||
"console": None,
|
||||
"panel": None,
|
||||
# dram and iram alias the same internal SRAM, so their numbers overlap.
|
||||
"memory_regions": {
|
||||
"dram": (0x3FC80000, 0x60000),
|
||||
"iram": (0x4037C000, 0x60000 + 16 * 1024),
|
||||
"rtcram": (0x50000000, 0x2000),
|
||||
},
|
||||
},
|
||||
"e32r40t": {
|
||||
"name": "e32r40t",
|
||||
"qemu_env": "E32R40T_QEMU",
|
||||
"qemu_binary": "qemu-system-xtensa",
|
||||
"machine": "e32r40t",
|
||||
# Plain stdio drops the guest UART on this machine; the mux form works.
|
||||
"serial": "mon:stdio",
|
||||
"variants": (),
|
||||
"memory": "4M",
|
||||
"flash_size": 4 * 1024 * 1024,
|
||||
"bootloader_offset": 0x1000,
|
||||
"packaged_flash_parts": False,
|
||||
"buttons": {},
|
||||
"console": "lcd",
|
||||
"panel": (320, 480),
|
||||
"memory_regions": {
|
||||
"dram": (0x3FFAE000, 0x52000),
|
||||
"iram": (0x40080000, 0x20000),
|
||||
"rtcram": (0x50000000, 0x2000),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_BOARD = "xteink"
|
||||
MEMORY_REGIONS = tuple(BOARDS[DEFAULT_BOARD]["memory_regions"])
|
||||
|
||||
|
||||
def board(name):
|
||||
try:
|
||||
return BOARDS[name]
|
||||
except KeyError:
|
||||
raise RuntimeError(f"unknown board: {name}") from None
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Command line front end for the xteink emulator."""
|
||||
"""Command line front end for the ESP32 board emulators."""
|
||||
|
||||
import argparse
|
||||
import shlex
|
||||
@@ -15,7 +15,8 @@ class ArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
|
||||
def state_dir(args):
|
||||
return Path(args.state).expanduser().resolve()
|
||||
state = args.state or emu.default_state_dir(args.board)
|
||||
return Path(state).expanduser().resolve()
|
||||
|
||||
|
||||
def nonnegative(value):
|
||||
@@ -60,6 +61,7 @@ def launch(args, interactive):
|
||||
state,
|
||||
args.firmware,
|
||||
card,
|
||||
name=args.board,
|
||||
variant=args.variant,
|
||||
power_hold_ms=args.power_hold_ms,
|
||||
interactive=interactive,
|
||||
@@ -71,7 +73,10 @@ def launch(args, interactive):
|
||||
print(f"serial log: {state / 'serial.log'}")
|
||||
return
|
||||
|
||||
print("keys: Left/Right, 1-4 bottom buttons, P power; close the window to stop")
|
||||
if emu.board(args.board)["buttons"]:
|
||||
print("keys: Left/Right, 1-4 bottom buttons, P power; close the window to stop")
|
||||
else:
|
||||
print("click the panel to touch it; close the window to stop")
|
||||
try:
|
||||
returncode = process.wait()
|
||||
except KeyboardInterrupt:
|
||||
@@ -119,6 +124,25 @@ def release(args):
|
||||
emu.set_button(state, args.button, down=False)
|
||||
|
||||
|
||||
def tap(args):
|
||||
state = state_dir(args)
|
||||
emu.require_running(state)
|
||||
emu.tap(state, args.x, args.y, hold_ms=args.hold_ms)
|
||||
|
||||
|
||||
def touch(args):
|
||||
state = state_dir(args)
|
||||
emu.require_running(state)
|
||||
emu.touch_move(state, args.x, args.y)
|
||||
emu.touch_button(state, down=args.command == "touch-hold")
|
||||
|
||||
|
||||
def drag(args):
|
||||
state = state_dir(args)
|
||||
emu.require_running(state)
|
||||
emu.drag(state, args.x, args.y, args.to_x, args.to_y, steps=args.steps, step_ms=args.step_ms)
|
||||
|
||||
|
||||
def wait_log(args):
|
||||
line = emu.wait_for_log(state_dir(args), args.pattern, args.timeout)
|
||||
print(line, end="" if line.endswith("\n") else "\n")
|
||||
@@ -153,20 +177,25 @@ def memory(args):
|
||||
|
||||
|
||||
def web(args):
|
||||
serve(state_dir(args), args.host, args.port, args.log_bytes)
|
||||
serve(state_dir(args), args.host, args.port, args.log_bytes, args.board)
|
||||
|
||||
|
||||
def run_script(args):
|
||||
if args.file == "-":
|
||||
execute_lines(args, sys.stdin, "stdin")
|
||||
return
|
||||
path = Path(args.file).expanduser()
|
||||
with path.open() as lines:
|
||||
execute_lines(args, lines, str(path))
|
||||
try:
|
||||
if args.file == "-":
|
||||
execute_lines(args, sys.stdin, "stdin")
|
||||
return
|
||||
path = Path(args.file).expanduser()
|
||||
with path.open() as lines:
|
||||
execute_lines(args, lines, str(path))
|
||||
finally:
|
||||
state = state_dir(args)
|
||||
if emu.is_running(state):
|
||||
emu.shutdown(state)
|
||||
|
||||
|
||||
def execute_lines(args, lines, source):
|
||||
command_parser = parser()
|
||||
command_parser = parser(args.board)
|
||||
for number, raw_line in enumerate(lines, 1):
|
||||
try:
|
||||
arguments = shlex.split(raw_line, comments=True)
|
||||
@@ -181,9 +210,10 @@ def execute_lines(args, lines, source):
|
||||
raise RuntimeError(f"{source}:{number}: {error}") from error
|
||||
|
||||
|
||||
def parser():
|
||||
root = ArgumentParser(prog="xteink-emu")
|
||||
root.add_argument("--state", default=emu.default_state_dir(), help="emulator state directory")
|
||||
def parser(board=emu.DEFAULT_BOARD):
|
||||
root = ArgumentParser(prog="esp-emu")
|
||||
root.add_argument("--board", choices=emu.BOARDS, default=board)
|
||||
root.add_argument("--state", default=None, help="emulator state directory (default: per board)")
|
||||
commands = root.add_subparsers(dest="command", required=True, parser_class=ArgumentParser)
|
||||
|
||||
def add_boot_arguments(command):
|
||||
@@ -217,12 +247,33 @@ def parser():
|
||||
command.add_argument("button", choices=emu.BUTTONS)
|
||||
command.set_defaults(handler=release)
|
||||
|
||||
command = commands.add_parser("tap", help="tap the touch panel at a pixel position")
|
||||
command.add_argument("x", type=int)
|
||||
command.add_argument("y", type=int)
|
||||
command.add_argument("--hold-ms", type=nonnegative, default=250)
|
||||
command.set_defaults(handler=tap)
|
||||
|
||||
command = commands.add_parser("drag", help="press, slide across the panel and release")
|
||||
command.add_argument("x", type=int)
|
||||
command.add_argument("y", type=int)
|
||||
command.add_argument("to_x", type=int)
|
||||
command.add_argument("to_y", type=int)
|
||||
command.add_argument("--steps", type=positive_int, default=16)
|
||||
command.add_argument("--step-ms", type=nonnegative, default=30)
|
||||
command.set_defaults(handler=drag)
|
||||
|
||||
for name, help_text in (("touch-hold", "press and hold the panel"), ("touch-release", "release the panel")):
|
||||
command = commands.add_parser(name, help=help_text)
|
||||
command.add_argument("x", type=int)
|
||||
command.add_argument("y", type=int)
|
||||
command.set_defaults(handler=touch)
|
||||
|
||||
command = commands.add_parser("wait-log", help="wait for a regular expression in new serial output")
|
||||
command.add_argument("pattern")
|
||||
command.add_argument("--timeout", type=nonnegative, default=30)
|
||||
command.set_defaults(handler=wait_log)
|
||||
|
||||
command = commands.add_parser("wait-frame", help="wait for e-ink refreshes")
|
||||
command = commands.add_parser("wait-frame", help="wait for panel refreshes")
|
||||
command.add_argument("count", type=positive_int, nargs="?", default=1)
|
||||
command.add_argument("--timeout", type=nonnegative, default=30)
|
||||
command.set_defaults(handler=wait_frame)
|
||||
@@ -241,7 +292,7 @@ def parser():
|
||||
command.set_defaults(handler=run_script)
|
||||
|
||||
command = commands.add_parser("memory", help="RAM high-water marks, or dump a region with --dump")
|
||||
command.add_argument("--region", choices=emu.MEMORY_REGIONS, default="dram")
|
||||
command.add_argument("--region", choices=emu.MEMORY_REGIONS, default="dram") # same names on every board
|
||||
command.add_argument("--dump", help="write the raw region to this file")
|
||||
command.set_defaults(handler=memory)
|
||||
|
||||
@@ -250,23 +301,30 @@ def parser():
|
||||
command.add_argument("--port", type=int, default=8080)
|
||||
command.add_argument("--log-bytes", type=int, default=200_000, help="tail size of the serial log")
|
||||
command.set_defaults(handler=web)
|
||||
COMMANDS.update(commands.choices)
|
||||
return root
|
||||
|
||||
|
||||
COMMANDS = set()
|
||||
|
||||
|
||||
def command_line(arguments):
|
||||
if len(arguments) == 1 and Path(arguments[0]).is_file():
|
||||
return ["run", arguments[0]]
|
||||
"""Shebang support - `#!/usr/bin/env -S esp-emu --board x` passes flags plus a script path and no command."""
|
||||
if not COMMANDS:
|
||||
parser()
|
||||
if arguments and not COMMANDS.intersection(arguments) and Path(arguments[-1]).is_file():
|
||||
return [*arguments[:-1], "run", arguments[-1]]
|
||||
return arguments
|
||||
|
||||
|
||||
def main():
|
||||
args = parser().parse_args(command_line(sys.argv[1:]))
|
||||
def main(board=emu.DEFAULT_BOARD):
|
||||
args = parser(board).parse_args(command_line(sys.argv[1:]))
|
||||
args.handler(args)
|
||||
|
||||
|
||||
def run_cli():
|
||||
def run():
|
||||
try:
|
||||
main()
|
||||
except (RuntimeError, OSError) as error:
|
||||
print(f"xteink-emu: {error}", file=sys.stderr)
|
||||
print(f"esp-emu: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
+158
-59
@@ -11,56 +11,66 @@ import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
FLASH_SIZE = 16 * 1024 * 1024
|
||||
from .boards import BOARDS, DEFAULT_BOARD, MEMORY_REGIONS, RELEASED_ADC, board # noqa: F401
|
||||
|
||||
APP_OFFSET = 0x10000
|
||||
PARTITIONS_OFFSET = 0x8000
|
||||
RELEASED_ADC = 4095
|
||||
def project_root():
|
||||
configured = os.environ.get("XTEINK_QEMU_REPO")
|
||||
if configured:
|
||||
return Path(configured).expanduser().resolve()
|
||||
for parent in Path(__file__).resolve().parents:
|
||||
if (parent / "README.xteink.md").is_file() and (parent / "scripts/assets").is_dir():
|
||||
if (parent / "README.esp-boards.md").is_file() and (parent / "scripts/assets").is_dir():
|
||||
return parent
|
||||
raise RuntimeError("cannot locate the qemu-xteink checkout; set XTEINK_QEMU_REPO")
|
||||
raise RuntimeError("cannot locate the qemu-esp-boards checkout; set XTEINK_QEMU_REPO")
|
||||
|
||||
|
||||
PROJECT_ROOT = project_root()
|
||||
DEFAULT_PATHS = {
|
||||
"XTEINK_QEMU": PROJECT_ROOT / "dist/qemu-native/bin/qemu-system-riscv32",
|
||||
"E32R40T_QEMU": PROJECT_ROOT / "dist/qemu-native/bin/qemu-system-xtensa",
|
||||
"XTEINK_BIOS": PROJECT_ROOT / "dist/qemu-native/share/qemu",
|
||||
"XTEINK_BOOTLOADER": PROJECT_ROOT / "scripts/assets/esp32c3-bootloader.bin",
|
||||
"XTEINK_PARTITIONS": PROJECT_ROOT / "scripts/assets/xteink-partitions.bin",
|
||||
}
|
||||
BUTTONS = {
|
||||
"left": ("/machine/adc", "adci[2]", 2242, RELEASED_ADC),
|
||||
"right": ("/machine/adc", "adci[2]", 5, RELEASED_ADC),
|
||||
"bottom-1": ("/machine/adc", "adci[1]", 3512, RELEASED_ADC),
|
||||
"bottom-2": ("/machine/adc", "adci[1]", 2694, RELEASED_ADC),
|
||||
"bottom-3": ("/machine/adc", "adci[1]", 1493, RELEASED_ADC),
|
||||
"bottom-4": ("/machine/adc", "adci[1]", 5, RELEASED_ADC),
|
||||
"power": ("/machine/gpio", "input-level[3]", False, True),
|
||||
}
|
||||
VARIANTS = ("x3", "x4")
|
||||
# ESP32-C3 RAM windows. dram and iram alias the same internal SRAM, so their numbers overlap.
|
||||
MEMORY_REGIONS = {
|
||||
"dram": (0x3FC80000, 0x60000),
|
||||
"iram": (0x4037C000, 0x60000 + 16 * 1024),
|
||||
"rtcram": (0x50000000, 0x2000),
|
||||
}
|
||||
BUTTONS = BOARDS["xteink"]["buttons"]
|
||||
VARIANTS = BOARDS["xteink"]["variants"]
|
||||
|
||||
|
||||
def default_state_dir():
|
||||
return Path(tempfile.gettempdir()) / f"xteink-emu-{os.getuid()}"
|
||||
def default_state_dir(name=DEFAULT_BOARD):
|
||||
return Path(tempfile.gettempdir()) / f"esp-emu-{name}-{os.getuid()}"
|
||||
|
||||
|
||||
def ensure_qemu():
|
||||
def board_of(state):
|
||||
"""Board recorded at boot, so later commands do not have to repeat --board."""
|
||||
try:
|
||||
return board((Path(state) / "board").read_text().strip())
|
||||
except FileNotFoundError:
|
||||
return board(DEFAULT_BOARD)
|
||||
|
||||
|
||||
def require_buttons(state, command):
|
||||
spec = board_of(state)
|
||||
if not spec["buttons"]:
|
||||
raise RuntimeError(f"{command}: board {spec['machine']} has no buttons")
|
||||
return spec
|
||||
|
||||
|
||||
def require_touch(state, command):
|
||||
spec = board_of(state)
|
||||
if not spec["panel"]:
|
||||
raise RuntimeError(f"{command}: board {spec['machine']} has no touch panel")
|
||||
return spec
|
||||
|
||||
|
||||
def ensure_qemu(spec=None):
|
||||
"""Build the checkout's native emulator on first use unless an explicit binary was configured."""
|
||||
if os.environ.get("XTEINK_QEMU") or DEFAULT_PATHS["XTEINK_QEMU"].exists():
|
||||
env = (spec or BOARDS[DEFAULT_BOARD])["qemu_env"]
|
||||
if os.environ.get(env) or DEFAULT_PATHS[env].exists():
|
||||
return
|
||||
nix = shutil.which("nix")
|
||||
if not nix:
|
||||
raise RuntimeError("nix is required to build the native xteink emulator")
|
||||
raise RuntimeError("nix is required to build the native emulator")
|
||||
output = PROJECT_ROOT / "dist/qemu-native"
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
result = subprocess.run([
|
||||
@@ -73,7 +83,7 @@ def ensure_qemu():
|
||||
str(output),
|
||||
])
|
||||
if result.returncode:
|
||||
raise RuntimeError("failed to build the native xteink emulator")
|
||||
raise RuntimeError("failed to build the native emulator")
|
||||
|
||||
|
||||
def require_env(name):
|
||||
@@ -109,7 +119,7 @@ def is_running(state):
|
||||
def require_running(state):
|
||||
pid = pid_from(state)
|
||||
if not process_alive(pid):
|
||||
raise RuntimeError(f"xteink-emu is not running in {state}")
|
||||
raise RuntimeError(f"no emulator is running in {state}")
|
||||
return pid
|
||||
|
||||
|
||||
@@ -138,20 +148,37 @@ def qmp_command(state, execute, arguments=None, timeout=5):
|
||||
return send(command)
|
||||
|
||||
|
||||
def build_flash(firmware, output):
|
||||
def flash_parts(firmware, spec):
|
||||
"""PlatformIO writes bootloader.bin and partitions.bin beside firmware.bin, so a board
|
||||
without packaged assets takes them from the app image's own build directory."""
|
||||
if spec["packaged_flash_parts"]:
|
||||
return require_env("XTEINK_BOOTLOADER"), require_env("XTEINK_PARTITIONS")
|
||||
parts = []
|
||||
for name in ("bootloader.bin", "partitions.bin"):
|
||||
path = firmware.parent / name
|
||||
if not path.is_file():
|
||||
raise RuntimeError(f"{name} not found next to {firmware}")
|
||||
parts.append(path)
|
||||
return tuple(parts)
|
||||
|
||||
|
||||
def build_flash(firmware, output, spec):
|
||||
flash_size = spec["flash_size"]
|
||||
app = firmware.read_bytes()
|
||||
if len(app) == FLASH_SIZE:
|
||||
if len(app) == flash_size:
|
||||
output.write_bytes(app)
|
||||
return
|
||||
if not app or app[0] != 0xE9:
|
||||
raise RuntimeError(f"{firmware} is not an ESP32 app image")
|
||||
if APP_OFFSET + len(app) > FLASH_SIZE:
|
||||
raise RuntimeError(f"{firmware} does not fit in a 16 MiB flash image")
|
||||
if APP_OFFSET + len(app) > flash_size:
|
||||
raise RuntimeError(f"{firmware} does not fit in a {flash_size // (1024 * 1024)} MiB flash image")
|
||||
|
||||
flash = bytearray(b"\xff" * FLASH_SIZE)
|
||||
bootloader = require_env("XTEINK_BOOTLOADER").read_bytes()
|
||||
partitions = require_env("XTEINK_PARTITIONS").read_bytes()
|
||||
flash[: len(bootloader)] = bootloader
|
||||
flash = bytearray(b"\xff" * flash_size)
|
||||
bootloader_path, partitions_path = flash_parts(firmware, spec)
|
||||
bootloader = bootloader_path.read_bytes()
|
||||
partitions = partitions_path.read_bytes()
|
||||
offset = spec["bootloader_offset"]
|
||||
flash[offset : offset + len(bootloader)] = bootloader
|
||||
flash[PARTITIONS_OFFSET : PARTITIONS_OFFSET + len(partitions)] = partitions
|
||||
flash[APP_OFFSET : APP_OFFSET + len(app)] = app
|
||||
output.write_bytes(flash)
|
||||
@@ -166,13 +193,14 @@ def hold_power(state, hold_ms):
|
||||
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": released})
|
||||
|
||||
|
||||
def launch(state, firmware, sdcard, variant="x3", power_hold_ms=2000, interactive=False):
|
||||
def launch(state, firmware, sdcard, name=DEFAULT_BOARD, variant="x3", power_hold_ms=2000, interactive=False):
|
||||
"""Boot QEMU in `state`, returning the process once its QMP socket answers."""
|
||||
ensure_qemu()
|
||||
spec = board(name)
|
||||
ensure_qemu(spec)
|
||||
old_pid = pid_from(state)
|
||||
if process_alive(old_pid):
|
||||
raise RuntimeError(f"xteink-emu is already running as PID {old_pid} in {state}")
|
||||
if variant not in VARIANTS:
|
||||
raise RuntimeError(f"emulator is already running as PID {old_pid} in {state}")
|
||||
if spec["variants"] and variant not in spec["variants"]:
|
||||
raise RuntimeError(f"unknown variant: {variant}")
|
||||
|
||||
firmware = Path(firmware).expanduser().resolve()
|
||||
@@ -183,32 +211,46 @@ def launch(state, firmware, sdcard, variant="x3", power_hold_ms=2000, interactiv
|
||||
raise RuntimeError(f"SD card image not found: {sdcard}")
|
||||
|
||||
state.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
for name in ("pid", "qmp.sock", "serial.log", "qemu.log", "wait.offset"):
|
||||
(state / name).unlink(missing_ok=True)
|
||||
for scratch in ("pid", "qmp.sock", "serial.log", "qemu.log", "wait.offset"):
|
||||
(state / scratch).unlink(missing_ok=True)
|
||||
(state / "serial.log").touch()
|
||||
(state / "wait.offset").write_text("0\n")
|
||||
(state / "board").write_text(f"{name}\n")
|
||||
flash = state / "flash.bin"
|
||||
build_flash(firmware, flash)
|
||||
efuse = bytearray(1024)
|
||||
efuse[0x18:0x1E] = bytes.fromhex("563412005452")
|
||||
efuse[0x26] = 0x0C
|
||||
(state / "efuse.bin").write_bytes(efuse)
|
||||
build_flash(firmware, flash, spec)
|
||||
|
||||
machine = f"xteink,variant={variant}" if spec["variants"] else spec["machine"]
|
||||
command = [
|
||||
str(require_env("XTEINK_QEMU")),
|
||||
"-machine", f"xteink,variant={variant}",
|
||||
str(require_env(spec["qemu_env"])),
|
||||
"-machine", machine,
|
||||
"-accel", "tcg",
|
||||
"-L", str(require_env("XTEINK_BIOS")),
|
||||
"-display", "gtk" if interactive else "none",
|
||||
"-serial", "stdio",
|
||||
"-serial", spec["serial"],
|
||||
"-monitor", "none",
|
||||
"-qmp", f"unix:{state / 'qmp.sock'},server=on,wait=off",
|
||||
"-nic", "user,model=esp32c3_wifi,mac=52:54:00:12:34:56,net=192.168.4.0/24",
|
||||
"-drive", f"file={flash},if=mtd,format=raw",
|
||||
"-drive", f"file={state / 'efuse.bin'},if=none,format=raw,id=efuse",
|
||||
"-global", "driver=nvram.esp32c3.efuse,property=drive,value=efuse",
|
||||
"-drive", f"file={sdcard},if=sd,format=raw",
|
||||
]
|
||||
if spec["memory"]:
|
||||
command += ["-m", spec["memory"]]
|
||||
if name == "xteink":
|
||||
efuse = bytearray(1024)
|
||||
efuse[0x18:0x1E] = bytes.fromhex("563412005452")
|
||||
efuse[0x26] = 0x0C
|
||||
(state / "efuse.bin").write_bytes(efuse)
|
||||
command += [
|
||||
"-L", str(require_env("XTEINK_BIOS")),
|
||||
"-nic", "user,model=esp32c3_wifi,mac=52:54:00:12:34:56,net=192.168.4.0/24",
|
||||
"-drive", f"file={state / 'efuse.bin'},if=none,format=raw,id=efuse",
|
||||
"-global", "driver=nvram.esp32c3.efuse,property=drive,value=efuse",
|
||||
]
|
||||
else:
|
||||
# TCG is slow enough that IDF startup trips the timer-group watchdogs, and every
|
||||
# reset restarts SD initialisation.
|
||||
command += [
|
||||
"-nic", "user,model=esp32_wifi,mac=52:54:00:12:34:56,net=192.168.4.0/24",
|
||||
"-global", "driver=timer.esp32.timg,property=wdt_disable,value=true",
|
||||
]
|
||||
if interactive:
|
||||
process = subprocess.Popen(command)
|
||||
else:
|
||||
@@ -229,7 +271,7 @@ def launch(state, firmware, sdcard, variant="x3", power_hold_ms=2000, interactiv
|
||||
raise RuntimeError(f"QEMU exited during startup\n{message}")
|
||||
try:
|
||||
qmp_command(state, "query-status")
|
||||
if power_hold_ms:
|
||||
if power_hold_ms and spec["buttons"].get("power"):
|
||||
hold_power(state, power_hold_ms)
|
||||
return process
|
||||
except (FileNotFoundError, ConnectionRefusedError, socket.timeout):
|
||||
@@ -360,12 +402,12 @@ def await_samples(state, prop, count, timeout):
|
||||
|
||||
|
||||
def set_button(state, name, down):
|
||||
path, prop, pressed, released = BUTTONS[name]
|
||||
path, prop, pressed, released = require_buttons(state, name)["buttons"][name]
|
||||
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": pressed if down else released})
|
||||
|
||||
|
||||
def press(state, name, samples=8, timeout=90, hold_ms=500):
|
||||
path, prop, _, _ = BUTTONS[name]
|
||||
path, prop, _, _ = require_buttons(state, name)["buttons"][name]
|
||||
adc = not path.endswith("gpio")
|
||||
set_button(state, name, down=True)
|
||||
try:
|
||||
@@ -379,8 +421,60 @@ def press(state, name, samples=8, timeout=90, hold_ms=500):
|
||||
await_samples(state, prop, samples, timeout)
|
||||
|
||||
|
||||
def touch_move(state, x, y):
|
||||
"""Position the pointer in panel pixels; QEMU's absolute axes are 0..0x7fff."""
|
||||
width, height = require_touch(state, "touch")["panel"]
|
||||
if not (0 <= x < width and 0 <= y < height):
|
||||
raise RuntimeError(f"touch outside the {width}x{height} panel: {x},{y}")
|
||||
def scale(value, size):
|
||||
return (value * 0x7FFF + (size - 1) // 2) // (size - 1)
|
||||
|
||||
qmp_command(state, "input-send-event", {"events": [
|
||||
{"type": "abs", "data": {"axis": "x", "value": scale(x, width)}},
|
||||
{"type": "abs", "data": {"axis": "y", "value": scale(y, height)}},
|
||||
]})
|
||||
|
||||
|
||||
def touch_button(state, down):
|
||||
qmp_command(state, "input-send-event", {"events": [
|
||||
{"type": "btn", "data": {"button": "left", "down": down}},
|
||||
]})
|
||||
|
||||
|
||||
def tap(state, x, y, hold_ms=250):
|
||||
"""Press and release at one point.
|
||||
|
||||
The hold matters: the guest polls the touch controller between redraws, so an
|
||||
instantaneous press can fall entirely between two polls.
|
||||
"""
|
||||
touch_move(state, x, y)
|
||||
touch_button(state, down=True)
|
||||
try:
|
||||
time.sleep(hold_ms / 1000)
|
||||
finally:
|
||||
touch_button(state, down=False)
|
||||
|
||||
|
||||
def drag(state, x1, y1, x2, y2, steps=16, step_ms=30, settle_ms=250):
|
||||
"""Press at the first point, slide to the second, release.
|
||||
|
||||
Interpolated for the same reason tap holds: the guest polls the touch controller
|
||||
between redraws, and a jump straight to the end point is indistinguishable from a tap
|
||||
there. The settle repeats tap's hold so the press itself is never missed.
|
||||
"""
|
||||
touch_move(state, x1, y1)
|
||||
touch_button(state, down=True)
|
||||
try:
|
||||
time.sleep(settle_ms / 1000)
|
||||
for step in range(1, steps + 1):
|
||||
touch_move(state, round(x1 + (x2 - x1) * step / steps), round(y1 + (y2 - y1) * step / steps))
|
||||
time.sleep(step_ms / 1000)
|
||||
finally:
|
||||
touch_button(state, down=False)
|
||||
|
||||
|
||||
def dump_memory(state, region, output):
|
||||
base, size = MEMORY_REGIONS[region]
|
||||
base, size = board_of(state)["memory_regions"][region]
|
||||
output = Path(output).expanduser().resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
qmp_command(state, "pmemsave", {"val": base, "size": size, "filename": str(output)}, timeout=30)
|
||||
@@ -396,7 +490,7 @@ def memory_usage(state):
|
||||
scratch = state / "memory.bin"
|
||||
rows = []
|
||||
try:
|
||||
for region, (base, size) in MEMORY_REGIONS.items():
|
||||
for region, (base, size) in board_of(state)["memory_regions"].items():
|
||||
data = dump_memory(state, region, scratch).read_bytes()
|
||||
rows.append({"region": region, "base": base, "size": size, "touched": len(data) - data.count(0)})
|
||||
finally:
|
||||
@@ -410,5 +504,10 @@ def screendump(state, output, settle_ms=0):
|
||||
time.sleep(settle_ms / 1000)
|
||||
output = Path(output).expanduser().resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
qmp_command(state, "screendump", {"filename": str(output), "format": "png"}, timeout=30)
|
||||
arguments = {"filename": str(output), "format": "png"}
|
||||
console = board_of(state)["console"]
|
||||
if console:
|
||||
# Console 0 is the text console on this board, so the panel must be named.
|
||||
arguments["device"] = console
|
||||
qmp_command(state, "screendump", arguments, timeout=30)
|
||||
return output
|
||||
+69
-18
@@ -13,7 +13,14 @@ from . import sdcard
|
||||
PAGE = Path(__file__).resolve().parent / "index.html"
|
||||
MAX_UPLOAD = 512 * 1024 * 1024
|
||||
LONG_POLL_SECONDS = 25
|
||||
UPLOADS = {"firmware": "firmware.bin", "sdcard": "sdcard.img"}
|
||||
# Boards without packaged flash parts read these from beside the firmware, which is
|
||||
# exactly where an upload lands, so no extra plumbing is needed to assemble the image.
|
||||
UPLOADS = {
|
||||
"firmware": "firmware.bin",
|
||||
"sdcard": "sdcard.img",
|
||||
"bootloader": "bootloader.bin",
|
||||
"partitions": "partitions.bin",
|
||||
}
|
||||
|
||||
|
||||
def settings_path(state):
|
||||
@@ -31,6 +38,19 @@ def save_settings(state, values):
|
||||
settings_path(state).write_text(json.dumps(settings(state) | values))
|
||||
|
||||
|
||||
def panel_point(spec, x, y):
|
||||
"""Clamp a browser-supplied point into panel pixels.
|
||||
|
||||
The page maps clicks through a scaled image, so rounding at the edges can land a
|
||||
pixel outside the panel; emu.touch_move rejects that outright.
|
||||
"""
|
||||
panel = spec["panel"]
|
||||
if not panel:
|
||||
raise RuntimeError(f"board {spec['machine']} has no touch panel")
|
||||
width, height = panel
|
||||
return max(0, min(int(x), width - 1)), max(0, min(int(y), height - 1))
|
||||
|
||||
|
||||
def blank_sdcard(path, size_mb):
|
||||
"""Create a FAT32 card so the UI works without the user supplying an image."""
|
||||
mkfs = shutil.which("mkfs.fat")
|
||||
@@ -41,9 +61,16 @@ def blank_sdcard(path, size_mb):
|
||||
subprocess.run([mkfs, "-F", "32", str(path)], check=True, capture_output=True)
|
||||
|
||||
|
||||
def serve(state, host, port, log_bytes):
|
||||
def serve(state, host, port, log_bytes, name=emu.DEFAULT_BOARD):
|
||||
state.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
capture = state / "web-screen.png"
|
||||
spec = emu.board(name)
|
||||
|
||||
def ensure_sdcard(card, size_mb=64):
|
||||
if card.is_file():
|
||||
return
|
||||
blank_sdcard(card, size_mb)
|
||||
save_settings(state, {"sdcard": f"blank {size_mb} MiB"})
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
@@ -76,11 +103,14 @@ def serve(state, host, port, log_bytes):
|
||||
stored = settings(state)
|
||||
return {
|
||||
"running": emu.is_running(state),
|
||||
"board": spec["machine"],
|
||||
"variant": stored.get("variant", "x3"),
|
||||
"firmware": stored.get("firmware"),
|
||||
"sdcard": stored.get("sdcard"),
|
||||
"buttons": list(emu.BUTTONS),
|
||||
"variants": list(emu.VARIANTS),
|
||||
"buttons": list(spec["buttons"]),
|
||||
"variants": list(spec["variants"]),
|
||||
"panel": list(spec["panel"]) if spec["panel"] else None,
|
||||
"flashParts": not spec["packaged_flash_parts"],
|
||||
}
|
||||
|
||||
def do_GET(self):
|
||||
@@ -144,15 +174,15 @@ def serve(state, host, port, log_bytes):
|
||||
query = self.query()
|
||||
try:
|
||||
if route == "/upload":
|
||||
name = query.get("name", [""])[0]
|
||||
if name not in UPLOADS:
|
||||
upload = query.get("name", [""])[0]
|
||||
if upload not in UPLOADS:
|
||||
return self.send_error(404)
|
||||
data = self.body()
|
||||
if not data:
|
||||
raise RuntimeError(f"empty {name} upload")
|
||||
(state / UPLOADS[name]).write_bytes(data)
|
||||
filename = query.get("filename", [name])[0]
|
||||
save_settings(state, {name: filename})
|
||||
raise RuntimeError(f"empty {upload} upload")
|
||||
(state / UPLOADS[upload]).write_bytes(data)
|
||||
filename = query.get("filename", [upload])[0]
|
||||
save_settings(state, {upload: filename})
|
||||
return self.reply_json(self.status_payload())
|
||||
|
||||
if route == "/sdcard/blank":
|
||||
@@ -168,12 +198,18 @@ def serve(state, host, port, log_bytes):
|
||||
card = state / UPLOADS["sdcard"]
|
||||
if not firmware.is_file():
|
||||
raise RuntimeError("upload a firmware image first")
|
||||
if not card.is_file():
|
||||
blank_sdcard(card, 64)
|
||||
save_settings(state, {"sdcard": "blank 64 MiB"})
|
||||
if not spec["packaged_flash_parts"] and len(firmware.read_bytes()) != spec["flash_size"]:
|
||||
missing = [
|
||||
UPLOADS[part]
|
||||
for part in ("bootloader", "partitions")
|
||||
if not (state / UPLOADS[part]).is_file()
|
||||
]
|
||||
if missing:
|
||||
raise RuntimeError(f"this board also needs {' and '.join(missing)}")
|
||||
ensure_sdcard(card)
|
||||
if emu.is_running(state):
|
||||
emu.shutdown(state)
|
||||
emu.launch(state, firmware, card, variant=variant, power_hold_ms=hold)
|
||||
emu.launch(state, firmware, card, name=spec["name"], variant=variant, power_hold_ms=hold)
|
||||
save_settings(state, {"variant": variant})
|
||||
return self.reply_json(self.status_payload())
|
||||
|
||||
@@ -187,6 +223,7 @@ def serve(state, host, port, log_bytes):
|
||||
if emu.is_running(state):
|
||||
emu.shutdown(state)
|
||||
image = state / UPLOADS["sdcard"]
|
||||
ensure_sdcard(image) # the card can be populated before the first boot
|
||||
path = query.get("path", [""])[0]
|
||||
if route == "/files/write":
|
||||
sdcard.write(image, path, self.body())
|
||||
@@ -203,15 +240,29 @@ def serve(state, host, port, log_bytes):
|
||||
return self.reply_json(self.status_payload())
|
||||
|
||||
if route == "/button":
|
||||
name = query.get("name", [""])[0]
|
||||
if name not in emu.BUTTONS:
|
||||
button = query.get("name", [""])[0]
|
||||
if button not in spec["buttons"]:
|
||||
return self.send_error(404)
|
||||
emu.require_running(state)
|
||||
edge = query.get("edge", [""])[0]
|
||||
if edge in ("down", "up"):
|
||||
emu.set_button(state, name, down=edge == "down")
|
||||
emu.set_button(state, button, down=edge == "down")
|
||||
else:
|
||||
emu.press(state, name, hold_ms=int(query.get("hold-ms", ["500"])[0]))
|
||||
emu.press(state, button, hold_ms=int(query.get("hold-ms", ["500"])[0]))
|
||||
return self.reply("ok")
|
||||
|
||||
if route == "/touch":
|
||||
if not spec["panel"]:
|
||||
return self.send_error(404)
|
||||
emu.require_running(state)
|
||||
x, y = panel_point(spec, query.get("x", ["0"])[0], query.get("y", ["0"])[0])
|
||||
edge = query.get("edge", ["tap"])[0]
|
||||
if edge == "tap":
|
||||
emu.tap(state, x, y, hold_ms=int(query.get("hold-ms", ["250"])[0]))
|
||||
else:
|
||||
emu.touch_move(state, x, y)
|
||||
if edge in ("down", "up"):
|
||||
emu.touch_button(state, down=edge == "down")
|
||||
return self.reply("ok")
|
||||
except (RuntimeError, OSError, ValueError, subprocess.CalledProcessError) as error:
|
||||
return self.reply(str(error), status=500)
|
||||
+79
-8
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>xteink emulator</title>
|
||||
<title>esp32 emulator</title>
|
||||
<style>
|
||||
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #20211f; background: #e9e5dc; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -31,6 +31,8 @@ button.active { transform: translateY(2px); background: #d2752b; }
|
||||
.device-frame { display: grid; grid-template-columns: auto minmax(14rem, 30rem) auto; grid-template-rows: auto auto auto; gap: .6rem; }
|
||||
.screen-shell { grid-column: 2; grid-row: 2; padding: 1.1rem; border-radius: 1.2rem; background: #30312e; box-shadow: 0 1rem 2.5rem #5d584c40; }
|
||||
img { display: block; width: 100%; height: auto; aspect-ratio: 528 / 792; background: #fff; image-rendering: pixelated; }
|
||||
.touch img { cursor: crosshair; touch-action: none; }
|
||||
.no-buttons .btn-power, .no-buttons .btn-side, .no-buttons .btn-bottom { display: none; }
|
||||
.device-frame button { min-height: 0; border-radius: .6rem; font-size: .75rem; }
|
||||
.btn-power { grid-column: 2; grid-row: 1; height: 1.8rem; }
|
||||
.btn-side { grid-row: 2; width: 2rem; writing-mode: vertical-rl; }
|
||||
@@ -79,19 +81,25 @@ img { display: block; width: 100%; height: auto; aspect-ratio: 528 / 792; backgr
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<p class="eyebrow">ESP32-C3 emulator</p>
|
||||
<h1>xteink X3 / X4</h1>
|
||||
<p>Upload a firmware <code>.bin</code> (release app image or a full 16 MB flash) and boot it in the emulator running on this host. The SD card image persists between boots.</p>
|
||||
<p class="eyebrow">ESP32 emulator</p>
|
||||
<h1 id="title">emulator</h1>
|
||||
<p>Upload a firmware <code>.bin</code> (release app image or a full flash image) and boot it in the emulator running on this host. The SD card image persists between boots.</p>
|
||||
</header>
|
||||
|
||||
<form id="boot-form" class="boot-panel">
|
||||
<label>Firmware (.bin)
|
||||
<input id="firmware" type="file" accept=".bin,application/octet-stream">
|
||||
</label>
|
||||
<label id="bootloader-field" hidden>Bootloader (.bin)
|
||||
<input id="bootloader" type="file" accept=".bin,application/octet-stream">
|
||||
</label>
|
||||
<label id="partitions-field" hidden>Partitions (.bin)
|
||||
<input id="partitions" type="file" accept=".bin,application/octet-stream">
|
||||
</label>
|
||||
<label>SD card image (optional)
|
||||
<input id="sdcard" type="file" accept=".img,.bin,application/octet-stream">
|
||||
</label>
|
||||
<label>Device
|
||||
<label id="variant-field">Device
|
||||
<select id="variant"><option value="x3">X3</option><option value="x4">X4 (display stub)</option></select>
|
||||
</label>
|
||||
<button id="boot" type="submit">Boot firmware</button>
|
||||
@@ -101,11 +109,11 @@ img { display: block; width: 100%; height: auto; aspect-ratio: 528 / 792; backgr
|
||||
<p id="status" class="status" role="status">Loading.</p>
|
||||
|
||||
<div class="workspace">
|
||||
<section class="device" aria-label="Emulated xteink device">
|
||||
<div class="device-frame">
|
||||
<section class="device" aria-label="Emulated device">
|
||||
<div id="device-frame" class="device-frame">
|
||||
<button type="button" class="btn-power" data-button="power" data-hold-ms="2000">power</button>
|
||||
<button type="button" class="btn-side btn-left" data-button="left">left</button>
|
||||
<div class="screen-shell"><img id="screen" alt="E-ink display"></div>
|
||||
<div class="screen-shell"><img id="screen" alt="Device display"></div>
|
||||
<button type="button" class="btn-side btn-right" data-button="right">right</button>
|
||||
<div class="btn-bottom">
|
||||
<button type="button" data-button="bottom-1">1</button>
|
||||
@@ -173,6 +181,7 @@ const $ = (id) => document.getElementById(id);
|
||||
const status = $("status"), screen = $("screen"), log = $("log"), follow = $("follow");
|
||||
const deviceButtons = [...document.querySelectorAll("[data-button]")];
|
||||
let running = false;
|
||||
let panel = null;
|
||||
|
||||
function report(message, error) {
|
||||
status.textContent = message;
|
||||
@@ -190,9 +199,18 @@ function applyStatus(payload) {
|
||||
// A fresh QEMU process restarts its frame counter, so drop the cursor across boots.
|
||||
if (payload.running !== running) generation = null;
|
||||
running = payload.running;
|
||||
panel = payload.panel;
|
||||
for (const button of deviceButtons) button.disabled = !running;
|
||||
$("stop").disabled = !running;
|
||||
$("variant").value = payload.variant;
|
||||
$("title").textContent = payload.board;
|
||||
document.title = payload.board + " emulator";
|
||||
$("variant-field").hidden = !payload.variants.length;
|
||||
$("bootloader-field").hidden = !payload.flashParts;
|
||||
$("partitions-field").hidden = !payload.flashParts;
|
||||
$("device-frame").classList.toggle("no-buttons", !payload.buttons.length);
|
||||
$("device-frame").classList.toggle("touch", Boolean(panel));
|
||||
if (panel) screen.style.aspectRatio = `${panel[0]} / ${panel[1]}`;
|
||||
const parts = [running ? "Running." : "Stopped."];
|
||||
if (payload.firmware) parts.push(`firmware: ${payload.firmware}`);
|
||||
if (payload.sdcard) parts.push(`sd: ${payload.sdcard}`);
|
||||
@@ -249,6 +267,8 @@ $("boot-form").onsubmit = async (event) => {
|
||||
$("boot").disabled = true;
|
||||
try {
|
||||
await upload("firmware", $("firmware"));
|
||||
await upload("bootloader", $("bootloader"));
|
||||
await upload("partitions", $("partitions"));
|
||||
await upload("sdcard", $("sdcard"));
|
||||
report("Booting...", false);
|
||||
applyStatus(JSON.parse(await call(`boot?variant=${$("variant").value}`, { method: "POST" })));
|
||||
@@ -267,6 +287,57 @@ $("stop").onclick = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Touch - The panel takes physical pixels regardless of guest rotation, so the image
|
||||
// rect maps straight onto it. A press shorter than the guest's poll interval can fall
|
||||
// between two reads, hence the minimum hold on release.
|
||||
const MIN_HOLD_MS = 250;
|
||||
let pressedAt = 0, dragging = false, moveInFlight = false;
|
||||
|
||||
function panelPoint(event) {
|
||||
const rect = screen.getBoundingClientRect();
|
||||
const x = Math.round((event.clientX - rect.left) / rect.width * (panel[0] - 1));
|
||||
const y = Math.round((event.clientY - rect.top) / rect.height * (panel[1] - 1));
|
||||
return `x=${x}&y=${y}`;
|
||||
}
|
||||
|
||||
async function sendTouch(event, edge) {
|
||||
try {
|
||||
await call(`touch?${panelPoint(event)}&edge=${edge}`, { method: "POST" });
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
}
|
||||
}
|
||||
|
||||
screen.onpointerdown = async (event) => {
|
||||
if (!panel || !running) return;
|
||||
event.preventDefault();
|
||||
screen.setPointerCapture(event.pointerId);
|
||||
dragging = true;
|
||||
pressedAt = Date.now();
|
||||
await sendTouch(event, "down");
|
||||
};
|
||||
|
||||
screen.onpointermove = async (event) => {
|
||||
// Drop moves while one is in flight; the guest only samples between redraws anyway.
|
||||
if (!dragging || moveInFlight) return;
|
||||
moveInFlight = true;
|
||||
try {
|
||||
await sendTouch(event, "move");
|
||||
} finally {
|
||||
moveInFlight = false;
|
||||
}
|
||||
};
|
||||
|
||||
screen.onpointerup = async (event) => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
const remaining = MIN_HOLD_MS - (Date.now() - pressedAt);
|
||||
if (remaining > 0) await new Promise((done) => setTimeout(done, remaining));
|
||||
await sendTouch(event, "up");
|
||||
};
|
||||
|
||||
screen.onpointercancel = screen.onpointerup;
|
||||
|
||||
for (const button of deviceButtons) {
|
||||
button.onclick = async () => {
|
||||
button.classList.add("active");
|
||||
@@ -0,0 +1,111 @@
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from esp_emu import emu
|
||||
from esp_emu.cli import command_line, parser, run_script
|
||||
from esp_emu.web import panel_point, sdcard
|
||||
|
||||
|
||||
class CliTest(unittest.TestCase):
|
||||
def test_run_defaults_to_stdin_and_global_state(self):
|
||||
args = parser("e32r40t").parse_args(["--state", "/tmp/example", "run"])
|
||||
self.assertEqual(args.state, "/tmp/example")
|
||||
self.assertEqual(args.file, "-")
|
||||
self.assertEqual(args.board, "e32r40t")
|
||||
|
||||
def test_script_path_is_an_implicit_run(self):
|
||||
with tempfile.NamedTemporaryFile() as script:
|
||||
self.assertEqual(command_line([script.name]), ["run", script.name])
|
||||
|
||||
def test_shebang_flags_survive_the_implicit_run(self):
|
||||
with tempfile.NamedTemporaryFile() as script:
|
||||
arguments = command_line(["--board", "e32r40t", script.name])
|
||||
self.assertEqual(arguments, ["--board", "e32r40t", "run", script.name])
|
||||
self.assertEqual(parser().parse_args(arguments).board, "e32r40t")
|
||||
|
||||
def test_explicit_command_is_never_rewritten(self):
|
||||
with tempfile.NamedTemporaryFile() as script:
|
||||
self.assertEqual(command_line(["capture", script.name]), ["capture", script.name])
|
||||
|
||||
def test_script_stops_the_emulator_after_failure(self):
|
||||
with tempfile.NamedTemporaryFile() as script:
|
||||
args = parser().parse_args(["run", script.name])
|
||||
with (
|
||||
mock.patch("esp_emu.cli.execute_lines", side_effect=RuntimeError("failed")),
|
||||
mock.patch("esp_emu.cli.emu.is_running", return_value=True),
|
||||
mock.patch("esp_emu.cli.emu.shutdown") as shutdown,
|
||||
self.assertRaisesRegex(RuntimeError, "failed"),
|
||||
):
|
||||
run_script(args)
|
||||
shutdown.assert_called_once_with(emu.default_state_dir("xteink").resolve())
|
||||
|
||||
def test_each_board_gets_its_own_state_directory(self):
|
||||
self.assertNotEqual(emu.default_state_dir("xteink"), emu.default_state_dir("e32r40t"))
|
||||
|
||||
def test_board_specific_commands_are_gated(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
state = Path(temporary)
|
||||
(state / "board").write_text("e32r40t\n")
|
||||
with self.assertRaisesRegex(RuntimeError, "no buttons"):
|
||||
emu.require_buttons(state, "press")
|
||||
emu.require_touch(state, "tap")
|
||||
|
||||
(state / "board").write_text("xteink\n")
|
||||
with self.assertRaisesRegex(RuntimeError, "no touch panel"):
|
||||
emu.require_touch(state, "tap")
|
||||
emu.require_buttons(state, "press")
|
||||
|
||||
def test_web_boots_the_board_the_tool_was_launched_for(self):
|
||||
self.assertEqual(parser("e32r40t").parse_args(["web"]).board, "e32r40t")
|
||||
self.assertEqual(parser("xteink").parse_args(["web"]).board, "xteink")
|
||||
|
||||
def test_touch_points_are_clamped_to_the_panel(self):
|
||||
spec = emu.board("e32r40t")
|
||||
self.assertEqual(panel_point(spec, 0, 0), (0, 0))
|
||||
self.assertEqual(panel_point(spec, 319, 479), (319, 479))
|
||||
# Rounding through a scaled image can land just past the last pixel.
|
||||
self.assertEqual(panel_point(spec, 320, 480), (319, 479))
|
||||
self.assertEqual(panel_point(spec, -3, -1), (0, 0))
|
||||
with self.assertRaisesRegex(RuntimeError, "no touch panel"):
|
||||
panel_point(emu.board("xteink"), 1, 1)
|
||||
|
||||
def test_flash_layout_matches_the_board(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
(root / "bootloader.bin").write_bytes(b"\xe9boot")
|
||||
(root / "partitions.bin").write_bytes(b"parts")
|
||||
firmware = root / "firmware.bin"
|
||||
firmware.write_bytes(b"\xe9app")
|
||||
output = root / "flash.bin"
|
||||
|
||||
spec = emu.board("e32r40t")
|
||||
emu.build_flash(firmware, output, spec)
|
||||
image = output.read_bytes()
|
||||
self.assertEqual(len(image), spec["flash_size"])
|
||||
self.assertEqual(image[0x1000:0x1005], b"\xe9boot")
|
||||
self.assertEqual(image[0x8000:0x8005], b"parts")
|
||||
self.assertEqual(image[0x10000:0x10004], b"\xe9app")
|
||||
|
||||
@unittest.skipUnless(shutil.which("mkfs.fat") and shutil.which("mcopy"), "requires dosfstools and mtools")
|
||||
def test_directory_merge_overwrites_conflicts_and_preserves_other_files(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
source = root / "source"
|
||||
source.mkdir()
|
||||
(source / "host.txt").write_text("first")
|
||||
image = sdcard.create_image(root / "card.img")
|
||||
sdcard.merge_directory(image, source)
|
||||
sdcard.write(image, "/guest.txt", b"guest")
|
||||
|
||||
(source / "host.txt").write_text("second")
|
||||
sdcard.merge_directory(image, source)
|
||||
|
||||
self.assertEqual(sdcard.read(image, "/host.txt"), b"second")
|
||||
self.assertEqual(sdcard.read(image, "/guest.txt"), b"guest")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+1
-1
@@ -3,6 +3,6 @@ revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "xteink-emu-cli"
|
||||
name = "esp-emu-cli"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
@@ -1,91 +0,0 @@
|
||||
# xteink-emu
|
||||
|
||||
Scriptable controller for the native Xteink firmware emulator.
|
||||
|
||||
## Install
|
||||
|
||||
From the qemu-xteink checkout:
|
||||
|
||||
```sh
|
||||
uv tool install -e tools/xteink-emu-cli
|
||||
```
|
||||
|
||||
The editable install exposes `xteink-emu` globally and reflects source changes immediately. On first `boot`, it locates the containing qemu-xteink checkout and builds `.#qemu-native-xteink` with Nix if the native emulator is missing. Set `XTEINK_QEMU_REPO` to select a different checkout or `XTEINK_QEMU` to use an explicit binary.
|
||||
|
||||
The build requires Nix. SD helpers require `mtools` and `dosfstools` (`mcopy` and `mkfs.fat`).
|
||||
|
||||
## Basic Use
|
||||
|
||||
```sh
|
||||
xteink-emu boot firmware.bin
|
||||
xteink-emu wait-log 'Entering activity: Home' --timeout 60
|
||||
xteink-emu press bottom-4 bottom-4 bottom-2
|
||||
xteink-emu capture screen.png
|
||||
xteink-emu stop
|
||||
```
|
||||
|
||||
State defaults to `/tmp/xteink-emu-$UID`. Use a separate state for each test:
|
||||
|
||||
```sh
|
||||
xteink-emu --state /tmp/reader-test boot firmware.bin
|
||||
```
|
||||
|
||||
`boot` replaces an emulator already running in that state.
|
||||
|
||||
## SD Cards
|
||||
|
||||
```sh
|
||||
# Create or reuse $STATE/sdcard.img
|
||||
xteink-emu boot firmware.bin
|
||||
|
||||
# Use an existing image directly
|
||||
xteink-emu boot firmware.bin --sdcard card.img
|
||||
|
||||
# Merge a directory into the persistent state image before every boot
|
||||
xteink-emu boot firmware.bin --sdcard test-library/
|
||||
|
||||
# Recreate the state image before merging the directory
|
||||
xteink-emu boot firmware.bin --sdcard test-library/ --fresh-sd
|
||||
```
|
||||
|
||||
Directory imports overwrite matching files while preserving other image contents. The source directory is never modified.
|
||||
|
||||
## Scripts and Flows
|
||||
|
||||
A script is a newline-delimited list of the same commands, with blank lines and `#` comments allowed:
|
||||
|
||||
```text
|
||||
#!/usr/bin/env xteink-emu
|
||||
|
||||
boot firmware.bin
|
||||
wait-log "Entering activity: Home" --timeout 60
|
||||
press bottom-4 bottom-4 bottom-2
|
||||
wait-log "Entering activity: Settings"
|
||||
capture settings.png
|
||||
```
|
||||
|
||||
Make it executable and run it directly:
|
||||
|
||||
```sh
|
||||
chmod +x flow.xteink
|
||||
./flow.xteink
|
||||
```
|
||||
|
||||
A file argument is treated as an implicit `run`. Explicit file, custom-state, and stdin forms also work:
|
||||
|
||||
```sh
|
||||
xteink-emu flow.xteink
|
||||
xteink-emu --state /tmp/reader-test run flow.xteink
|
||||
printf '%s\n' 'boot firmware.bin' 'wait-log "ready"' | xteink-emu run
|
||||
```
|
||||
|
||||
The outer `--state` applies to every command in the script.
|
||||
|
||||
## Synchronization
|
||||
|
||||
- `press` waits for debounced guest press and release sampling; display waits are unnecessary between presses.
|
||||
- `wait-log REGEX` waits for new serial output and acts as an assertion.
|
||||
- `wait-frame [COUNT]` waits for actual e-ink refreshes when rendering itself matters.
|
||||
- `wait-idle SECONDS` waits for serial silence.
|
||||
|
||||
Run `xteink-emu --help` or `xteink-emu COMMAND --help` for all options.
|
||||
@@ -1,39 +0,0 @@
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from xteink_emu.cli import command_line, parser
|
||||
from xteink_emu.web import sdcard
|
||||
|
||||
|
||||
class CliTest(unittest.TestCase):
|
||||
def test_run_defaults_to_stdin_and_global_state(self):
|
||||
args = parser().parse_args(["--state", "/tmp/example", "run"])
|
||||
self.assertEqual(args.state, "/tmp/example")
|
||||
self.assertEqual(args.file, "-")
|
||||
|
||||
def test_script_path_is_an_implicit_run(self):
|
||||
with tempfile.NamedTemporaryFile() as script:
|
||||
self.assertEqual(command_line([script.name]), ["run", script.name])
|
||||
|
||||
@unittest.skipUnless(shutil.which("mkfs.fat") and shutil.which("mcopy"), "requires dosfstools and mtools")
|
||||
def test_directory_merge_overwrites_conflicts_and_preserves_other_files(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
source = root / "source"
|
||||
source.mkdir()
|
||||
(source / "host.txt").write_text("first")
|
||||
image = sdcard.create_image(root / "card.img")
|
||||
sdcard.merge_directory(image, source)
|
||||
sdcard.write(image, "/guest.txt", b"guest")
|
||||
|
||||
(source / "host.txt").write_text("second")
|
||||
sdcard.merge_directory(image, source)
|
||||
|
||||
self.assertEqual(sdcard.read(image, "/host.txt"), b"second")
|
||||
self.assertEqual(sdcard.read(image, "/guest.txt"), b"guest")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1264,6 +1264,10 @@ QemuConsole *qemu_console_lookup_by_device(DeviceState *dev, uint32_t head)
|
||||
uint32_t h;
|
||||
|
||||
QTAILQ_FOREACH(con, &consoles, next) {
|
||||
/* Text consoles have no backing device, so skip rather than abort. */
|
||||
if (!object_property_find(OBJECT(con), "device")) {
|
||||
continue;
|
||||
}
|
||||
obj = object_property_get_link(OBJECT(con),
|
||||
"device", &error_abort);
|
||||
if (DEVICE(obj) != dev) {
|
||||
|
||||
Reference in New Issue
Block a user