feat: enhance ESP32 emulation with GPIO mapping and ADC support
parent
bc31ee76c1
commit
20ccd87d1b
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <wokwi-potentiometer>
|
||||
→ 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<number, { adc: 1|2; ch: number; chn: number }> = {
|
||||
// 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 <wokwi-pushbutton>
|
||||
→ 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 <archivo.obj>` + `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.
|
||||
|
|
|
|||
|
|
@ -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<number, { adc: 1 | 2; ch: number; chn: number }> = {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -308,11 +308,15 @@ export const useSimulatorStore = create<SimulatorState>((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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue