2026-03-16 23:23:31 +07:00
# ESP32 Emulation (Xtensa) — Technical Documentation
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
> Status: **Functional** · Backend complete · Frontend complete
> Engine: **lcgamboa/qemu-8.1.3** · Platform: **arduino-esp32 2.0.17 (IDF 4.4.x)**
> Available on: **Windows** (`.dll`) · **Linux / Docker** (`.so`, included in the official image)
> Applies to: **ESP32, ESP32-S3** (Xtensa LX6/LX7 architecture)
2026-03-16 09:15:31 +07:00
2026-03-16 23:23:31 +07:00
> **Note on ESP32-C3:** The ESP32-C3, XIAO ESP32-C3, and ESP32-C3 SuperMini boards use the **RISC-V RV32IMC** architecture and have their own in-browser emulator. See → [RISCV_EMULATION.md](./RISCV_EMULATION.md)
2026-03-14 22:05:35 +07:00
2026-03-17 00:28:16 +07:00
## Supported Boards
< table >
< tr >
< td align = "center" > < img src = "img/boards/esp32-devkit-c-v4.png" width = "160" alt = "ESP32 DevKit C V4" / > < br / > < b > ESP32 DevKit C V4< / b > < / td >
< td align = "center" > < img src = "img/boards/esp32-s3.png" width = "160" alt = "ESP32-S3" / > < br / > < b > ESP32-S3< / b > < / td >
< td align = "center" > < img src = "img/boards/esp32-cam.png" width = "160" alt = "ESP32-CAM" / > < br / > < b > ESP32-CAM< / b > < / td >
< td align = "center" > < img src = "img/boards/xiao-esp32-s3.png" width = "160" alt = "Seeed XIAO ESP32-S3" / > < br / > < b > Seeed XIAO ESP32-S3< / b > < / td >
< td align = "center" > < img src = "img/boards/arduino-nano-esp32.png" width = "160" alt = "Arduino Nano ESP32" / > < br / > < b > Arduino Nano ESP32< / b > < / td >
< / tr >
< / table >
2026-03-14 22:05:35 +07:00
---
2026-03-16 23:23:31 +07:00
## Table of Contents
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
1. [Quick Setup — Windows ](#1-quick-setup--windows )
2. [Quick Setup — Docker / Linux ](#2-quick-setup--docker--linux )
3. [General Architecture ](#3-general-architecture )
4. [System Components ](#4-system-components )
5. [Firmware — Requirements for lcgamboa ](#5-firmware--requirements-for-lcgamboa )
6. [Emulated WiFi ](#6-emulated-wifi )
7. [Emulated I2C ](#7-emulated-i2c )
2026-03-15 00:35:59 +07:00
8. [RMT / NeoPixel (WS2812) ](#8-rmt--neopixel-ws2812 )
2026-03-16 23:23:31 +07:00
9. [LEDC / PWM and GPIO Mapping ](#9-ledc--pwm-and-gpio-mapping )
10. [Building the Library Manually ](#10-building-the-library-manually )
2026-03-15 00:35:59 +07:00
11. [Tests ](#11-tests )
2026-03-16 23:23:31 +07:00
12. [Frontend — Implemented Events ](#12-frontend--implemented-events )
13. [Known Limitations ](#13-known-limitations )
14. [Environment Variables ](#14-environment-variables )
15. [GPIO Banks — GPIO32-39 Fix ](#15-gpio-banks--gpio32-39-fix )
16. [UI Interaction — ADC, Buttons, and Visual PWM ](#16-ui-interaction--adc-buttons-and-visual-pwm )
17. [lcgamboa Fork Modifications — Incremental Rebuild ](#17-lcgamboa-fork-modifications--incremental-rebuild )
2026-03-14 22:33:17 +07:00
---
2026-03-16 23:23:31 +07:00
## 1. Quick Setup — Windows
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
This section covers everything needed to get ESP32 emulation running from scratch on Windows.
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
### 1.1 System Prerequisites
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
| Tool | Minimum Version | Purpose |
|------|----------------|---------|
| Python | 3.11+ | FastAPI backend |
| MSYS2 | any | Build the QEMU DLL |
| arduino-cli | 1.x | Compile ESP32 sketches |
| esptool | 4.x or 5.x | Create 4 MB flash images |
| Git | 2.x | Clone the qemu-lcgamboa submodule |
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
### 1.2 Install MSYS2
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
Download and install from [msys2.org ](https://www.msys2.org ) or via winget:
2026-03-14 22:33:17 +07:00
```powershell
winget install MSYS2.MSYS2
```
2026-03-16 23:23:31 +07:00
Open the **MSYS2 MINGW64** terminal and run:
2026-03-14 22:33:17 +07:00
```bash
2026-03-16 23:23:31 +07:00
pacman -Syu # update base
2026-03-14 22:33:17 +07:00
pacman -S \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-glib2 \
mingw-w64-x86_64-libgcrypt \
mingw-w64-x86_64-libslirp \
mingw-w64-x86_64-pixman \
mingw-w64-x86_64-ninja \
mingw-w64-x86_64-meson \
mingw-w64-x86_64-python \
mingw-w64-x86_64-pkg-config \
git diffutils
```
2026-03-16 23:23:31 +07:00
### 1.3 Install arduino-cli and the ESP32 2.0.17 Core
2026-03-14 22:33:17 +07:00
```bash
2026-03-16 23:23:31 +07:00
# Install arduino-cli (if not already installed)
2026-03-14 22:33:17 +07:00
winget install ArduinoSA.arduino-cli
2026-03-16 23:23:31 +07:00
# Verify
2026-03-14 22:33:17 +07:00
arduino-cli version
2026-03-16 23:23:31 +07:00
# Add ESP32 support
2026-03-14 22:33:17 +07:00
arduino-cli core update-index
2026-03-16 23:23:31 +07:00
arduino-cli core install esp32:esp32@2.0.17 # ← IMPORTANT: 2.x, NOT 3.x
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
# Verify
arduino-cli core list # should show esp32:esp32 2.0.17
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
> **Why 2.0.17 and not 3.x?** The lcgamboa emulated WiFi periodically disables the SPI flash cache.
> In IDF 5.x (arduino-esp32 3.x) this causes a cache crash when core 0 interrupts
> try to execute code from IROM. IDF 4.4.x has different, compatible cache behavior.
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
### 1.4 Install esptool
2026-03-14 22:33:17 +07:00
```bash
pip install esptool
2026-03-16 23:23:31 +07:00
# Verify
esptool version # or: python -m esptool version
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
### 1.5 Build the QEMU DLL (libqemu-xtensa.dll)
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
The DLL is the main emulation engine. It needs to be compiled once from the `wokwi-libs/qemu-lcgamboa` submodule.
2026-03-14 22:33:17 +07:00
```bash
2026-03-16 23:23:31 +07:00
# Make sure you have the submodule
2026-03-14 22:33:17 +07:00
git submodule update --init wokwi-libs/qemu-lcgamboa
2026-03-16 23:23:31 +07:00
# In the MSYS2 MINGW64 terminal:
2026-03-15 00:35:59 +07:00
cd /e/Hardware/wokwi_clon/wokwi-libs/qemu-lcgamboa
bash build_libqemu-esp32-win.sh
2026-03-16 23:23:31 +07:00
# Produces: build/libqemu-xtensa.dll and build/libqemu-riscv32.dll
2026-03-15 00:35:59 +07:00
```
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
Copy the DLL to the backend:
2026-03-14 22:33:17 +07:00
2026-03-15 00:35:59 +07:00
```bash
cp build/libqemu-xtensa.dll /e/Hardware/wokwi_clon/backend/app/services/
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
**Verify the DLL was created:**
2026-03-14 22:33:17 +07:00
```bash
ls -lh backend/app/services/libqemu-xtensa.dll
2026-03-16 23:23:31 +07:00
# → should be ~40-50 MB
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
**Verify exports:**
2026-03-14 22:33:17 +07:00
```bash
objdump -p backend/app/services/libqemu-xtensa.dll | grep -i "qemu_picsimlab\|qemu_init"
2026-03-16 23:23:31 +07:00
# → should show qemu_init, qemu_main_loop, qemu_picsimlab_register_callbacks, etc.
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
### 1.6 Obtain the ESP32 ROM Binaries
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
The DLL requires two ROM files from Espressif to boot the ESP32. They must be placed in the same folder as the DLL:
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
**Option A — From esp-qemu (if installed):**
2026-03-14 22:33:17 +07:00
```bash
copy "C:\esp-qemu\qemu\share\qemu\esp32-v3-rom.bin" backend\app\services\
copy "C:\esp-qemu\qemu\share\qemu\esp32-v3-rom-app.bin" backend\app\services\
```
2026-03-16 23:23:31 +07:00
**Option B — From the lcgamboa submodule (easier):**
2026-03-14 22:33:17 +07:00
```bash
2026-03-15 00:35:59 +07:00
cp wokwi-libs/qemu-lcgamboa/pc-bios/esp32-v3-rom.bin backend/app/services/
cp wokwi-libs/qemu-lcgamboa/pc-bios/esp32-v3-rom-app.bin backend/app/services/
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
**Verify:**
2026-03-14 22:33:17 +07:00
```bash
ls -lh backend/app/services/esp32-v3-rom.bin
ls -lh backend/app/services/esp32-v3-rom-app.bin
2026-03-16 23:23:31 +07:00
# → both ~446 KB
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
### 1.7 Install Backend Python Dependencies
2026-03-14 22:33:17 +07:00
```bash
cd backend
python -m venv venv
venv\Scripts\activate # Windows
pip install -r requirements.txt
```
2026-03-16 23:23:31 +07:00
### 1.8 Verify Installation with Tests
2026-03-14 22:33:17 +07:00
```bash
2026-03-16 23:23:31 +07:00
# From the repo root (with venv activated):
2026-03-14 22:33:17 +07:00
python -m pytest test/esp32/test_esp32_lib_bridge.py -v
2026-03-16 23:23:31 +07:00
# Expected result: 28 passed in ~13 seconds
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
If you see `28 passed` — the emulation is fully functional.
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
**Additional tests (Arduino ↔ ESP32 serial):**
2026-03-14 22:33:17 +07:00
```bash
python -m pytest test/esp32/test_arduino_esp32_integration.py -v
2026-03-16 23:23:31 +07:00
# Expected result: 13 passed
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
### 1.9 Start the Backend with ESP32 Emulation
2026-03-14 22:33:17 +07:00
```bash
cd backend
venv\Scripts\activate
uvicorn app.main:app --reload --port 8001
```
2026-03-16 23:23:31 +07:00
The system automatically detects the DLL. Verify in the logs:
2026-03-14 22:33:17 +07:00
```
INFO: libqemu-xtensa.dll found at backend/app/services/libqemu-xtensa.dll
2026-03-15 00:35:59 +07:00
INFO: EspLibManager: lib mode active (GPIO, ADC, UART, WiFi, I2C, SPI, RMT, LEDC)
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
If it does not appear, verify with:
2026-03-14 22:33:17 +07:00
```bash
python -c "
import sys; sys.path.insert(0,'backend')
from app.services.esp32_lib_manager import esp_lib_manager
2026-03-16 23:23:31 +07:00
print('lib available:', esp_lib_manager.is_available())
2026-03-14 22:33:17 +07:00
"
```
2026-03-16 23:23:31 +07:00
### 1.10 Compile Your Own ESP32 Sketch
2026-03-14 22:33:17 +07:00
```bash
2026-03-16 23:23:31 +07:00
# Compile with DIO flash mode (required by QEMU lcgamboa):
2026-03-14 22:33:17 +07:00
arduino-cli compile \
--fqbn esp32:esp32:esp32:FlashMode=dio \
--output-dir build/ \
mi_sketch/
2026-03-16 23:23:31 +07:00
# Create a complete 4 MB image (required for QEMU):
2026-03-14 22:33:17 +07:00
esptool --chip esp32 merge_bin \
--fill-flash-size 4MB \
-o firmware.merged.bin \
--flash_mode dio \
--flash_size 4MB \
0x1000 build/mi_sketch.ino.bootloader.bin \
0x8000 build/mi_sketch.ino.partitions.bin \
0x10000 build/mi_sketch.ino.bin
```
2026-03-16 23:23:31 +07:00
The `firmware.merged.bin` file is what gets loaded into the emulation.
2026-03-14 22:33:17 +07:00
---
2026-03-16 23:23:31 +07:00
## 2. Quick Setup — Docker / Linux
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
**Full ESP32 emulation is included in the official Docker image.** No additional installation is required — the `libqemu-xtensa.so` is compiled automatically during the image build from the lcgamboa fork.
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
### 2.1 Use the Pre-built Image (Recommended)
2026-03-15 00:35:59 +07:00
```bash
docker run -d \
--name velxio \
-p 3080:80 \
-v $(pwd)/data:/app/data \
2026-03-16 23:23:31 +07:00
-e SECRET_KEY=your-secret \
2026-03-15 00:35:59 +07:00
ghcr.io/davidmonterocrespo24/velxio:master
```
2026-03-16 23:23:31 +07:00
ESP32 emulation with full GPIO is active automatically. No additional environment variables are needed.
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
### 2.2 Local Image Build
2026-03-15 00:35:59 +07:00
```bash
git clone https://github.com/davidmonterocrespo24/velxio.git
cd velxio
docker build -f Dockerfile.standalone -t velxio .
2026-03-16 23:23:31 +07:00
docker run -d -p 3080:80 -e SECRET_KEY=secret velxio
2026-03-15 00:35:59 +07:00
```
2026-03-16 23:23:31 +07:00
> **Build time note:** QEMU compilation takes 15-30 minutes the first time.
> Subsequent builds use the Docker cached layer — they are instantaneous as long as
> the lcgamboa source has not changed.
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
### 2.3 Verify ESP32 Emulation in the Container
2026-03-15 00:35:59 +07:00
```bash
2026-03-16 23:23:31 +07:00
# Verify that .so and ROMs are present
2026-03-15 00:35:59 +07:00
docker exec < container_id > ls -lh /app/lib/
2026-03-16 23:23:31 +07:00
# Verify that ctypes can load the .so
2026-03-15 00:35:59 +07:00
docker exec < container_id > python3 -c \
"import ctypes; ctypes.CDLL('/app/lib/libqemu-xtensa.so'); print('OK')"
2026-03-16 23:23:31 +07:00
# Verify that the manager detects it
2026-03-15 00:35:59 +07:00
docker exec < container_id > python3 -c \
"import sys; sys.path.insert(0,'/app')
from app.services.esp32_lib_manager import esp_lib_manager
2026-03-16 23:23:31 +07:00
print('ESP32 lib available:', esp_lib_manager.is_available())"
2026-03-15 00:35:59 +07:00
```
2026-03-16 23:23:31 +07:00
### 2.4 Linux (without Docker)
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
If you run the backend directly on Linux:
2026-03-15 00:35:59 +07:00
```bash
2026-03-16 23:23:31 +07:00
# 1. Install runtime dependencies
2026-03-15 00:35:59 +07:00
sudo apt-get install -y libglib2.0-0 libgcrypt20 libslirp0 libpixman-1-0
2026-03-16 23:23:31 +07:00
# 2. Compile the .so (requires build tools)
2026-03-15 00:35:59 +07:00
sudo apt-get install -y git python3-pip ninja-build pkg-config flex bison \
gcc g++ make libglib2.0-dev libgcrypt20-dev libslirp-dev libpixman-1-dev libfdt-dev
pip3 install meson
git clone --depth=1 --branch picsimlab-esp32 \
https://github.com/lcgamboa/qemu /tmp/qemu-lcgamboa
cd /tmp/qemu-lcgamboa
bash build_libqemu-esp32.sh
# → build/libqemu-xtensa.so
2026-03-16 23:23:31 +07:00
# 3. Copy .so and ROMs next to the Python module
cp build/libqemu-xtensa.so /path/to/project/backend/app/services/
cp pc-bios/esp32-v3-rom.bin /path/to/project/backend/app/services/
cp pc-bios/esp32-v3-rom-app.bin /path/to/project/backend/app/services/
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
# 4. Start backend (auto-detects the .so)
cd /path/to/project/backend
2026-03-15 00:35:59 +07:00
uvicorn app.main:app --reload --port 8001
```
---
2026-03-16 23:23:31 +07:00
## 3. General Architecture
2026-03-14 22:05:35 +07:00
```
2026-03-16 23:23:31 +07:00
User (browser)
2026-03-14 22:05:35 +07:00
└── WebSocket (/ws/{client_id})
└── simulation.py (FastAPI router)
2026-03-16 23:23:31 +07:00
├── EspLibManager ← backend with .so/.dll (GPIO, WiFi, I2C, SPI, RMT…)
└── EspQemuManager ← UART-only fallback via subprocess
2026-03-14 22:05:35 +07:00
│
2026-03-15 00:35:59 +07:00
[QEMU_ESP32_LIB=libqemu-xtensa.so|.dll]
2026-03-14 22:05:35 +07:00
│
Esp32LibBridge (ctypes)
│
2026-03-16 23:23:31 +07:00
libqemu-xtensa.so/.dll ← lcgamboa fork of QEMU 8.1.3
2026-03-14 22:05:35 +07:00
│
Machine: esp32-picsimlab
│
┌──────────┴──────────┐
2026-03-16 23:23:31 +07:00
CPU Xtensa LX6 emulated peripherals
2026-03-14 22:05:35 +07:00
(dual-core) GPIO · ADC · UART · I2C · SPI
RMT · LEDC · Timer · WiFi · Flash
```
2026-03-16 23:23:31 +07:00
The system selects the backend automatically:
- **lib available** → `EspLibManager` (full GPIO + all peripherals)
- **lib absent** → `EspQemuManager` (UART serial only via TCP, QEMU subprocess)
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
Automatic detection:
| Platform | Library searched | Source |
|----------|-----------------|--------|
| Docker / Linux | `/app/lib/libqemu-xtensa.so` | Compiled in the Dockerfile |
| Windows (development) | `backend/app/services/libqemu-xtensa.dll` | Compiled with MSYS2 |
| Custom | `$QEMU_ESP32_LIB` | Environment variable |
2026-03-14 22:05:35 +07:00
---
2026-03-16 23:23:31 +07:00
## 4. System Components
2026-03-14 22:05:35 +07:00
2026-03-15 00:35:59 +07:00
### 4.1 `libqemu-xtensa.so` / `libqemu-xtensa.dll`
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
Compiled from the [lcgamboa/qemu ](https://github.com/lcgamboa/qemu ) fork, branch `picsimlab-esp32` .
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**Runtime dependencies:**
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
*Windows (resolved automatically from `C:\msys64\mingw64\bin\` ):*
2026-03-14 22:05:35 +07:00
```
2026-03-15 00:35:59 +07:00
libglib-2.0-0.dll, libgcrypt-20.dll, libslirp-0.dll,
2026-03-16 23:23:31 +07:00
libgpg-error-0.dll, libintl-8.dll, libpcre2-8-0.dll (+~15 MinGW64 DLLs)
2026-03-14 22:05:35 +07:00
```
2026-03-16 23:23:31 +07:00
*Linux / Docker (system packages):*
2026-03-15 00:35:59 +07:00
```
libglib2.0-0, libgcrypt20, libslirp0, libpixman-1-0
```
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**Required ROM binaries** (in the same folder as the lib):
2026-03-14 22:05:35 +07:00
```
2026-03-15 00:35:59 +07:00
# Windows (backend/app/services/):
2026-03-16 23:23:31 +07:00
libqemu-xtensa.dll ← main engine (not in git — 43 MB)
esp32-v3-rom.bin ← ESP32 boot ROM (not in git — 446 KB)
esp32-v3-rom-app.bin ← application ROM (not in git — 446 KB)
2026-03-15 00:35:59 +07:00
# Docker (/app/lib/):
2026-03-16 23:23:31 +07:00
libqemu-xtensa.so ← compiled in Stage 0 of the Dockerfile
2026-03-15 00:35:59 +07:00
libqemu-riscv32.so ← ESP32-C3 (RISC-V)
2026-03-16 23:23:31 +07:00
esp32-v3-rom.bin ← copied from the lcgamboa repo's pc-bios/
2026-03-15 00:35:59 +07:00
esp32-v3-rom-app.bin
2026-03-14 22:05:35 +07:00
```
2026-03-16 23:23:31 +07:00
> On Windows these files are in `.gitignore` due to their size. Each developer generates them locally.
> In Docker they are automatically included in the image.
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**Library exports:**
2026-03-14 22:05:35 +07:00
```c
void qemu_init(int argc, char** argv, char** envp)
void qemu_main_loop(void)
void qemu_cleanup(void)
void qemu_picsimlab_register_callbacks(callbacks_t* cbs)
void qemu_picsimlab_set_pin(int slot, int value) // GPIO input
void qemu_picsimlab_set_apin(int channel, int value) // ADC input (0-4095)
void qemu_picsimlab_uart_receive(int id, uint8_t* buf, int size)
void* qemu_picsimlab_get_internals(int type) // LEDC duty array
int qemu_picsimlab_get_TIOCM(void) // UART modem lines
```
2026-03-16 23:23:31 +07:00
**C callbacks struct:**
2026-03-14 22:05:35 +07:00
```c
typedef struct {
void (*picsimlab_write_pin)(int pin, int value); // GPIO output changed
void (*picsimlab_dir_pin)(int pin, int value); // GPIO direction changed
int (*picsimlab_i2c_event)(uint8_t id, uint8_t addr, uint16_t event);
uint8_t (*picsimlab_spi_event)(uint8_t id, uint16_t event);
void (*picsimlab_uart_tx_event)(uint8_t id, uint8_t value);
const short int *pinmap; // slot → GPIO number mapping
void (*picsimlab_rmt_event)(uint8_t ch, uint32_t config0, uint32_t value);
} callbacks_t;
```
---
2026-03-15 00:35:59 +07:00
### 4.2 GPIO Pinmap
2026-03-14 22:05:35 +07:00
```python
# Identity mapping: QEMU IRQ slot i → GPIO number i-1
_PINMAP = (ctypes.c_int16 * 41)(
40, # pinmap[0] = count
*range(40) # pinmap[1..40] = GPIO 0..39
)
```
2026-03-16 23:23:31 +07:00
When GPIO N changes, QEMU calls `picsimlab_write_pin(slot=N+1, value)` .
The bridge automatically translates slot → actual GPIO before notifying listeners.
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**Input-only GPIOs on ESP32-WROOM-32:** `{34, 35, 36, 39}` — cannot be outputs.
2026-03-14 22:05:35 +07:00
---
2026-03-15 00:35:59 +07:00
### 4.3 `Esp32LibBridge` (Python ctypes)
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
File: `backend/app/services/esp32_lib_bridge.py`
2026-03-14 22:05:35 +07:00
```python
bridge = Esp32LibBridge(lib_path, asyncio_loop)
2026-03-16 23:23:31 +07:00
# Register listeners (async, called from asyncio)
2026-03-14 22:05:35 +07:00
bridge.register_gpio_listener(fn) # fn(gpio_num: int, value: int)
bridge.register_dir_listener(fn) # fn(gpio_num: int, direction: int)
bridge.register_uart_listener(fn) # fn(uart_id: int, byte_val: int)
bridge.register_rmt_listener(fn) # fn(channel: int, config0: int, value: int)
2026-03-16 23:23:31 +07:00
# Register I2C/SPI handlers (sync, called from QEMU thread)
2026-03-14 22:05:35 +07:00
bridge.register_i2c_handler(fn) # fn(bus, addr, event) -> int
bridge.register_spi_handler(fn) # fn(bus, event) -> int
# Control
bridge.start(firmware_b64, machine='esp32-picsimlab')
bridge.stop()
bridge.is_alive # bool
# GPIO / ADC / UART
2026-03-16 23:23:31 +07:00
bridge.set_pin(gpio_num, value) # Drive GPIO input (uses actual GPIO 0-39)
bridge.set_adc(channel, millivolts) # ADC in mV (0-3300)
bridge.set_adc_raw(channel, raw) # ADC in 12-bit raw (0-4095)
bridge.uart_send(uart_id, data) # Send bytes to ESP32 UART RX
2026-03-14 22:05:35 +07:00
# LEDC/PWM
2026-03-16 23:23:31 +07:00
bridge.get_ledc_duty(channel) # channel 0-15 → raw duty | None
2026-03-14 22:05:35 +07:00
bridge.get_tiocm() # UART modem lines bitmask
```
2026-03-16 23:23:31 +07:00
**Critical threading:**
`qemu_init()` and `qemu_main_loop()` **must run in the same thread** (BQL — Big QEMU Lock is thread-local). The bridge runs them in a single daemon thread:
2026-03-14 22:05:35 +07:00
```python
2026-03-16 23:23:31 +07:00
# Correct:
2026-03-14 22:05:35 +07:00
def _qemu_thread():
2026-03-15 00:35:59 +07:00
lib.qemu_init(argc, argv, None) # init
2026-03-16 23:23:31 +07:00
lib.qemu_main_loop() # blocks indefinitely
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
# Incorrect:
lib.qemu_init(...) # in thread A
lib.qemu_main_loop() # in thread B ← crash: "qemu_mutex_unlock_iothread assertion failed"
2026-03-14 22:05:35 +07:00
```
---
2026-03-15 00:35:59 +07:00
### 4.4 `EspLibManager` (Python)
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
File: `backend/app/services/esp32_lib_manager.py`
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
Converts hardware callbacks into **WebSocket events** for the frontend:
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
| Event emitted | Data | When |
|---------------|------|------|
| `system` | `{event: 'booting'│'booted'│'crash'│'reboot', ...}` | Lifecycle |
| `serial_output` | `{data: str, uart: 0│1│2}` | ESP32 UART TX |
| `gpio_change` | `{pin: int, state: 0│1}` | GPIO output changes |
| `gpio_dir` | `{pin: int, dir: 0│1}` | GPIO changes direction |
| `i2c_event` | `{bus, addr, event, response}` | I2C transaction |
| `spi_event` | `{bus, event, response}` | SPI transaction |
| `rmt_event` | `{channel, config0, value, level0, dur0, level1, dur1}` | RMT pulse |
| `ws2812_update` | `{channel, pixels: [[r,g,b],...]}` | Complete NeoPixel frame |
| `ledc_update` | `{channel, duty, duty_pct, gpio}` | PWM duty cycle + GPIO controlled by that channel |
| `error` | `{message: str}` | Boot error |
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**Crash and reboot detection:**
2026-03-14 22:05:35 +07:00
```python
"Cache disabled but cached memory region accessed" → event: crash
"Rebooting..." → event: reboot
```
2026-03-16 23:23:31 +07:00
**Manager public API:**
2026-03-14 22:05:35 +07:00
```python
manager = esp_lib_manager # singleton
manager.start_instance(client_id, board_type, callback, firmware_b64)
manager.stop_instance(client_id)
manager.load_firmware(client_id, firmware_b64) # hot-reload
manager.set_pin_state(client_id, gpio_num, value) # GPIO input
manager.set_adc(client_id, channel, millivolts)
manager.set_adc_raw(client_id, channel, raw)
await manager.send_serial_bytes(client_id, data, uart_id=0)
2026-03-16 23:23:31 +07:00
manager.set_i2c_response(client_id, addr, byte) # Simulate I2C device
manager.set_spi_response(client_id, byte) # Simulate SPI device
await manager.poll_ledc(client_id) # Read PWM (call periodically)
manager.get_status(client_id) # → dict with runtime state
2026-03-14 22:05:35 +07:00
```
---
2026-03-16 23:23:31 +07:00
### 4.5 `simulation.py` — WebSocket Messages
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**Frontend → Backend (incoming messages):**
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
| Type | Data | Action |
|------|------|--------|
| `start_esp32` | `{board, firmware_b64?}` | Start emulation |
| `stop_esp32` | `{}` | Stop |
2026-03-14 22:05:35 +07:00
| `load_firmware` | `{firmware_b64}` | Hot-reload firmware |
2026-03-16 23:23:31 +07:00
| `esp32_gpio_in` | `{pin, state}` | Drive GPIO input (actual GPIO 0-39) |
| `esp32_serial_input` | `{bytes: [int], uart: 0}` | Send serial data to ESP32 |
2026-03-14 22:05:35 +07:00
| `esp32_uart1_input` | `{bytes: [int]}` | UART1 RX |
| `esp32_uart2_input` | `{bytes: [int]}` | UART2 RX |
2026-03-16 23:23:31 +07:00
| `esp32_adc_set` | `{channel, millivolts?}` or `{channel, raw?}` | Set ADC |
| `esp32_i2c_response` | `{addr, response}` | Configure I2C response |
| `esp32_spi_response` | `{response}` | Configure SPI MISO |
| `esp32_status` | `{}` | Query runtime state |
2026-03-14 22:05:35 +07:00
---
2026-03-16 23:23:31 +07:00
## 5. Firmware — Requirements for lcgamboa
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
### 5.1 Required Platform Version
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**✅ Use: arduino-esp32 2.x (IDF 4.4.x)**
**❌ Do not use: arduino-esp32 3.x (IDF 5.x)**
2026-03-14 22:05:35 +07:00
```bash
arduino-cli core install esp32:esp32@2.0.17
```
2026-03-16 23:23:31 +07:00
**Why:** The lcgamboa emulated WiFi (core 1) periodically disables the SPI flash cache. In IDF 5.x this causes a crash when core 0 interrupts try to execute code from IROM (flash cache). In IDF 4.4.x the cache behavior is different and compatible.
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**Crash message (IDF 5.x):**
2026-03-14 22:05:35 +07:00
```
Guru Meditation Error: Core / panic'ed (Cache error).
Cache disabled but cached memory region accessed
EXCCAUSE: 0x00000007
```
2026-03-16 23:23:31 +07:00
### 5.2 Flash Image
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
The image must be a complete **4 MB** binary file (merged flash format):
2026-03-14 22:05:35 +07:00
```bash
2026-03-16 23:23:31 +07:00
# Compile with DIO flash mode:
2026-03-14 22:05:35 +07:00
arduino-cli compile --fqbn esp32:esp32:esp32:FlashMode=dio \
--output-dir build/ sketch/
2026-03-16 23:23:31 +07:00
# Create complete 4MB image (mandatory! QEMU requires exactly 2/4/8/16 MB):
2026-03-14 22:05:35 +07:00
esptool --chip esp32 merge_bin \
2026-03-14 22:33:17 +07:00
--fill-flash-size 4MB \
2026-03-14 22:05:35 +07:00
-o firmware.merged.bin \
2026-03-14 22:33:17 +07:00
--flash_mode dio \
--flash_size 4MB \
0x1000 build/sketch.ino.bootloader.bin \
0x8000 build/sketch.ino.partitions.bin \
0x10000 build/sketch.ino.bin
2026-03-14 22:05:35 +07:00
```
2026-03-16 23:23:31 +07:00
The backend (`arduino_cli.py`) forces `FlashMode=dio` automatically for all `esp32:*` targets.
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
### 5.3 lcgamboa-Compatible Sketch (Minimal IRAM-Safe Example)
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
For sketches that require maximum compatibility (without the Arduino framework):
2026-03-14 22:05:35 +07:00
```cpp
2026-03-16 23:23:31 +07:00
// Direct GPIO via registers (avoids code in flash in ISRs)
2026-03-14 22:05:35 +07:00
#define GPIO_OUT_W1TS (*((volatile uint32_t*)0x3FF44008))
#define GPIO_OUT_W1TC (*((volatile uint32_t*)0x3FF4400C))
#define GPIO_ENABLE_W1TS (*((volatile uint32_t*)0x3FF44020))
#define LED_BIT (1u << 2) // GPIO2
2026-03-16 23:23:31 +07:00
// ROM functions (always in IRAM, never crash)
2026-03-14 22:05:35 +07:00
extern "C" {
void ets_delay_us(uint32_t us);
int esp_rom_printf(const char* fmt, ...);
}
2026-03-16 23:23:31 +07:00
// Strings in DRAM (not in flash)
2026-03-14 22:05:35 +07:00
static const char DRAM_ATTR s_on[] = "LED_ON\n";
static const char DRAM_ATTR s_off[] = "LED_OFF\n";
void IRAM_ATTR setup() {
GPIO_ENABLE_W1TS = LED_BIT;
for (int i = 0; i < 5 ; i + + ) {
GPIO_OUT_W1TS = LED_BIT;
esp_rom_printf(s_on);
ets_delay_us(300000); // 300 ms
GPIO_OUT_W1TC = LED_BIT;
esp_rom_printf(s_off);
ets_delay_us(300000);
}
}
void IRAM_ATTR loop() { ets_delay_us(1000000); }
```
2026-03-16 23:23:31 +07:00
**Normal Arduino sketches** (with `Serial.print` , `delay` , `digitalWrite` ) also work correctly with IDF 4.4.x.
2026-03-14 22:05:35 +07:00
---
2026-03-16 23:23:31 +07:00
## 6. Emulated WiFi
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
lcgamboa implements a simulated WiFi with hardcoded SSIDs:
2026-03-14 22:05:35 +07:00
```cpp
2026-03-16 23:23:31 +07:00
// Only these networks are available in the emulation:
WiFi.begin("PICSimLabWifi", ""); // no password
2026-03-14 22:05:35 +07:00
WiFi.begin("Espressif", "");
```
2026-03-16 23:23:31 +07:00
The emulated ESP32 can:
- Scan networks (`WiFi.scanNetworks()`) → returns the two SSIDs
- Connect and obtain an IP (`192.168.4.x`)
- Open TCP/UDP sockets (via SLIRP — NAT to the host)
- Use `HTTPClient` , `WebServer` , etc.
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**Limitations:**
- There is no way to configure the SSIDs or passwords from Python
- The virtual "router" IP is `10.0.2.2` (host)
- The emulated ESP32 is accessible at `localhost:PORT` via SLIRP port forwarding
2026-03-14 22:05:35 +07:00
---
2026-03-16 23:23:31 +07:00
## 7. Emulated I2C
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
The I2C callback is **synchronous** — QEMU waits for the response before continuing:
2026-03-14 22:05:35 +07:00
```python
2026-03-16 23:23:31 +07:00
# I2C event protocol (field `event`):
0x0100 # START + address (READ if bit0 of addr=1)
0x0200 # WRITE byte (byte in bits 7:0 of event)
0x0300 # READ request (the callback must return the byte to place on SDA)
2026-03-14 22:05:35 +07:00
0x0000 # STOP / idle
```
2026-03-16 23:23:31 +07:00
**Simulating an I2C sensor** (e.g. temperature):
2026-03-14 22:05:35 +07:00
```python
2026-03-16 23:23:31 +07:00
# Configure which byte the ESP32 returns when reading address 0x48:
2026-03-14 22:05:35 +07:00
esp_lib_manager.set_i2c_response(client_id, addr=0x48, response_byte=75)
```
2026-03-16 23:23:31 +07:00
Via WebSocket:
2026-03-14 22:05:35 +07:00
```json
{"type": "esp32_i2c_response", "data": {"addr": 72, "response": 75}}
```
---
2026-03-15 00:35:59 +07:00
## 8. RMT / NeoPixel (WS2812)
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
The RMT event carries a 32-bit item encoded as follows:
2026-03-14 22:05:35 +07:00
```
bit31: level0 | bits[30:16]: duration0 | bit15: level1 | bits[14:0]: duration1
```
2026-03-16 23:23:31 +07:00
The `_RmtDecoder` accumulates bits and decodes WS2812 frames (24 bits per LED in GRB order):
2026-03-14 22:05:35 +07:00
```python
2026-03-16 23:23:31 +07:00
# Bit threshold: high pulse > 48 ticks (at 80 MHz APB = ~600 ns) → bit 1
2026-03-14 22:05:35 +07:00
_WS2812_HIGH_THRESHOLD = 48
# Bit 1: high ~64 ticks (800 ns), low ~36 ticks (450 ns)
# Bit 0: high ~32 ticks (400 ns), low ~68 ticks (850 ns)
```
2026-03-16 23:23:31 +07:00
The event emitted to the frontend:
2026-03-14 22:05:35 +07:00
```json
{
"type": "ws2812_update",
"data": {
"channel": 0,
2026-03-14 22:33:17 +07:00
"pixels": [[255, 0, 0], [0, 255, 0]]
2026-03-14 22:05:35 +07:00
}
}
```
---
2026-03-16 23:23:31 +07:00
## 9. LEDC / PWM and GPIO Mapping
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
### 9.1 Duty Cycle Polling
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
`qemu_picsimlab_get_internals(0)` returns a pointer to an array of 16 `uint32_t` values with the duty cycle of each LEDC channel (8 High-Speed channels + 8 Low-Speed). It is called periodically (every ~50 ms):
2026-03-14 22:05:35 +07:00
```python
await esp_lib_manager.poll_ledc(client_id)
2026-03-16 23:23:31 +07:00
# Emits: {"type": "ledc_update", "data": {"channel": 0, "duty": 4096, "duty_pct": 50.0, "gpio": 2}}
2026-03-14 22:05:35 +07:00
```
2026-03-16 23:23:31 +07:00
The typical maximum duty is 8192 (13-bit timer). For LED brightness: `duty_pct / 100` .
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**LEDC signal indices in the GPIO multiplexer:**
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
| LEDC Channel | Signal (signal index) |
2026-03-16 23:11:59 +07:00
|-------------|----------------------|
| HS ch 0-7 | 72-79 |
| LS ch 0-7 | 80-87 |
2026-03-16 23:23:31 +07:00
### 9.2 LEDC → GPIO Mapping (out_sel mechanism)
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
The original problem was that `ledc_update {channel: N}` arrived at the frontend but it was unknown which physical GPIO was controlled by that channel — that association is established dynamically in firmware via `ledcAttachPin(gpio, channel)` .
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Complete solution flow:**
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
1. **Firmware calls** `ledcAttachPin(gpio, ch)` — writes the LEDC channel signal index (72-87) into `GPIO_FUNCX_OUT_SEL_CFG_REG[gpio]` .
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
2. **QEMU detects** the write to the `out_sel` register and fires a sync event (`psync_irq_handler`). The modified code in `hw/gpio/esp32_gpio.c` encodes the signal index in bits 8-15 of the event:
2026-03-16 23:11:59 +07:00
```c
2026-03-16 23:23:31 +07:00
// Modification in esp32_gpio.c (psync_irq_handler function / out_sel write):
// BEFORE: only the GPIO number
2026-03-16 23:11:59 +07:00
qemu_set_irq(s->gpios_sync[0], (0x2000 | n));
2026-03-16 23:23:31 +07:00
// AFTER: GPIO in bits 7:0, signal index in bits 15:8
2026-03-16 23:11:59 +07:00
qemu_set_irq(s->gpios_sync[0], (0x2000 | ((value & 0xFF) < < 8 ) | ( n & 0xFF ) ) ) ;
```
2026-03-16 23:23:31 +07:00
3. **The Python worker** (`esp32_worker.py`) decodes the event in `_on_dir_change(slot=-1, direction)` :
2026-03-16 23:11:59 +07:00
```python
if slot == -1:
marker = direction & 0xF000
if marker == 0x2000: # GPIO_FUNCX_OUT_SEL_CFG change
gpio_pin = direction & 0xFF
signal = (direction >> 8) & 0xFF
if 72 < = signal < = 87:
2026-03-16 23:23:31 +07:00
ledc_ch = signal - 72 # channel 0-15
2026-03-16 23:11:59 +07:00
_ledc_gpio_map[ledc_ch] = gpio_pin
```
2026-03-16 23:23:31 +07:00
4. ** `ledc_update` includes `gpio` ** — polling includes the resolved `gpio` field:
2026-03-16 23:11:59 +07:00
```python
gpio = _ledc_gpio_map.get(ch, -1)
_emit({'type': 'ledc_update', 'channel': ch,
'duty': duty, 'duty_pct': round(duty / 8192 * 100, 1),
2026-03-16 23:23:31 +07:00
'gpio': gpio}) # -1 if ledcAttachPin has not been called yet
2026-03-16 23:11:59 +07:00
```
2026-03-16 23:23:31 +07:00
5. **The frontend store** (`useSimulatorStore.ts`) routes the PWM to the correct GPIO:
2026-03-16 23:11:59 +07:00
```typescript
bridge.onLedcUpdate = (update) => {
const targetPin = (update.gpio !== undefined & & update.gpio >= 0)
? update.gpio
2026-03-16 23:23:31 +07:00
: update.channel; // fallback: use channel number
2026-03-16 23:11:59 +07:00
boardPm.updatePwm(targetPin, update.duty_pct / 100);
};
```
2026-03-16 23:23:31 +07:00
6. ** `SimulatorCanvas` ** subscribes components to the PWM of the correct pin and adjusts the opacity of the visual element:
2026-03-16 23:11:59 +07:00
```typescript
const pwmUnsub = pinManager.onPwmChange(pin, (_p, duty) => {
const el = document.getElementById(component.id);
if (el) el.style.opacity = String(duty); // duty 0.0– 1.0
});
```
2026-03-14 22:05:35 +07:00
---
2026-03-16 23:23:31 +07:00
## 10. Building the Library Manually
2026-03-14 22:05:35 +07:00
2026-03-15 00:35:59 +07:00
### 10.1 Windows (MSYS2 MINGW64)
2026-03-16 23:23:31 +07:00
The `build_libqemu-esp32-win.sh` script in `wokwi-libs/qemu-lcgamboa/` automates the process:
2026-03-14 22:05:35 +07:00
```bash
2026-03-16 23:23:31 +07:00
# In MSYS2 MINGW64:
2026-03-14 22:05:35 +07:00
cd wokwi-libs/qemu-lcgamboa
2026-03-15 00:35:59 +07:00
bash build_libqemu-esp32-win.sh
2026-03-16 23:23:31 +07:00
# Produces: build/libqemu-xtensa.dll and build/libqemu-riscv32.dll
2026-03-14 22:05:35 +07:00
```
2026-03-16 23:23:31 +07:00
The script configures QEMU with `--extra-cflags=-fPIC` (required for Windows/PE with ASLR), compiles the full binary, and then relinks removing `softmmu_main.c.obj` (which contains `main()` ):
2026-03-14 22:05:35 +07:00
```bash
cc -m64 -mcx16 -shared \
-Wl,--export-all-symbols \
-Wl,--allow-multiple-definition \
-o libqemu-xtensa.dll \
2026-03-16 23:23:31 +07:00
@dll_link .rsp # all .obj files except softmmu_main
2026-03-14 22:05:35 +07:00
```
2026-03-15 00:35:59 +07:00
### 10.2 Linux
2026-03-16 23:23:31 +07:00
The `build_libqemu-esp32.sh` script produces a `.so` :
2026-03-15 00:35:59 +07:00
```bash
cd wokwi-libs/qemu-lcgamboa
bash build_libqemu-esp32.sh
2026-03-16 23:23:31 +07:00
# Produces: build/libqemu-xtensa.so and build/libqemu-riscv32.so
2026-03-15 00:35:59 +07:00
```
2026-03-16 23:23:31 +07:00
### 10.3 Verify Exports (Both Platforms)
2026-03-14 22:05:35 +07:00
```bash
2026-03-15 00:35:59 +07:00
# Linux:
nm -D build/libqemu-xtensa.so | grep -i "qemu_picsimlab\|qemu_init\|qemu_main"
# Windows:
objdump -p build/libqemu-xtensa.dll | grep -i "qemu_picsimlab\|qemu_init"
2026-03-16 23:23:31 +07:00
# Should show:
2026-03-15 00:35:59 +07:00
# qemu_init, qemu_main_loop, qemu_cleanup
2026-03-14 22:05:35 +07:00
# qemu_picsimlab_register_callbacks
2026-03-15 00:35:59 +07:00
# qemu_picsimlab_set_pin, qemu_picsimlab_set_apin
2026-03-14 22:05:35 +07:00
# qemu_picsimlab_uart_receive
2026-03-15 00:35:59 +07:00
# qemu_picsimlab_get_internals, qemu_picsimlab_get_TIOCM
2026-03-14 22:05:35 +07:00
```
2026-03-16 23:23:31 +07:00
### 10.4 Required Patch on Windows (symlink-install-tree.py)
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
Windows does not allow creating symlinks without administrator privileges. The QEMU script fails with `WinError 1314` . Applied patch:
2026-03-14 22:05:35 +07:00
```python
2026-03-16 23:23:31 +07:00
# In scripts/symlink-install-tree.py, inside the symlinks loop:
2026-03-14 22:05:35 +07:00
if os.name == 'nt':
if not os.path.exists(source):
continue
import shutil
try:
shutil.copy2(source, bundle_dest)
except Exception as copy_err:
print(f'error copying {source}: {copy_err}', file=sys.stderr)
continue
```
2026-03-16 23:23:31 +07:00
### 10.5 Incremental Rebuild (Single Modified File)
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
When only a single QEMU source file is modified (e.g. `esp32_gpio.c` ), there is no need to recompile the entire library — it is sufficient to compile the modified `.obj` and relink the DLL/SO.
2026-03-16 23:11:59 +07:00
**Windows (MSYS2 MINGW64):**
```bash
cd wokwi-libs/qemu-lcgamboa/build
2026-03-16 23:23:31 +07:00
# 1. Compile only the modified file:
2026-03-16 23:11:59 +07:00
ninja libcommon.fa.p/hw_gpio_esp32_gpio.c.obj
2026-03-16 23:23:31 +07:00
# 2. Relink the complete DLL using the response file (contains all .obj files and flags):
2026-03-16 23:11:59 +07:00
/c/msys64/mingw64/bin/gcc.exe @dll_link .rsp
2026-03-16 23:23:31 +07:00
# 3. Copy the new DLL to the backend:
2026-03-16 23:11:59 +07:00
cp libqemu-xtensa.dll ../../backend/app/services/
2026-03-16 23:23:31 +07:00
# Verify size (~43-44 MB):
2026-03-16 23:11:59 +07:00
ls -lh libqemu-xtensa.dll
```
2026-03-16 23:23:31 +07:00
> `dll_link.rsp` is generated by ninja during the first full build and contains the complete link command with all `.obj` files and MSYS2 libraries. It is the file that allows relinking without depending on the build system.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**What happens if ninja fails to compile the `.obj` ?**
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
Some files have dependencies on pre-generated headers (e.g. `version.h` , `windres` files, or `config-host.h` ). If ninja reports an error in a file that was NOT modified, compiling only the `.obj` of the file that was actually changed always works as long as a previous full build already exists.
2026-03-16 23:11:59 +07:00
**Linux:**
```bash
cd wokwi-libs/qemu-lcgamboa/build
2026-03-16 23:23:31 +07:00
# Compile only the modified .obj:
2026-03-16 23:11:59 +07:00
ninja libcommon.fa.p/hw_gpio_esp32_gpio.c.obj
2026-03-16 23:23:31 +07:00
# Relink the .so:
2026-03-16 23:11:59 +07:00
gcc -shared -o libqemu-xtensa.so @so_link .rsp
2026-03-16 23:23:31 +07:00
# Copy to the backend:
2026-03-16 23:11:59 +07:00
cp libqemu-xtensa.so ../../backend/app/services/
```
2026-03-14 22:05:35 +07:00
---
2026-03-15 00:35:59 +07:00
## 11. Tests
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
### 11.1 Main Test Suite (28 tests)
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
File: `test/esp32/test_esp32_lib_bridge.py`
2026-03-14 22:05:35 +07:00
```bash
2026-03-14 22:33:17 +07:00
python -m pytest test/esp32/test_esp32_lib_bridge.py -v
2026-03-16 23:23:31 +07:00
# Expected result: 28 passed in ~13 seconds
2026-03-14 22:05:35 +07:00
```
2026-03-16 23:23:31 +07:00
| Group | Tests | What it verifies |
|-------|-------|-----------------|
| `TestDllExists` | 5 | Lib paths, ROM binaries, platform dependencies |
| `TestDllLoads` | 3 | Lib loading, exported symbols |
| `TestPinmap` | 3 | Pinmap structure, GPIO2 at slot 3 |
2026-03-14 22:05:35 +07:00
| `TestManagerAvailability` | 2 | `is_available()` , API surface |
2026-03-16 23:23:31 +07:00
| `TestEsp32LibIntegration` | 15 | Real QEMU with blink firmware: boot, UART, GPIO, ADC, SPI, I2C |
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
### 11.2 Arduino ↔ ESP32 Integration Test (13 tests)
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
File: `test/esp32/test_arduino_esp32_integration.py`
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
Simulates full serial communication between an Arduino Uno (emulated in Python) and the ESP32 (QEMU lcgamboa). The "Arduino" sends `LED_ON` /`LED_OFF`/`PING` commands to the ESP32 and verifies responses + GPIO changes.
2026-03-14 22:33:17 +07:00
```bash
python -m pytest test/esp32/test_arduino_esp32_integration.py -v
2026-03-16 23:23:31 +07:00
# Expected result: 13 passed in ~30 seconds
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
| Test | What it verifies |
|------|----------------|
| `test_01_esp32_boots_ready` | ESP32 boots and sends "READY" over UART |
2026-03-14 22:33:17 +07:00
| `test_02_ping_pong` | Arduino→"PING", ESP32→"PONG" |
| `test_03_led_on_command` | LED_ON → GPIO2=HIGH + "OK:ON" |
| `test_04_led_off_command` | LED_OFF → GPIO2=LOW + "OK:OFF" |
2026-03-16 23:23:31 +07:00
| `test_05_toggle_five_times` | 5 ON/OFF cycles → ≥10 GPIO2 transitions |
| `test_06_gpio_sequence` | Correct sequence: ON→OFF→ON→OFF |
| `test_07_unknown_cmd_ignored` | Unknown command does not crash the ESP32 |
| `test_08_rapid_commands` | 20 commands in burst → all responses arrive |
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
**Test firmware:** `test/esp32-emulator/binaries_lcgamboa/serial_led.ino.merged.bin`
Source sketch: `test/esp32-emulator/sketches/serial_led/serial_led.ino`
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
### 11.3 Skip Integration Tests (Unit Tests Only)
2026-03-14 22:33:17 +07:00
```bash
SKIP_LIB_INTEGRATION=1 python -m pytest test/esp32/ -v
```
2026-03-16 23:23:31 +07:00
### 11.4 Recompile the Test Firmware
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
If you need to recompile the test binaries:
2026-03-14 22:33:17 +07:00
```bash
2026-03-16 23:23:31 +07:00
# Blink (IRAM-safe firmware for GPIO testing):
2026-03-14 22:33:17 +07:00
arduino-cli compile \
--fqbn esp32:esp32:esp32:FlashMode=dio \
--output-dir test/esp32-emulator/out_blink \
test/esp32-emulator/sketches/blink_lcgamboa
esptool --chip esp32 merge_bin --fill-flash-size 4MB \
-o test/esp32-emulator/binaries_lcgamboa/blink_lcgamboa.ino.merged.bin \
--flash_mode dio --flash_size 4MB \
0x1000 test/esp32-emulator/out_blink/blink_lcgamboa.ino.bootloader.bin \
0x8000 test/esp32-emulator/out_blink/blink_lcgamboa.ino.partitions.bin \
0x10000 test/esp32-emulator/out_blink/blink_lcgamboa.ino.bin
2026-03-16 23:23:31 +07:00
# Serial LED (firmware for Arduino↔ESP32 test):
2026-03-14 22:33:17 +07:00
arduino-cli compile \
--fqbn esp32:esp32:esp32:FlashMode=dio \
--output-dir test/esp32-emulator/out_serial_led \
test/esp32-emulator/sketches/serial_led
esptool --chip esp32 merge_bin --fill-flash-size 4MB \
-o test/esp32-emulator/binaries_lcgamboa/serial_led.ino.merged.bin \
--flash_mode dio --flash_size 4MB \
0x1000 test/esp32-emulator/out_serial_led/serial_led.ino.bootloader.bin \
0x8000 test/esp32-emulator/out_serial_led/serial_led.ino.partitions.bin \
0x10000 test/esp32-emulator/out_serial_led/serial_led.ino.bin
```
2026-03-14 22:05:35 +07:00
---
2026-03-16 23:23:31 +07:00
## 12. Frontend — Implemented Events
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
All backend events are wired to the frontend:
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
| Event | Component | Status |
|-------|-----------|--------|
| `gpio_change` | `PinManager.triggerPinChange()` → connected LEDs/components | ✅ Implemented |
| `ledc_update` | `PinManager.updatePwm(gpio, duty)` → CSS opacity of element connected to the GPIO | ✅ Implemented |
| `ws2812_update` | `NeoPixel.tsx` — RGB LED strip with canvas | ✅ Implemented |
| `gpio_dir` | Callback `onPinDir` in `Esp32Bridge.ts` | ✅ Implemented |
| `i2c_event` | Callback `onI2cEvent` in `Esp32Bridge.ts` | ✅ Implemented |
| `spi_event` | Callback `onSpiEvent` in `Esp32Bridge.ts` | ✅ Implemented |
| `system: crash` | Red banner in `SimulatorCanvas.tsx` with Dismiss button | ✅ Implemented |
| `system: reboot` | `onSystemEvent` in `Esp32Bridge.ts` | ✅ Implemented |
2026-03-14 22:33:17 +07:00
2026-03-16 23:23:31 +07:00
**Available send methods in `Esp32Bridge` (frontend → backend):**
2026-03-16 23:11:59 +07:00
2026-03-14 22:33:17 +07:00
```typescript
2026-03-16 23:23:31 +07:00
bridge.sendSerialBytes(bytes, uart?) // Send serial data to the ESP32
bridge.sendPinEvent(gpioPin, state) // Simulate external input on a GPIO (buttons)
bridge.setAdc(channel, millivolts) // Set ADC voltage (0-3300 mV)
bridge.setI2cResponse(addr, response) // I2C device response
bridge.setSpiResponse(response) // SPI device MISO byte
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:23:31 +07:00
**UI component interaction with the emulated ESP32:**
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
- **`wokwi-pushbutton`** (any GPIO) — `button-press` / `button-release` events → `sendPinEvent(gpio, true/false)`
- **`wokwi-potentiometer`** (SIG pin → ADC GPIO) — `input` event (0– 100) → `setAdc(chn, mV)`
- **`wokwi-led`** (GPIO with `ledcWrite` ) — receives `onPwmChange` → CSS opacity proportional to duty cycle
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
The connection logic lives in `SimulatorCanvas.tsx` : it detects the tag of the web component element connected to the ESP32, registers the appropriate listener, and translates events to the bridge protocol. See section 16 for more detail.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Using the NeoPixel component:**
2026-03-14 22:33:17 +07:00
```tsx
2026-03-16 23:23:31 +07:00
// The id must follow the pattern ws2812-{boardId}-{channel}
// so the store can send pixels to it via CustomEvent
2026-03-14 22:33:17 +07:00
< NeoPixel
id="ws2812-esp32-0"
count={8}
x={200}
y={300}
direction="horizontal"
/>
```
---
2026-03-16 23:23:31 +07:00
## 13. Known Limitations (Not Fixable Without Modifying QEMU)
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
| Limitation | Cause | Workaround |
2026-03-14 22:05:35 +07:00
|------------|-------|------------|
2026-03-16 23:23:31 +07:00
| **Single ESP32 instance per process** | QEMU uses global state in static variables | Launch multiple Python processes |
| **WiFi only with hardcoded SSIDs** | lcgamboa hardcodes "PICSimLabWifi" and "Espressif" in C | Modify and recompile the lib |
| **No BLE / Classic Bluetooth** | Not implemented in lcgamboa | Not available |
| **No capacitive touch** | `touchRead()` has no callback in picsimlab | Not available |
| **No DAC** | GPIO25/GPIO26 analog output not exposed by picsimlab | Not available |
| **Fixed flash at 4MB** | Hardcoded in the esp32-picsimlab machine | Recompile the lib |
| **arduino-esp32 3.x causes crash** | IDF 5.x handles cache differently from the emulated WiFi | Use 2.x (IDF 4.4.x) |
| **ADC only on pins defined in `ESP32_ADC_PIN_MAP`** | The GPIO→ADC channel mapping is static in the frontend | Update `ESP32_ADC_PIN_MAP` in `Esp32Element.ts` |
2026-03-14 22:05:35 +07:00
---
2026-03-16 23:23:31 +07:00
## 14. Environment Variables
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
| Variable | Example Value | Effect |
|----------|--------------|--------|
| `QEMU_ESP32_LIB` | `/app/lib/libqemu-xtensa.so` | Force lib path (overrides auto-detect) |
| `QEMU_ESP32_BINARY` | `/usr/bin/qemu-system-xtensa` | Subprocess fallback (without lib) |
| `SKIP_LIB_INTEGRATION` | `1` | Skip QEMU integration tests in pytest |
2026-03-14 22:05:35 +07:00
2026-03-16 23:23:31 +07:00
**Auto-detection by platform:**
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
| Platform | Library auto-searched |
|----------|-----------------------|
2026-03-15 00:35:59 +07:00
| Docker / Linux | `/app/lib/libqemu-xtensa.so` (via `QEMU_ESP32_LIB` ) |
| Windows | `backend/app/services/libqemu-xtensa.dll` |
2026-03-16 23:23:31 +07:00
| Custom | `$QEMU_ESP32_LIB` (if set, takes priority) |
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
**Startup examples:**
2026-03-14 22:33:17 +07:00
```bash
2026-03-16 23:23:31 +07:00
# Docker — fully automatic, no extra variables needed:
docker run -d -p 3080:80 -e SECRET_KEY=secret ghcr.io/davidmonterocrespo24/velxio:master
2026-03-15 00:35:59 +07:00
2026-03-16 23:23:31 +07:00
# Windows with lib (full emulation: GPIO + WiFi + ADC + I2C + SPI + RMT + LEDC):
2026-03-14 22:33:17 +07:00
cd backend && venv\Scripts\activate
uvicorn app.main:app --reload --port 8001
2026-03-16 23:23:31 +07:00
# Linux with lib at custom path:
2026-03-15 00:35:59 +07:00
QEMU_ESP32_LIB=/opt/velxio/libqemu-xtensa.so uvicorn app.main:app --port 8001
2026-03-16 23:23:31 +07:00
# Without lib (fallback: UART serial only via QEMU subprocess):
2026-03-15 00:35:59 +07:00
QEMU_ESP32_BINARY=/usr/bin/qemu-system-xtensa uvicorn app.main:app --port 8001
2026-03-14 22:33:17 +07:00
```
2026-03-16 23:11:59 +07:00
---
2026-03-16 23:23:31 +07:00
## 15. GPIO Banks — GPIO32-39 Fix
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
### 15.1 The Problem
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
The ESP32 divides its GPIOs into two register banks:
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
| Bank | GPIOs | Output register | Address |
|--------|------------|-----------------|--------------|
| Bank 0 | GPIO 0-31 | `GPIO_OUT_REG` | `0x3FF44004` |
| Bank 1 | GPIO 32-39 | `GPIO_OUT1_REG` | `0x3FF44010` |
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
Before the fix, the frontend only monitored `GPIO_OUT_REG` (bank 0). When firmware called `digitalWrite(32, HIGH)` or used GPIO32-39 for any function, QEMU updated `GPIO_OUT1_REG` but the `gpio_change` event never reached the frontend, and components connected to those pins did not respond.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
### 15.2 The Fix
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
The backend (`esp32_worker.py`) was already correctly receiving GPIO32-39 changes through the `picsimlab_write_pin` callback — QEMU calls this callback for all GPIOs regardless of bank. The fix was to ensure the pinmap includes slots 33-40 (GPIOs 32-39):
2026-03-16 23:11:59 +07:00
```python
2026-03-16 23:23:31 +07:00
# Identity mapping: slot i → GPIO i-1 (for all 40 GPIOs of the ESP32)
2026-03-16 23:11:59 +07:00
_PINMAP = (ctypes.c_int16 * 41)(
2026-03-16 23:23:31 +07:00
40, # pinmap[0] = GPIO count
2026-03-16 23:11:59 +07:00
*range(40) # pinmap[1..40] = GPIO 0..39
)
```
2026-03-16 23:23:31 +07:00
With this complete pinmap, `picsimlab_write_pin(slot=33, value=1)` is correctly translated to `gpio_change {pin: 32, state: 1}` and reaches the frontend.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
### 15.3 Verification
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
The ** "ESP32: 7-Segment Counter"** example uses GPIO32 for the G segment of the display:
2026-03-16 23:11:59 +07:00
```cpp
2026-03-16 23:23:31 +07:00
// Segments: a=12, b=13, c=14, d=25, e=26, f=27, g=32
2026-03-16 23:11:59 +07:00
const int SEG[7] = {12, 13, 14, 25, 26, 27, 32};
```
2026-03-16 23:23:31 +07:00
If the 0-9 counter displays all segments correctly (including the G segment on the digits that require it), GPIO32-39 is working.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**GPIOs 34-39 are input-only** on the ESP32-WROOM-32 — they have no output driver. The pinmap includes them so they work as inputs (ADC, buttons), but `digitalWrite()` on them has no real effect on hardware.
2026-03-16 23:11:59 +07:00
---
2026-03-16 23:23:31 +07:00
## 16. UI Interaction — ADC, Buttons, and Visual PWM
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
This section documents the three bidirectional interaction capabilities added between canvas visual components and the emulated ESP32.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
### 16.1 ADC — Potentiometer → `analogRead()`
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Goal:** When the user moves a `wokwi-potentiometer` connected to an ESP32 ADC pin, the value read by `analogRead()` in the firmware should change.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Flow:**
2026-03-16 23:11:59 +07:00
```text
2026-03-16 23:23:31 +07:00
User moves potentiometer (0-100%)
→ DOM 'input' event on < wokwi-potentiometer >
2026-03-16 23:11:59 +07:00
→ SimulatorCanvas.tsx: onInput handler
→ ESP32_ADC_PIN_MAP[gpioPin] → { adc, ch, chn }
→ bridge.setAdc(chn, mV) // mV = pct/100 * 3300
→ WebSocket: {type: "esp32_adc_set", data: {channel: chn, millivolts: mV}}
→ Backend: esp_lib_manager.set_adc(client_id, chn, mV)
→ lib.qemu_picsimlab_set_apin(chn, raw) // raw = mV * 4095 / 3300
2026-03-16 23:23:31 +07:00
→ analogRead() in firmware returns raw (0-4095)
2026-03-16 23:11:59 +07:00
```
2026-03-16 23:23:31 +07:00
**ADC pin map** (`frontend/src/components/components-wokwi/Esp32Element.ts`):
2026-03-16 23:11:59 +07:00
```typescript
export const ESP32_ADC_PIN_MAP: Record< number , { adc: 1 | 2 ; ch: number ; chn: number } > = {
2026-03-16 23:23:31 +07:00
// ADC1 (input-only or input/output GPIOs):
2026-03-16 23:11:59 +07:00
36: { adc: 1, ch: 0, chn: 0 }, // VP
37: { adc: 1, ch: 1, chn: 1 },
38: { adc: 1, ch: 2, chn: 2 },
39: { adc: 1, ch: 3, chn: 3 }, // VN
32: { adc: 1, ch: 4, chn: 4 },
33: { adc: 1, ch: 5, chn: 5 },
34: { adc: 1, ch: 6, chn: 6 },
35: { adc: 1, ch: 7, chn: 7 },
2026-03-16 23:23:31 +07:00
// ADC2 (shared with WiFi — do not use when WiFi is active):
2026-03-16 23:11:59 +07:00
4: { adc: 2, ch: 0, chn: 8 },
0: { adc: 2, ch: 1, chn: 9 },
2: { adc: 2, ch: 2, chn: 10 },
15: { adc: 2, ch: 3, chn: 11 },
13: { adc: 2, ch: 4, chn: 12 },
12: { adc: 2, ch: 5, chn: 13 },
14: { adc: 2, ch: 6, chn: 14 },
27: { adc: 2, ch: 7, chn: 15 },
25: { adc: 2, ch: 8, chn: 16 },
26: { adc: 2, ch: 9, chn: 17 },
};
```
2026-03-16 23:23:31 +07:00
**Activation condition:** the wire must connect the `SIG` pin of the potentiometer to the ADC GPIO of the ESP32. The `VCC` and `GND` pins are ignored for ADC.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
### 16.2 GPIO Input — Button → ESP32 Interrupt
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Goal:** When the user presses/releases a `wokwi-pushbutton` connected to an ESP32 GPIO, the firmware should see the logic level change (works with `digitalRead()` , `attachInterrupt()` , etc.).
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Flow:**
2026-03-16 23:11:59 +07:00
```text
2026-03-16 23:23:31 +07:00
User clicks < wokwi-pushbutton >
→ DOM 'button-press' or 'button-release' event
2026-03-16 23:11:59 +07:00
→ SimulatorCanvas.tsx: onPress/onRelease handler
→ bridge.sendPinEvent(gpioPin, true/false)
→ WebSocket: {type: "esp32_gpio_in", data: {pin: gpioPin, state: 1/0}}
→ Backend: esp_lib_manager.set_pin_state(client_id, gpioPin, value)
→ lib.qemu_picsimlab_set_pin(slot, value) // slot = gpioPin + 1
2026-03-16 23:23:31 +07:00
→ ESP32 sees the change in the GPIO_IN_REG register
→ digitalRead(gpioPin) returns the new value
→ attachInterrupt() fires if it was configured
2026-03-16 23:11:59 +07:00
```
2026-03-16 23:23:31 +07:00
**Detection logic in SimulatorCanvas** (effect that runs when `components` or `wires` change):
2026-03-16 23:11:59 +07:00
```typescript
2026-03-16 23:23:31 +07:00
// For each non-ESP32 component:
// 1. Find wires that connect this component to an ESP32 pin
// 2. Resolve the GPIO number from the ESP32 endpoint (boardPinToNumber)
// 3. If the element is wokwi-pushbutton → register button-press/release
// 4. If the element is wokwi-potentiometer (SIG pin) → register ADC input
2026-03-16 23:11:59 +07:00
```
2026-03-16 23:23:31 +07:00
> The effect uses `setTimeout(300ms)` to wait for the DOM to render the web components before calling `getElementById` and `addEventListener`.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
### 16.3 Visual PWM — `ledcWrite()` → LED Brightness
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Goal:** When the firmware uses `ledcWrite(channel, duty)` , the LED connected to the GPIO controlled by that channel should display brightness proportional to the duty cycle.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**The mapping problem:** QEMU knows the duty of each LEDC channel, but not which GPIO uses it — that association is established with `ledcAttachPin(gpio, ch)` which writes to `GPIO_FUNCX_OUT_SEL_CFG_REG` . See section 9.2 for the complete mechanism.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Visual flow:**
2026-03-16 23:11:59 +07:00
```text
2026-03-16 23:23:31 +07:00
ledcWrite(ch, duty) in firmware
→ QEMU updates duty in internal LEDC array
→ poll_ledc() reads the array every ~50ms
→ ledc_update {channel, duty, duty_pct, gpio} sent to frontend
2026-03-16 23:11:59 +07:00
→ useSimulatorStore: bridge.onLedcUpdate → pinManager.updatePwm(gpio, duty/100)
2026-03-16 23:23:31 +07:00
→ PinManager fires callbacks registered for that pin
2026-03-16 23:11:59 +07:00
→ SimulatorCanvas: onPwmChange → el.style.opacity = String(duty)
2026-03-16 23:23:31 +07:00
→ The visual element (wokwi-led) shows proportional brightness
2026-03-16 23:11:59 +07:00
```
2026-03-16 23:23:31 +07:00
**Value ranges:**
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
- `duty` raw: 0– 8191 (13-bit timer, the most common on ESP32)
- `duty_pct` : 0.0– 100.0 (calculated as `duty / 8192 * 100` )
- CSS `opacity` : 0.0– 1.0 (= `duty_pct / 100` )
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Compatible sketch example:**
2026-03-16 23:11:59 +07:00
```cpp
const int LED_PIN = 2;
const int LEDC_CH = 0;
const int FREQ = 5000;
const int BITS = 13;
void setup() {
ledcSetup(LEDC_CH, FREQ, BITS);
ledcAttachPin(LED_PIN, LEDC_CH);
}
void loop() {
for (int duty = 0; duty < 8192 ; duty + = 100 ) {
2026-03-16 23:23:31 +07:00
ledcWrite(LEDC_CH, duty); // the LED on GPIO2 gradually brightens
2026-03-16 23:11:59 +07:00
delay(10);
}
}
```
---
2026-03-16 23:23:31 +07:00
## 17. lcgamboa Fork Modifications — Incremental Rebuild
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
This section documents all modifications made to the [lcgamboa/qemu ](https://github.com/lcgamboa/qemu ) fork for Velxio, and how to recompile only the modified files.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
### 17.1 Modified File: `hw/gpio/esp32_gpio.c`
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Logical commit:** Encode the LEDC signal index in the out_sel sync event.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Problem:** When firmware calls `ledcAttachPin(gpio, ch)` , QEMU writes the signal index (72-87) to `GPIO_FUNCX_OUT_SEL_CFG_REG[gpio]` . The sync event fired toward the backend only included the GPIO number — the signal index (and therefore the LEDC channel) was lost.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
**Change:**
2026-03-16 23:11:59 +07:00
```c
2026-03-16 23:23:31 +07:00
// File: hw/gpio/esp32_gpio.c
// Function: psync_irq_handler (or equivalent that handles out_sel writes)
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
// BEFORE (only GPIO number in bits 12:0):
2026-03-16 23:11:59 +07:00
qemu_set_irq(s->gpios_sync[0], (0x2000 | n));
2026-03-16 23:23:31 +07:00
// AFTER (GPIO in bits 7:0, signal index in bits 15:8):
2026-03-16 23:11:59 +07:00
qemu_set_irq(s->gpios_sync[0], (0x2000 | ((value & 0xFF) < < 8 ) | ( n & 0xFF ) ) ) ;
```
2026-03-16 23:23:31 +07:00
The `0x2000` marker in bits [13:12] identifies this event type in the backend. The backend (`esp32_worker.py`) decodes:
2026-03-16 23:11:59 +07:00
```python
marker = direction & 0xF000 # → 0x2000
gpio_pin = direction & 0xFF # bits 7:0
2026-03-16 23:23:31 +07:00
signal = (direction >> 8) & 0xFF # bits 15:8 → LEDC signal index
2026-03-16 23:11:59 +07:00
```
2026-03-16 23:23:31 +07:00
### 17.2 How to Recompile After Modifying `esp32_gpio.c`
2026-03-16 23:11:59 +07:00
```bash
2026-03-16 23:23:31 +07:00
# In MSYS2 MINGW64 (Windows):
2026-03-16 23:11:59 +07:00
cd /e/Hardware/wokwi_clon/wokwi-libs/qemu-lcgamboa/build
2026-03-16 23:23:31 +07:00
# Step 1: Compile only the modified .obj
2026-03-16 23:11:59 +07:00
ninja libcommon.fa.p/hw_gpio_esp32_gpio.c.obj
2026-03-16 23:23:31 +07:00
# Step 2: Relink the complete DLL
2026-03-16 23:11:59 +07:00
/c/msys64/mingw64/bin/gcc.exe @dll_link .rsp
2026-03-16 23:23:31 +07:00
# Step 3: Deploy to the backend
2026-03-16 23:11:59 +07:00
cp libqemu-xtensa.dll /e/Hardware/wokwi_clon/backend/app/services/
2026-03-16 23:23:31 +07:00
# Verify:
2026-03-16 23:11:59 +07:00
ls -lh libqemu-xtensa.dll
2026-03-16 23:23:31 +07:00
# → approx 43-44 MB
2026-03-16 23:11:59 +07:00
```
2026-03-16 23:23:31 +07:00
**Compilation time:** ~10 seconds (vs 15-30 minutes for a full build).
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
### 17.3 Why the Full Build May Fail on Windows
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
The first full build (`bash build_libqemu-esp32-win.sh`) may fail with errors in unmodified files:
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
- **`windres: version.rc: No such file`** — Generated dynamically by meson; only occurs in clean builds. Run the script once from scratch.
- **`gcrypt.h: No such file`** — MSYS2 package not installed. Fix: `pacman -S mingw-w64-x86_64-libgcrypt`
- **`zlib.h: No such file`** — MSYS2 package not installed. Fix: `pacman -S mingw-w64-x86_64-zlib`
- **`WinError 1314`** in `symlink-install-tree.py` — Windows does not allow symlinks without admin. See patch in section 10.4.
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
Once there is a successful full build (the `.dll` exists in `build/` ), the incremental rebuild always works — just `ninja <file.obj>` + `gcc @dll_link.rsp` .
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
### 17.4 Summary of All Modified Files in the Fork
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
- **`hw/gpio/esp32_gpio.c`** — Encode signal index in out_sel event (§17.1)
- **`scripts/symlink-install-tree.py`** — Use `shutil.copy2` instead of `os.symlink` on Windows (§10.4)
2026-03-16 23:11:59 +07:00
2026-03-16 23:23:31 +07:00
All other files in the fork are identical to the lcgamboa upstream. No files were modified in the `esp32-picsimlab` machine, the Xtensa core, or the ADC/UART/I2C/SPI/RMT peripherals.