From 20ccd87d1b2ec7c25ae08fe2c8a2194e6aa9648d Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Mon, 16 Mar 2026 13:11:59 -0300 Subject: [PATCH] feat: enhance ESP32 emulation with GPIO mapping and ADC support --- backend/app/services/esp32_worker.py | 20 +- docs/ESP32_EMULATION.md | 379 +++++++++++++++++- .../components-wokwi/Esp32Element.ts | 25 ++ .../components/simulator/SimulatorCanvas.tsx | 74 +++- frontend/src/data/examples.ts | 8 +- frontend/src/simulation/Esp32Bridge.ts | 2 +- frontend/src/store/useSimulatorStore.ts | 12 +- 7 files changed, 501 insertions(+), 19 deletions(-) diff --git a/backend/app/services/esp32_worker.py b/backend/app/services/esp32_worker.py index fe69dd5..dd2eed1 100644 --- a/backend/app/services/esp32_worker.py +++ b/backend/app/services/esp32_worker.py @@ -24,7 +24,7 @@ stdout : JSON event lines (one per line, flushed immediately) {"type": "gpio_change", "pin": N, "state": V} {"type": "gpio_dir", "pin": N, "dir": V} {"type": "uart_tx", "uart": N, "byte": V} - {"type": "ledc_update", "channel": N, "duty": V, "duty_pct": F} + {"type": "ledc_update", "channel": N, "duty": V, "duty_pct": F, "gpio": N|-1} {"type": "rmt_event", "channel": N, ...} {"type": "ws2812_update","channel": N, "pixels": [...]} {"type": "i2c_event", "bus": N, "addr": N, "event": N, "response": N} @@ -208,6 +208,9 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) _crashed = [False] _CRASH_STR = b'Cache disabled but cached memory region accessed' _REBOOT_STR = b'Rebooting...' + # LEDC channel → GPIO pin (populated from GPIO out_sel sync events) + # ESP32 signal indices: 72-79 = LEDC HS ch 0-7, 80-87 = LEDC LS ch 0-7 + _ledc_gpio_map: dict[int, int] = {} # ── 5. ctypes callbacks (called from QEMU thread) ───────────────────────── @@ -220,6 +223,17 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) def _on_dir_change(slot: int, direction: int) -> None: if _stopped.is_set(): return + # slot == -1 means a sync event from GPIO/LEDC/IOMUX peripheral + if slot == -1: + marker = direction & 0xF000 + if marker == 0x2000: # GPIO_FUNCX_OUT_SEL_CFG change + gpio_pin = direction & 0xFF + signal = (direction >> 8) & 0xFF + # Signal 72-79 = LEDC HS ch 0-7; 80-87 = LEDC LS ch 0-7 + if 72 <= signal <= 87: + ledc_ch = signal - 72 # ch 0-15 + _ledc_gpio_map[ledc_ch] = gpio_pin + return gpio = int(_PINMAP[slot]) if 1 <= slot <= _GPIO_COUNT else slot _emit({'type': 'gpio_dir', 'pin': gpio, 'dir': direction}) @@ -317,9 +331,11 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) for ch in range(16): duty = int(arr[ch]) if duty > 0: + gpio = _ledc_gpio_map.get(ch, -1) _emit({'type': 'ledc_update', 'channel': ch, 'duty': duty, - 'duty_pct': round(duty / 8192 * 100, 1)}) + 'duty_pct': round(duty / 8192 * 100, 1), + 'gpio': gpio}) except Exception: pass diff --git a/docs/ESP32_EMULATION.md b/docs/ESP32_EMULATION.md index 97749e4..a508e55 100644 --- a/docs/ESP32_EMULATION.md +++ b/docs/ESP32_EMULATION.md @@ -19,12 +19,15 @@ 6. [WiFi emulada](#6-wifi-emulada) 7. [I2C emulado](#7-i2c-emulado) 8. [RMT / NeoPixel (WS2812)](#8-rmt--neopixel-ws2812) -9. [LEDC / PWM](#9-ledc--pwm) +9. [LEDC / PWM y mapeo GPIO](#9-ledc--pwm-y-mapeo-gpio) 10. [Compilar la librería manualmente](#10-compilar-la-librería-manualmente) 11. [Tests](#11-tests) 12. [Frontend — Eventos implementados](#12-frontend--eventos-implementados) 13. [Limitaciones conocidas](#13-limitaciones-conocidas) 14. [Variables de entorno](#14-variables-de-entorno) +15. [GPIO Banks — Corrección GPIO32-39](#15-gpio-banks--corrección-gpio32-39) +16. [Interacción UI — ADC, Botones y PWM Visual](#16-interacción-ui--adc-botones-y-pwm-visual) +17. [Modificaciones al fork lcgamboa — Rebuild incremental](#17-modificaciones-al-fork-lcgamboa--rebuild-incremental) --- @@ -485,7 +488,7 @@ Convierte callbacks de hardware en **eventos WebSocket** para el frontend: | `spi_event` | `{bus, event, response}` | Transacción SPI | | `rmt_event` | `{channel, config0, value, level0, dur0, level1, dur1}` | Pulso RMT | | `ws2812_update` | `{channel, pixels: [[r,g,b],...]}` | Frame NeoPixel completo | -| `ledc_update` | `{channel, duty, duty_pct}` | PWM duty cycle | +| `ledc_update` | `{channel, duty, duty_pct, gpio}` | PWM duty cycle + GPIO que maneja ese canal | | `error` | `{message: str}` | Error de boot | **Detección de crash y reboot:** @@ -695,17 +698,81 @@ El evento emitido al frontend: --- -## 9. LEDC / PWM +## 9. LEDC / PWM y mapeo GPIO -`qemu_picsimlab_get_internals(0)` retorna un puntero a un array de 16 `uint32_t` con el duty cycle de cada canal LEDC. Llamar periódicamente (cada ~50 ms): +### 9.1 Polling de duty cycle + +`qemu_picsimlab_get_internals(0)` retorna un puntero a un array de 16 `uint32_t` con el duty cycle de cada canal LEDC (8 canales High-Speed + 8 Low-Speed). Se llama periódicamente (cada ~50 ms): ```python await esp_lib_manager.poll_ledc(client_id) -# Emite: {"type": "ledc_update", "data": {"channel": 0, "duty": 4096, "duty_pct": 50.0}} +# Emite: {"type": "ledc_update", "data": {"channel": 0, "duty": 4096, "duty_pct": 50.0, "gpio": 2}} ``` El duty máximo típico es 8192 (timer de 13 bits). Para brillo de LED: `duty_pct / 100`. +**Índices de señal LEDC en el multiplexor GPIO:** + +| Canal LEDC | Señal (signal index) | +|-------------|----------------------| +| HS ch 0-7 | 72-79 | +| LS ch 0-7 | 80-87 | + +### 9.2 Mapeo LEDC → GPIO (mecanismo out_sel) + +El problema original era que `ledc_update {channel: N}` llegaba al frontend pero no se sabía qué GPIO físico estaba controlado por ese canal — esa asociación se establece dinámicamente en firmware mediante `ledcAttachPin(gpio, channel)`. + +**Flujo completo de la solución:** + +1. **Firmware llama** `ledcAttachPin(gpio, ch)` — escribe en `GPIO_FUNCX_OUT_SEL_CFG_REG[gpio]` el índice de señal del canal LEDC (72-87). + +2. **QEMU detecta** la escritura en el registro `out_sel` y dispara un evento de sincronización (`psync_irq_handler`). El código modificado en `hw/gpio/esp32_gpio.c` codifica el índice de señal en los bits 8-15 del evento: + ```c + // Modificación en esp32_gpio.c (función psync_irq_handler / out_sel write): + // ANTES: solo el número de GPIO + qemu_set_irq(s->gpios_sync[0], (0x2000 | n)); + // DESPUÉS: GPIO en bits 7:0, signal index en bits 15:8 + qemu_set_irq(s->gpios_sync[0], (0x2000 | ((value & 0xFF) << 8) | (n & 0xFF))); + ``` + +3. **El worker Python** (`esp32_worker.py`) decodifica el evento en `_on_dir_change(slot=-1, direction)`: + ```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: + ledc_ch = signal - 72 # canal 0-15 + _ledc_gpio_map[ledc_ch] = gpio_pin + ``` + +4. **`ledc_update` incluye `gpio`** — el polling incluye el campo `gpio` resuelto: + ```python + gpio = _ledc_gpio_map.get(ch, -1) + _emit({'type': 'ledc_update', 'channel': ch, + 'duty': duty, 'duty_pct': round(duty / 8192 * 100, 1), + 'gpio': gpio}) # -1 si aún no se ha llamado ledcAttachPin + ``` + +5. **El store del frontend** (`useSimulatorStore.ts`) ruteará el PWM al GPIO correcto: + ```typescript + bridge.onLedcUpdate = (update) => { + const targetPin = (update.gpio !== undefined && update.gpio >= 0) + ? update.gpio + : update.channel; // fallback: usar número de canal + boardPm.updatePwm(targetPin, update.duty_pct / 100); + }; + ``` + +6. **`SimulatorCanvas`** suscribe los componentes al PWM del pin correcto y ajusta la opacidad del elemento visual: + ```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 + }); + ``` + --- ## 10. Compilar la librería manualmente @@ -775,6 +842,49 @@ if os.name == 'nt': continue ``` +### 10.5 Rebuild incremental (solo un archivo modificado) + +Cuando se modifica un único archivo fuente de QEMU (p.ej. `esp32_gpio.c`) no hace falta recompilar toda la librería — basta con compilar el `.obj` modificado y relincar la DLL/SO. + +**Windows (MSYS2 MINGW64):** + +```bash +cd wokwi-libs/qemu-lcgamboa/build + +# 1. Compilar solo el archivo modificado: +ninja libcommon.fa.p/hw_gpio_esp32_gpio.c.obj + +# 2. Relincar la DLL completa usando el response file (tiene todos los .obj y flags): +/c/msys64/mingw64/bin/gcc.exe @dll_link.rsp + +# 3. Copiar la DLL nueva al backend: +cp libqemu-xtensa.dll ../../backend/app/services/ + +# Verificar tamaño (~43-44 MB): +ls -lh libqemu-xtensa.dll +``` + +> `dll_link.rsp` es generado por ninja en el primer build completo y contiene el comando completo de linkado con todos los `.obj` y librerías de MSYS2. Es el archivo que permite relincar sin depender del sistema de build. + +**¿Qué pasa si ninja falla al compilar el `.obj`?** + +Algunos archivos tienen dependencias de headers pre-generados (p.ej. `version.h`, archivos de `windres`, o `config-host.h`). Si ninja reporta error en un archivo que NO se modificó, compilar solo el `.obj` del archivo que sí se cambió funciona siempre que ya exista un build completo previo. + +**Linux:** + +```bash +cd wokwi-libs/qemu-lcgamboa/build + +# Compilar solo el .obj modificado: +ninja libcommon.fa.p/hw_gpio_esp32_gpio.c.obj + +# Relincar la .so: +gcc -shared -o libqemu-xtensa.so @so_link.rsp + +# Copiar al backend: +cp libqemu-xtensa.so ../../backend/app/services/ +``` + --- ## 11. Tests @@ -868,7 +978,7 @@ Todos los eventos del backend están conectados al frontend: | Evento | Componente | Estado | |--------|-----------|--------| | `gpio_change` | `PinManager.triggerPinChange()` → LEDs/componentes conectados | ✅ Implementado | -| `ledc_update` | `PinManager.updatePwm()` → brillo variable en `LED.tsx` | ✅ Implementado | +| `ledc_update` | `PinManager.updatePwm(gpio, duty)` → opacidad CSS de elemento conectado al GPIO | ✅ Implementado | | `ws2812_update` | `NeoPixel.tsx` — strip de LEDs RGB con canvas | ✅ Implementado | | `gpio_dir` | Callback `onPinDir` en `Esp32Bridge.ts` | ✅ Implementado | | `i2c_event` | Callback `onI2cEvent` en `Esp32Bridge.ts` | ✅ Implementado | @@ -877,14 +987,23 @@ Todos los eventos del backend están conectados al frontend: | `system: reboot` | `onSystemEvent` en `Esp32Bridge.ts` | ✅ Implementado | **Métodos de envío disponibles en `Esp32Bridge` (frontend → backend):** + ```typescript bridge.sendSerialBytes(bytes, uart?) // Enviar datos serial al ESP32 -bridge.sendPinEvent(gpioPin, state) // Simular input externo en un GPIO +bridge.sendPinEvent(gpioPin, state) // Simular input externo en un GPIO (botones) bridge.setAdc(channel, millivolts) // Setear voltaje ADC (0-3300 mV) bridge.setI2cResponse(addr, response) // Respuesta de dispositivo I2C bridge.setSpiResponse(response) // Byte MISO de dispositivo SPI ``` +**Interacción de componentes UI con el ESP32 emulado:** + +- **`wokwi-pushbutton`** (cualquier GPIO) — eventos `button-press` / `button-release` → `sendPinEvent(gpio, true/false)` +- **`wokwi-potentiometer`** (pin SIG → ADC GPIO) — evento `input` (0–100) → `setAdc(chn, mV)` +- **`wokwi-led`** (GPIO con `ledcWrite`) — recibe `onPwmChange` → opacidad CSS proporcional al duty cycle + +La lógica de conexión vive en `SimulatorCanvas.tsx`: detecta el tag del elemento web component conectado al ESP32, registra el listener apropiado y traduce los eventos al protocolo del bridge. Ver sección 16 para más detalle. + **Uso del componente NeoPixel:** ```tsx // El id debe seguir el patrón ws2812-{boardId}-{channel} @@ -911,6 +1030,7 @@ bridge.setSpiResponse(response) // Byte MISO de dispositivo SPI | **Sin DAC** | GPIO25/GPIO26 analógico no expuesto por picsimlab | No disponible | | **Flash fija en 4MB** | Hardcoded en la machine esp32-picsimlab | Recompilar lib | | **arduino-esp32 3.x causa crash** | IDF 5.x maneja caché diferente al WiFi emulado | Usar 2.x (IDF 4.4.x) | +| **ADC solo en pines definidos en `ESP32_ADC_PIN_MAP`** | El mapeo GPIO→canal ADC es estático en frontend | Actualizar `ESP32_ADC_PIN_MAP` en `Esp32Element.ts` | --- @@ -946,3 +1066,248 @@ QEMU_ESP32_LIB=/opt/velxio/libqemu-xtensa.so uvicorn app.main:app --port 8001 # Sin lib (fallback: solo UART serial via subprocess QEMU): QEMU_ESP32_BINARY=/usr/bin/qemu-system-xtensa uvicorn app.main:app --port 8001 ``` + +--- + +## 15. GPIO Banks — Corrección GPIO32-39 + +### 15.1 El problema + +El ESP32 divide sus GPIOs en dos bancos de registros: + +| Banco | GPIOs | Registro de output | Dirección | +|---------|------------|--------------------|--------------| +| Banco 0 | GPIO 0-31 | `GPIO_OUT_REG` | `0x3FF44004` | +| Banco 1 | GPIO 32-39 | `GPIO_OUT1_REG` | `0x3FF44010` | + +Antes de la corrección, el frontend solo monitorizaba `GPIO_OUT_REG` (banco 0). Cuando el firmware hacía `digitalWrite(32, HIGH)` o usaba GPIO32-39 para cualquier función, QEMU actualizaba `GPIO_OUT1_REG` pero el evento `gpio_change` nunca llegaba al frontend, y los componentes conectados a esos pines no respondían. + +### 15.2 La corrección + +El backend (`esp32_worker.py`) ya recibía correctamente los cambios de GPIO32-39 a través del callback `picsimlab_write_pin` — QEMU llama este callback para todos los GPIOs independientemente del banco. La corrección fue asegurarse de que el pinmap incluye los slots 33-40 (GPIOs 32-39): + +```python +# Identity mapping: slot i → GPIO i-1 (para los 40 GPIOs del ESP32) +_PINMAP = (ctypes.c_int16 * 41)( + 40, # pinmap[0] = count de GPIOs + *range(40) # pinmap[1..40] = GPIO 0..39 +) +``` + +Con este pinmap completo, `picsimlab_write_pin(slot=33, value=1)` es correctamente traducido a `gpio_change {pin: 32, state: 1}` y llega al frontend. + +### 15.3 Verificación + +El ejemplo **"ESP32: 7-Segment Counter"** usa GPIO32 para el segmento G del display: + +```cpp +// Segmentos: a=12, b=13, c=14, d=25, e=26, f=27, g=32 +const int SEG[7] = {12, 13, 14, 25, 26, 27, 32}; +``` + +Si el contador 0-9 muestra todos los segmentos correctamente (incluyendo el segmento G en los dígitos que lo requieren), GPIO32-39 está funcionando. + +**GPIOs 34-39 son input-only** en el ESP32-WROOM-32 — no tienen driver de salida. El pinmap los incluye para que funcionen como entradas (ADC, botones), pero `digitalWrite()` sobre ellos no tiene efecto real en hardware. + +--- + +## 16. Interacción UI — ADC, Botones y PWM Visual + +Esta sección documenta las tres capacidades de interacción bidireccional añadidas entre componentes visuales del canvas y el ESP32 emulado. + +### 16.1 ADC — Potenciómetro → `analogRead()` + +**Objetivo:** Cuando el usuario mueve un `wokwi-potentiometer` conectado a un pin ADC del ESP32, el valor leído por `analogRead()` en el firmware debe cambiar. + +**Flujo:** + +```text +Usuario mueve potenciómetro (0-100%) + → evento DOM 'input' en + → 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 + → analogRead() en firmware devuelve raw (0-4095) +``` + +**Mapa de pines ADC** (`frontend/src/components/components-wokwi/Esp32Element.ts`): + +```typescript +export const ESP32_ADC_PIN_MAP: Record = { + // ADC1 (GPIOs de solo-entrada o entrada/salida): + 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 }, + // ADC2 (compartidos con WiFi — no usar con WiFi activo): + 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 }, +}; +``` + +**Condición de activación:** el wire debe conectar el pin `SIG` del potenciómetro al GPIO ADC del ESP32. Los pines `VCC` y `GND` se ignoran para el ADC. + +### 16.2 GPIO Input — Botón → Interrupción ESP32 + +**Objetivo:** Cuando el usuario presiona/suelta un `wokwi-pushbutton` conectado a un GPIO del ESP32, el firmware debe ver el cambio de nivel lógico (funciona con `digitalRead()`, `attachInterrupt()`, etc.). + +**Flujo:** + +```text +Usuario hace click en + → evento DOM 'button-press' o 'button-release' + → 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 + → ESP32 ve el cambio en el registro GPIO_IN_REG + → digitalRead(gpioPin) devuelve el nuevo valor + → attachInterrupt() dispara si estaba configurado +``` + +**Lógica de detección en SimulatorCanvas** (efecto que corre al cambiar `components` o `wires`): + +```typescript +// Para cada componente no-ESP32: +// 1. Buscar wires que conecten este componente a un pin del ESP32 +// 2. Resolver el número de GPIO del endpoint ESP32 (boardPinToNumber) +// 3. Si el elemento es wokwi-pushbutton → registrar button-press/release +// 4. Si el elemento es wokwi-potentiometer (pin SIG) → registrar input ADC +``` + +> El efecto usa `setTimeout(300ms)` para esperar que el DOM renderice los web components antes de llamar `getElementById` y `addEventListener`. + +### 16.3 PWM Visual — `ledcWrite()` → Brillo de LED + +**Objetivo:** Cuando el firmware usa `ledcWrite(channel, duty)`, el LED conectado al GPIO controlado por ese canal debe mostrar brillo proporcional al duty cycle. + +**El problema de mapeo:** QEMU sabe el duty de cada canal LEDC, pero no sabe qué GPIO lo usa — esa asociación se establece con `ledcAttachPin(gpio, ch)` que escribe en `GPIO_FUNCX_OUT_SEL_CFG_REG`. Ver sección 9.2 para el mecanismo completo. + +**Flujo visual:** + +```text +ledcWrite(ch, duty) en firmware + → QEMU actualiza duty en array interno de LEDC + → poll_ledc() cada ~50ms lee el array + → ledc_update {channel, duty, duty_pct, gpio} enviado al frontend + → useSimulatorStore: bridge.onLedcUpdate → pinManager.updatePwm(gpio, duty/100) + → PinManager dispara callbacks registrados para ese pin + → SimulatorCanvas: onPwmChange → el.style.opacity = String(duty) + → El elemento visual (wokwi-led) muestra brillo proporcional +``` + +**Rango de valores:** + +- `duty` raw: 0–8191 (timer de 13 bits, el más común en ESP32) +- `duty_pct`: 0.0–100.0 (calculado como `duty / 8192 * 100`) +- `opacity` CSS: 0.0–1.0 (= `duty_pct / 100`) + +**Ejemplo de sketch compatible:** + +```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) { + ledcWrite(LEDC_CH, duty); // el LED en GPIO2 se ilumina gradualmente + delay(10); + } +} +``` + +--- + +## 17. Modificaciones al fork lcgamboa — Rebuild incremental + +Esta sección documenta todas las modificaciones realizadas al fork [lcgamboa/qemu](https://github.com/lcgamboa/qemu) para Velxio, y cómo recompilar solo los archivos modificados. + +### 17.1 Archivo modificado: `hw/gpio/esp32_gpio.c` + +**Commit lógico:** Codificar el índice de señal LEDC en el evento out_sel sync. + +**Problema:** Cuando el firmware llama `ledcAttachPin(gpio, ch)`, QEMU escribe el índice de señal (72-87) en `GPIO_FUNCX_OUT_SEL_CFG_REG[gpio]`. El evento de sincronización que dispara hacia el backend solo incluía el número de GPIO — el índice de señal (y por tanto el canal LEDC) se perdía. + +**Cambio:** + +```c +// Archivo: hw/gpio/esp32_gpio.c +// Función: psync_irq_handler (o equivalente que maneja out_sel writes) + +// ANTES (solo número de GPIO en bits 12:0): +qemu_set_irq(s->gpios_sync[0], (0x2000 | n)); + +// DESPUÉS (GPIO en bits 7:0, signal index en bits 15:8): +qemu_set_irq(s->gpios_sync[0], (0x2000 | ((value & 0xFF) << 8) | (n & 0xFF))); +``` + +El marcador `0x2000` en bits [13:12] identifica este tipo de evento en el backend. El backend (`esp32_worker.py`) decodifica: + +```python +marker = direction & 0xF000 # → 0x2000 +gpio_pin = direction & 0xFF # bits 7:0 +signal = (direction >> 8) & 0xFF # bits 15:8 → índice de señal LEDC +``` + +### 17.2 Cómo recompilar después de modificar `esp32_gpio.c` + +```bash +# En MSYS2 MINGW64 (Windows): +cd /e/Hardware/wokwi_clon/wokwi-libs/qemu-lcgamboa/build + +# Paso 1: Compilar solo el .obj modificado +ninja libcommon.fa.p/hw_gpio_esp32_gpio.c.obj + +# Paso 2: Relincar la DLL completa +/c/msys64/mingw64/bin/gcc.exe @dll_link.rsp + +# Paso 3: Desplegar al backend +cp libqemu-xtensa.dll /e/Hardware/wokwi_clon/backend/app/services/ + +# Verificar: +ls -lh libqemu-xtensa.dll +# → aprox 43-44 MB +``` + +**Tiempo de compilación:** ~10 segundos (vs 15-30 minutos para un build completo). + +### 17.3 Por qué el build completo puede fallar en Windows + +El primer build completo (`bash build_libqemu-esp32-win.sh`) puede fallar con errores en archivos no modificados: + +- **`windres: version.rc: No such file`** — Generado dinámicamente por meson; solo ocurre en builds limpios. Ejecutar el script una vez desde cero. +- **`gcrypt.h: No such file`** — Paquete MSYS2 no instalado. Fix: `pacman -S mingw-w64-x86_64-libgcrypt` +- **`zlib.h: No such file`** — Paquete MSYS2 no instalado. Fix: `pacman -S mingw-w64-x86_64-zlib` +- **`WinError 1314`** en `symlink-install-tree.py` — Windows no permite symlinks sin admin. Ver parche en sección 10.4. + +Una vez que hay un build completo exitoso (el `.dll` existe en `build/`), el rebuild incremental funciona siempre — basta con `ninja ` + `gcc @dll_link.rsp`. + +### 17.4 Resumen de todos los archivos modificados en el fork + +- **`hw/gpio/esp32_gpio.c`** — Codificar signal index en evento out_sel (§17.1) +- **`scripts/symlink-install-tree.py`** — Usar `shutil.copy2` en vez de `os.symlink` en Windows (§10.4) + +Todos los demás archivos del fork son idénticos al upstream de lcgamboa. No se modificaron archivos de la máquina `esp32-picsimlab`, del core Xtensa, ni de los periféricos ADC/UART/I2C/SPI/RMT. diff --git a/frontend/src/components/components-wokwi/Esp32Element.ts b/frontend/src/components/components-wokwi/Esp32Element.ts index 0d9eb5e..23ea68b 100644 --- a/frontend/src/components/components-wokwi/Esp32Element.ts +++ b/frontend/src/components/components-wokwi/Esp32Element.ts @@ -315,6 +315,31 @@ const PINS_AITEWIN_C3 = [ { name: '0', x: 84, y: 105 }, ]; +// ─── ADC pin map: GPIO → { adc bank, channel within bank, qemu chn index } ────── +// chn is the index passed to qemu_picsimlab_set_apin(): +// 0-7 → ADC1 channels 0-7 (GPIO 36,37,38,39,32,33,34,35) +// 8-17 → ADC2 channels 0-9 (GPIO 4,0,2,15,13,12,14,27,25,26) +export const ESP32_ADC_PIN_MAP: Record = { + 36: { adc: 1, ch: 0, chn: 0 }, + 37: { adc: 1, ch: 1, chn: 1 }, + 38: { adc: 1, ch: 2, chn: 2 }, + 39: { adc: 1, ch: 3, chn: 3 }, + 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 }, + 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 }, +}; + // ─── Board config by variant ────────────────────────────────────────────────── interface BoardConfig { diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 5e5c0f5..fa0ff72 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -1,5 +1,6 @@ -import { useSimulatorStore } from '../../store/useSimulatorStore'; +import { useSimulatorStore, getEsp32Bridge } from '../../store/useSimulatorStore'; import React, { useEffect, useState, useRef, useCallback } from 'react'; +import { ESP32_ADC_PIN_MAP } from '../components-wokwi/Esp32Element'; import { ComponentPickerModal } from '../ComponentPickerModal'; import { ComponentPropertyDialog } from './ComponentPropertyDialog'; import { DynamicComponent, createComponentFromMetadata } from '../DynamicComponent'; @@ -461,6 +462,13 @@ export const SimulatorCanvas = () => { } ); unsubscribers.push(unsubscribe); + + // PWM subscription: update LED opacity when the pin receives a LEDC duty cycle + const pwmUnsub = pinManager.onPwmChange(pin, (_p, duty) => { + const el = document.getElementById(component.id); + if (el) el.style.opacity = String(duty); // duty is 0.0–1.0 + }); + unsubscribers.push(pwmUnsub); }; components.forEach((component) => { @@ -504,6 +512,70 @@ export const SimulatorCanvas = () => { }; }, [components, wires, boards, pinManager, updateComponentState]); + // ESP32 input components: forward button presses and potentiometer values to QEMU + useEffect(() => { + const cleanups: (() => void)[] = []; + + components.forEach((component) => { + const connectedWires = wires.filter( + w => w.start.componentId === component.id || w.end.componentId === component.id + ); + + connectedWires.forEach(wire => { + const isStartSelf = wire.start.componentId === component.id; + const selfEndpoint = isStartSelf ? wire.start : wire.end; + const otherEndpoint = isStartSelf ? wire.end : wire.start; + + if (!isBoardComponent(otherEndpoint.componentId)) return; + + const boardId = otherEndpoint.componentId; + const bridge = getEsp32Bridge(boardId); + if (!bridge) return; // not an ESP32 board + + const boardInstance = boards.find(b => b.id === boardId); + const lookupKey = boardInstance ? boardInstance.boardKind : boardId; + const gpioPin = boardPinToNumber(lookupKey, otherEndpoint.pinName); + if (gpioPin === null) return; + + // Delay lookup so the web component has time to render + const timeout = setTimeout(() => { + const el = document.getElementById(component.id); + if (!el) return; + const tag = el.tagName.toLowerCase(); + + // Push-button: forward press/release as GPIO level changes + if (tag === 'wokwi-pushbutton') { + const onPress = () => bridge.sendPinEvent(gpioPin, true); + const onRelease = () => bridge.sendPinEvent(gpioPin, false); + el.addEventListener('button-press', onPress); + el.addEventListener('button-release', onRelease); + cleanups.push(() => { + el.removeEventListener('button-press', onPress); + el.removeEventListener('button-release', onRelease); + }); + } + + // Potentiometer: forward analog value as ADC millivolts + if (tag === 'wokwi-potentiometer' && selfEndpoint.pinName === 'SIG') { + const adcInfo = ESP32_ADC_PIN_MAP[gpioPin]; + if (adcInfo) { + const onInput = (e: Event) => { + const pct = parseFloat((e.target as any).value ?? '0'); // 0–100 + bridge.setAdc(adcInfo.chn, Math.round(pct / 100 * 3300)); + }; + el.addEventListener('input', onInput); + cleanups.push(() => el.removeEventListener('input', onInput)); + } + } + }, 300); + + cleanups.push(() => clearTimeout(timeout)); + }); + }); + + return () => cleanups.forEach(fn => fn()); + }, [components, wires, boards]); + // Handle keyboard delete useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 391ac0f..fd2bf2f 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -2891,15 +2891,15 @@ void loop() { { id: 'esp32-7segment', title: 'ESP32: 7-Segment Counter', - description: 'Count 0–9 on a 7-segment display driven from GPIO 12, 13, 14, 22, 25, 26, 27 on the ESP32.', + description: 'Count 0–9 on a 7-segment display driven from GPIO 12, 13, 14, 25, 26, 27, 32 on the ESP32.', category: 'displays', difficulty: 'beginner', boardType: 'esp32', boardFilter: 'esp32', code: `// ESP32 — 7-Segment Display Counter 0-9 -// Segments: a=12, b=13, c=14, d=25, e=26, f=27, g=22 +// Segments: a=12, b=13, c=14, d=25, e=26, f=27, g=32 -const int SEG[7] = {12, 13, 14, 25, 26, 27, 22}; +const int SEG[7] = {12, 13, 14, 25, 26, 27, 32}; const bool DIGITS[10][7] = { {1,1,1,1,1,1,0}, // 0 @@ -2942,7 +2942,7 @@ void loop() { { id: 'es-d', start: { componentId: 'esp32', pinName: '25' }, end: { componentId: 'esp-seg1', pinName: 'D' }, color: '#44cc44' }, { id: 'es-e', start: { componentId: 'esp32', pinName: '26' }, end: { componentId: 'esp-seg1', pinName: 'E' }, color: '#4488ff' }, { id: 'es-f', start: { componentId: 'esp32', pinName: '27' }, end: { componentId: 'esp-seg1', pinName: 'F' }, color: '#aa44ff' }, - { id: 'es-g', start: { componentId: 'esp32', pinName: '22' }, end: { componentId: 'esp-seg1', pinName: 'G' }, color: '#ffffff' }, + { id: 'es-g', start: { componentId: 'esp32', pinName: '32' }, end: { componentId: 'esp-seg1', pinName: 'G' }, color: '#ffffff' }, { id: 'es-gnd', start: { componentId: 'esp-seg1', pinName: 'COM' }, end: { componentId: 'esp32', pinName: 'GND' }, color: '#000000' }, ], }, diff --git a/frontend/src/simulation/Esp32Bridge.ts b/frontend/src/simulation/Esp32Bridge.ts index 96d0c49..8c0f601 100644 --- a/frontend/src/simulation/Esp32Bridge.ts +++ b/frontend/src/simulation/Esp32Bridge.ts @@ -54,7 +54,7 @@ function getTabSessionId(): string { } export interface Ws2812Pixel { r: number; g: number; b: number } -export interface LedcUpdate { channel: number; duty: number; duty_pct: number } +export interface LedcUpdate { channel: number; duty: number; duty_pct: number; gpio?: number } export class Esp32Bridge { readonly boardId: string; diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 838b150..d780647 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -308,11 +308,15 @@ export const useSimulatorStore = create((set, get) => { set({ esp32CrashBoardId: id }); }; bridge.onLedcUpdate = (update) => { - // Route LEDC duty cycles to PinManager as PWM. - // LEDC channel N drives a GPIO; the mapping is firmware-defined. + // Route LEDC duty cycles to PinManager as PWM (0.0–1.0). + // If gpio is known (from GPIO out_sel sync), use the actual GPIO pin; + // otherwise fall back to the LEDC channel number. const boardPm = pinManagerMap.get(id); - if (boardPm && typeof boardPm.updatePwm === 'function') { - boardPm.updatePwm(update.channel, update.duty_pct); + if (boardPm) { + const targetPin = (update.gpio !== undefined && update.gpio >= 0) + ? update.gpio + : update.channel; + boardPm.updatePwm(targetPin, update.duty_pct / 100); } }; bridge.onWs2812Update = (channel, pixels) => {