feat: add RISC-V emulation documentation and update ESP32 emulation section
parent
74e7ec58c1
commit
5439dcf65f
|
|
@ -1,8 +1,11 @@
|
|||
# ESP32 Emulation — Documentación Técnica
|
||||
# ESP32 Emulation (Xtensa) — Documentación Técnica
|
||||
|
||||
> Estado: **Funcional** · Backend completo · Frontend completo
|
||||
> Motor: **lcgamboa/qemu-8.1.3** · Plataforma: **arduino-esp32 2.0.17 (IDF 4.4.x)**
|
||||
> Disponible en: **Windows** (`.dll`) · **Linux / Docker** (`.so`, incluido en imagen oficial)
|
||||
> Aplica a: **ESP32, ESP32-S3** (arquitectura Xtensa LX6/LX7)
|
||||
|
||||
> **Nota sobre ESP32-C3:** Los boards ESP32-C3, XIAO ESP32-C3 y ESP32-C3 SuperMini usan la arquitectura **RISC-V RV32IMC** y tienen su propio emulador en el navegador. Ver → [RISCV_EMULATION.md](./RISCV_EMULATION.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,450 @@
|
|||
# RISC-V Emulation (ESP32-C3 / XIAO-C3 / C3 SuperMini)
|
||||
|
||||
> Estado: **Funcional** · Emulación en el navegador · Sin dependencias de backend
|
||||
> Motor: **RiscVCore (RV32IMC)** — implementado en TypeScript
|
||||
> Plataforma: **ESP32-C3 @ 160 MHz** — arquitectura RISC-V de 32 bits
|
||||
|
||||
---
|
||||
|
||||
## Índice
|
||||
|
||||
1. [Visión general](#1-visión-general)
|
||||
2. [Boards soportadas](#2-boards-soportadas)
|
||||
3. [Arquitectura del emulador](#3-arquitectura-del-emulador)
|
||||
4. [Memoria y periféricos emulados](#4-memoria-y-periféricos-emulados)
|
||||
5. [Flujo completo: compilar y ejecutar](#5-flujo-completo-compilar-y-ejecutar)
|
||||
6. [Formato de imagen ESP32](#6-formato-de-imagen-esp32)
|
||||
7. [ISA soportada — RV32IMC](#7-isa-soportada--rv32imc)
|
||||
8. [GPIO](#8-gpio)
|
||||
9. [UART0 — Serial Monitor](#9-uart0--serial-monitor)
|
||||
10. [Limitaciones](#10-limitaciones)
|
||||
11. [Tests](#11-tests)
|
||||
12. [Diferencias vs emulación Xtensa (ESP32 / ESP32-S3)](#12-diferencias-vs-emulación-xtensa-esp32--esp32-s3)
|
||||
13. [Archivos clave](#13-archivos-clave)
|
||||
|
||||
---
|
||||
|
||||
## 1. Visión general
|
||||
|
||||
Los boards basados en **ESP32-C3** usan el procesador **ESP32-C3** de Espressif, que implementa la arquitectura **RISC-V RV32IMC** (32 bits, Multiply, Compressed instructions). A diferencia del ESP32 y ESP32-S3 (Xtensa LX6/LX7), el C3 **no requiere QEMU ni backend** para emularse.
|
||||
|
||||
### Comparación de motores de emulación
|
||||
|
||||
| Board | CPU | Motor |
|
||||
|-------|-----|-------|
|
||||
| ESP32, ESP32-S3 | Xtensa LX6/LX7 | QEMU lcgamboa (backend WebSocket) |
|
||||
| **ESP32-C3, XIAO-C3, C3 SuperMini** | **RV32IMC @ 160 MHz** | **RiscVCore.ts (navegador, sin backend)** |
|
||||
| Arduino Uno/Nano/Mega | AVR ATmega | avr8js (navegador) |
|
||||
| Raspberry Pi Pico | RP2040 | rp2040js (navegador) |
|
||||
|
||||
### Ventajas del emulador JS
|
||||
|
||||
- **Sin dependencias de red** — funciona offline, sin conexión WebSocket al backend
|
||||
- **Arranque instantáneo** — no hay proceso QEMU que arrancar (0 ms de latencia)
|
||||
- **Testable con Vitest** — el mismo código TypeScript que se ejecuta en producción se puede probar en CI
|
||||
- **Multiplataforma** — funciona igual en Windows, macOS, Linux y Docker
|
||||
|
||||
---
|
||||
|
||||
## 2. Boards soportadas
|
||||
|
||||
| Board | FQBN arduino-cli | LED built-in |
|
||||
|-------|-----------------|--------------|
|
||||
| ESP32-C3 DevKit | `esp32:esp32:esp32c3` | GPIO 8 |
|
||||
| Seeed XIAO ESP32-C3 | `esp32:esp32:XIAO_ESP32C3` | GPIO 10 (active-low) |
|
||||
| ESP32-C3 SuperMini | `esp32:esp32:esp32c3` | GPIO 8 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Arquitectura del emulador
|
||||
|
||||
```
|
||||
Arduino Sketch (.ino)
|
||||
│
|
||||
▼ arduino-cli (backend)
|
||||
sketch.ino.bin ← ESP32 image format (segmentos IROM/DRAM/IRAM)
|
||||
│
|
||||
▼ base64 → frontend
|
||||
compileBoardProgram(boardId, base64)
|
||||
│
|
||||
▼ Esp32C3Simulator.loadFlashImage(base64)
|
||||
parseMergedFlashImage() ← lee segmentos de la imagen 4MB
|
||||
│
|
||||
├── IROM segment → flash buffer (0x42000000)
|
||||
├── DROM segment → flash buffer (0x3C000000, alias)
|
||||
├── DRAM segment → dram buffer (0x3FC80000)
|
||||
└── IRAM segment → iram buffer (0x4037C000)
|
||||
│
|
||||
▼ core.reset(entryPoint)
|
||||
RiscVCore.step() ← requestAnimationFrame @ 60 FPS
|
||||
│ 2.666.667 ciclos/frame (160 MHz ÷ 60)
|
||||
├── MMIO GPIO_W1TS/W1TC → onPinChangeWithTime → componentes visuales
|
||||
└── MMIO UART0 FIFO → onSerialData → Serial Monitor
|
||||
```
|
||||
|
||||
### Clases principales
|
||||
|
||||
| Clase | Archivo | Responsabilidad |
|
||||
|-------|---------|----------------|
|
||||
| `RiscVCore` | `simulation/RiscVCore.ts` | Decodificador/ejecutor RV32IMC, MMIO genérico |
|
||||
| `Esp32C3Simulator` | `simulation/Esp32C3Simulator.ts` | Mapa de memoria ESP32-C3, GPIO, UART0, ciclo RAF |
|
||||
| `parseMergedFlashImage` | `utils/esp32ImageParser.ts` | Parseo formato imagen ESP32 (segmentos, entry point) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Memoria y periféricos emulados
|
||||
|
||||
### Mapa de memoria
|
||||
|
||||
| Región | Dirección base | Tamaño | Descripción |
|
||||
|--------|---------------|--------|-------------|
|
||||
| Flash IROM | `0x42000000` | 4 MB | Código ejecutable (buffer principal del core) |
|
||||
| Flash DROM | `0x3C000000` | 4 MB | Datos de solo lectura (alias del mismo buffer) |
|
||||
| DRAM | `0x3FC80000` | 384 KB | RAM de datos (stack, variables globales) |
|
||||
| IRAM | `0x4037C000` | 384 KB | RAM de instrucciones (ISR, código time-critical) |
|
||||
| UART0 | `0x60000000` | 1 KB | Serial port 0 |
|
||||
| GPIO | `0x60004000` | 512 B | Registros GPIO |
|
||||
|
||||
### GPIO — registros implementados
|
||||
|
||||
| Registro | Offset | Función |
|
||||
|----------|--------|---------|
|
||||
| `GPIO_OUT_REG` | `+0x04` | Leer/escribir estado de salida de todos los pines |
|
||||
| `GPIO_OUT_W1TS_REG` | `+0x08` | **Set bits** — poner pines a HIGH (write-only) |
|
||||
| `GPIO_OUT_W1TC_REG` | `+0x0C` | **Clear bits** — poner pines a LOW (write-only) |
|
||||
| `GPIO_IN_REG` | `+0x3C` | Leer estado de entrada de pines |
|
||||
| `GPIO_ENABLE_REG` | `+0x20` | Dirección de pines (siempre devuelve `0xFF`) |
|
||||
|
||||
Cubre **GPIO 0–21** (todos los disponibles en ESP32-C3).
|
||||
|
||||
### UART0 — registros implementados
|
||||
|
||||
| Registro | Offset | Función |
|
||||
|----------|--------|---------|
|
||||
| `UART_FIFO_REG` | `+0x00` | Escribir byte TX / leer byte RX |
|
||||
| `UART_STATUS_REG` | `+0x1C` | Estado FIFO (siempre devuelve `0` = listo) |
|
||||
|
||||
Lectura de byte de RX disponible para simular input desde Serial Monitor.
|
||||
|
||||
### Periféricos NO emulados (retornan 0 en lectura)
|
||||
|
||||
- Interrupt Matrix (`0x600C2000`)
|
||||
- System / Clock (`0x600C0000`, `0x60008000`)
|
||||
- Cache controller (`0x600C4000`)
|
||||
- Timer Group 0/1
|
||||
- SPI flash controller
|
||||
- BLE / WiFi MAC
|
||||
- ADC / DAC
|
||||
|
||||
> Estos periféricos retornan `0` por defecto. El código que los requiere puede no funcionar correctamente (ver [Limitaciones](#10-limitaciones)).
|
||||
|
||||
---
|
||||
|
||||
## 5. Flujo completo: compilar y ejecutar
|
||||
|
||||
### 5.1 Compilar el sketch
|
||||
|
||||
```bash
|
||||
# arduino-cli compila para ESP32-C3:
|
||||
arduino-cli compile \
|
||||
--fqbn esp32:esp32:esp32c3 \
|
||||
--output-dir build/ \
|
||||
mi_sketch/
|
||||
|
||||
# El backend crea automáticamente la imagen fusionada (merged):
|
||||
# build/mi_sketch.ino.bootloader.bin → 0x01000
|
||||
# build/mi_sketch.ino.partitions.bin → 0x08000
|
||||
# build/mi_sketch.ino.bin → 0x10000 (app)
|
||||
# → merged: sketch.ino.merged.bin (4 MB)
|
||||
```
|
||||
|
||||
El backend de Velxio produce esta imagen automáticamente y la envía al frontend como base64.
|
||||
|
||||
### 5.2 Sketch mínimo para ESP32-C3
|
||||
|
||||
```cpp
|
||||
// LED en GPIO 8 (ESP32-C3 DevKit)
|
||||
#define LED_PIN 8
|
||||
|
||||
void setup() {
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
Serial.begin(115200);
|
||||
Serial.println("ESP32-C3 iniciado");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
Serial.println("LED ON");
|
||||
delay(500);
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
Serial.println("LED OFF");
|
||||
delay(500);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Sketch bare-metal (para tests de emulación directos)
|
||||
|
||||
Para verificar la emulación sin el framework Arduino, se puede compilar con el toolchain RISC-V directamente:
|
||||
|
||||
```c
|
||||
/* blink.c — bare-metal, sin ESP-IDF */
|
||||
#define GPIO_W1TS (*(volatile unsigned int *)0x60004008u)
|
||||
#define GPIO_W1TC (*(volatile unsigned int *)0x6000400Cu)
|
||||
#define LED_BIT (1u << 8)
|
||||
|
||||
static void delay(int n) { for (volatile int i = 0; i < n; i++); }
|
||||
|
||||
void _start(void) {
|
||||
while (1) {
|
||||
GPIO_W1TS = LED_BIT; /* LED ON */
|
||||
delay(500);
|
||||
GPIO_W1TC = LED_BIT; /* LED OFF */
|
||||
delay(500);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Compilar con el toolchain bundled en arduino-cli:
|
||||
|
||||
```bash
|
||||
# Toolchain instalado con: arduino-cli core install esp32:esp32
|
||||
TOOLCHAIN="$LOCALAPPDATA/Arduino15/packages/esp32/tools/riscv32-esp-elf-gcc/esp-2021r2-patch5-8.4.0/bin"
|
||||
|
||||
"$TOOLCHAIN/riscv32-esp-elf-gcc" \
|
||||
-march=rv32imc -mabi=ilp32 -Os -nostdlib -nostartfiles \
|
||||
-T link.ld -o blink.elf blink.c
|
||||
|
||||
"$TOOLCHAIN/riscv32-esp-elf-objcopy" -O binary blink.elf blink.bin
|
||||
```
|
||||
|
||||
Ver script completo: `frontend/src/__tests__/fixtures/esp32c3-blink/build.sh`
|
||||
|
||||
---
|
||||
|
||||
## 6. Formato de imagen ESP32
|
||||
|
||||
El backend produce una imagen fusionada de **4 MB**:
|
||||
|
||||
```
|
||||
Offset 0x00000: 0xFF (vacío)
|
||||
Offset 0x01000: bootloader (imagen ESP32 format, magic 0xE9)
|
||||
Offset 0x08000: partition table
|
||||
Offset 0x10000: app binary (imagen ESP32 format, magic 0xE9) ← parseamos aquí
|
||||
```
|
||||
|
||||
### Cabecera de imagen ESP32 (24 bytes)
|
||||
|
||||
```
|
||||
+0x00 magic (0xE9)
|
||||
+0x01 segment_count
|
||||
+0x02 spi_mode
|
||||
+0x03 spi_speed_size
|
||||
+0x04 entry_addr ← uint32 LE — PC de entrada del firmware
|
||||
+0x08 extended fields (16 bytes)
|
||||
```
|
||||
|
||||
### Cabecera de segmento (8 bytes)
|
||||
|
||||
```
|
||||
+0x00 load_addr ← dirección virtual destino (e.g. 0x42000000)
|
||||
+0x04 data_len
|
||||
+0x08 data[data_len]
|
||||
```
|
||||
|
||||
El parser `parseMergedFlashImage()` en `utils/esp32ImageParser.ts` extrae todos los segmentos y el entry point, que se usa para el reset del core (`core.reset(entryPoint)`).
|
||||
|
||||
---
|
||||
|
||||
## 7. ISA soportada — RV32IMC
|
||||
|
||||
`RiscVCore.ts` implementa las tres extensiones necesarias para ejecutar código compilado para ESP32-C3:
|
||||
|
||||
### RV32I — Base integer (40 instrucciones)
|
||||
|
||||
Incluye: LUI, AUIPC, JAL, JALR, BEQ/BNE/BLT/BGE/BLTU/BGEU, LB/LH/LW/LBU/LHU, SB/SH/SW, ADDI/SLTI/SLTIU/XORI/ORI/ANDI/SLLI/SRLI/SRAI, ADD/SUB/SLL/SLT/SLTU/XOR/SRL/SRA/OR/AND, FENCE, ECALL/EBREAK, CSR (lectura devuelve 0)
|
||||
|
||||
### RV32M — Multiplicación y división (8 instrucciones)
|
||||
|
||||
| Instrucción | Operación |
|
||||
|-------------|-----------|
|
||||
| `MUL` | Producto entero (32 bits bajos) |
|
||||
| `MULH` | Producto con signo (32 bits altos) |
|
||||
| `MULHSU` | Producto mixto firmado×sin firma (altos) |
|
||||
| `MULHU` | Producto sin firma (32 bits altos) |
|
||||
| `DIV` | División entera con signo |
|
||||
| `DIVU` | División entera sin firma |
|
||||
| `REM` | Resto con signo |
|
||||
| `REMU` | Resto sin firma |
|
||||
|
||||
### RV32C — Instrucciones comprimidas (16 bits)
|
||||
|
||||
Todas las instrucciones de 16 bits del estándar C son soportadas. Se detectan por `(halfword & 3) !== 3` y se descomprimen a su equivalente RV32I antes de ejecutar. Esto es crítico: el compilador GCC para ESP32-C3 genera intensamente instrucciones C (`c.addi`, `c.sw`, `c.lw`, `c.j`, `c.beqz`, `c.bnez`, etc.) que representan ~30-40% de todas las instrucciones en el binario final.
|
||||
|
||||
---
|
||||
|
||||
## 8. GPIO
|
||||
|
||||
El manejo de GPIO sigue el modelo de registros W1TS/W1TC del ESP32-C3:
|
||||
|
||||
```typescript
|
||||
// Sketch Arduino:
|
||||
digitalWrite(8, HIGH); // → internamente escribe 1<<8 a GPIO_OUT_W1TS_REG
|
||||
|
||||
// En el simulador:
|
||||
// SW x10, 0(x12) donde x10=256 (1<<8), x12=0x60004008 (W1TS)
|
||||
// → escribe 4 bytes a 0x60004008..0x6000400B
|
||||
// → byteIdx=1 (offset 0x09): val=0x01, shift=8 → gpioOut |= 0x100
|
||||
// → changed = prev ^ gpioOut ≠ 0 → dispara onPinChangeWithTime(8, true, timeMs)
|
||||
```
|
||||
|
||||
El callback `onPinChangeWithTime(pin, state, timeMs)` es el punto de integración con los componentes visuales. `timeMs` es el tiempo simulado en milisegundos (calculado como `core.cycles / CPU_HZ * 1000`).
|
||||
|
||||
---
|
||||
|
||||
## 9. UART0 — Serial Monitor
|
||||
|
||||
Cualquier byte escrito a `UART0_FIFO_REG` (0x60000000) llama al callback `onSerialData(char)`:
|
||||
|
||||
```cpp
|
||||
// Sketch Arduino:
|
||||
Serial.println("Hola!");
|
||||
// → Arduino framework escribe los bytes de "Hola!\r\n" a UART0_FIFO_REG
|
||||
// → simulador llama onSerialData("H"), onSerialData("o"), ...
|
||||
// → Serial Monitor muestra "Hola!"
|
||||
```
|
||||
|
||||
Para enviar datos al sketch desde el Serial Monitor:
|
||||
|
||||
```typescript
|
||||
sim.serialWrite("COMANDO\n");
|
||||
// → bytes se añaden a rxFifo
|
||||
// → lectura de UART0_FIFO_REG dequeue un byte del rxFifo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Limitaciones
|
||||
|
||||
### Framework ESP-IDF / Arduino
|
||||
|
||||
El framework Arduino para ESP32-C3 (basado en ESP-IDF 4.4.x) tiene una secuencia de inicialización compleja que accede a periféricos no emulados:
|
||||
|
||||
| Periférico | Por qué lo accede ESP-IDF | Efecto en emulador |
|
||||
|------------|--------------------------|-------------------|
|
||||
| Cache controller | Configura MMU para mapeo flash/DRAM | Lee 0, puede que no loop |
|
||||
| Interrupt Matrix | Registra vectores ISR | Sin efecto (silenciado) |
|
||||
| System registers | Configura PLLs y clocks | Lee 0 (asume velocidad por defecto) |
|
||||
| FreeRTOS tick timer | Timer 0 → interrupción periódica | Sin interrupción = tareas no se planifican |
|
||||
|
||||
Como resultado, un sketch Arduino compilado con el framework completo puede ejecutarse parcialmente — el código anterior a la inicialización de FreeRTOS puede funcionar, pero `setup()` y `loop()` dependen de que FreeRTOS esté corriendo.
|
||||
|
||||
**Escenarios que SÍ funcionan:**
|
||||
|
||||
- Código bare-metal (sin framework, acceso directo a GPIO MMIO)
|
||||
- Fragmentos de código que no usen FreeRTOS (`delay()`, `millis()`, `digitalWrite()` requieren FreeRTOS)
|
||||
- Programas de prueba de ISA (operaciones aritméticas, branches, loads/stores a DRAM)
|
||||
|
||||
**Roadmap para soporte completo:**
|
||||
|
||||
1. Stub del cache controller (devolver valores que indiquen "cache ya configurada")
|
||||
2. Stub del interrupt matrix (aceptar writes, ignorar)
|
||||
3. Timer peripheral básico (generar tick FreeRTOS periódicamente)
|
||||
4. Una vez activo FreeRTOS: sketches Arduino normales deberían funcionar
|
||||
|
||||
### Otras limitaciones
|
||||
|
||||
| Limitación | Detalle |
|
||||
|------------|---------|
|
||||
| Sin WiFi | El ESP32-C3 tiene radio BLE/WiFi; no emulada |
|
||||
| Sin ADC | GPIO 0-5 como ADC no implementado |
|
||||
| Sin SPI/I2C hardware | Los periféricos hardware SPI/I2C retornan 0 |
|
||||
| Sin interrupciones | `attachInterrupt()` no funciona |
|
||||
| Sin RTC | `esp_sleep_*`, `rtc_*` no implementados |
|
||||
| Sin NVS/Flash writes | `Preferences`, `SPIFFS` no implementados |
|
||||
|
||||
---
|
||||
|
||||
## 11. Tests
|
||||
|
||||
Los tests de la emulación RISC-V están en `frontend/src/__tests__/`:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm test -- esp32c3
|
||||
```
|
||||
|
||||
### `esp32c3-simulation.test.ts` — 30 tests (ISA unit tests)
|
||||
|
||||
Verifica directamente el decodificador de instrucciones de `RiscVCore`:
|
||||
|
||||
| Grupo | Tests | Qué verifica |
|
||||
|-------|-------|--------------|
|
||||
| RV32M | 8 | MUL, MULH, MULHSU, MULHU, DIV, DIVU, REM, REMU |
|
||||
| RV32C | 7 | C.ADDI, C.LI, C.LWSP, C.SWSP, C.MV, C.ADD, C.J, C.BEQZ |
|
||||
| UART | 3 | Escritura a FIFO → onSerialData, lectura de RX, múltiples bytes |
|
||||
| GPIO | 8 | W1TS set bit, W1TC clear bit, toggle, timestamp, múltiples pines |
|
||||
| Lifecycle | 4 | reset(), start/stop, loadHex básico |
|
||||
|
||||
### `esp32c3-blink.test.ts` — 8 tests (integración end-to-end)
|
||||
|
||||
Compila `blink.c` con `riscv32-esp-elf-gcc` (el toolchain de arduino-cli) y verifica la ejecución en el simulador:
|
||||
|
||||
| Test | Qué verifica |
|
||||
|------|-------------|
|
||||
| `build.sh produces blink.bin` | El toolchain compila correctamente |
|
||||
| `binary starts with valid RV32 instruction` | El entry point es código RISC-V válido |
|
||||
| `loadBin() resets PC to 0x42000000` | Carga correcta en flash |
|
||||
| `GPIO 8 goes HIGH after first SW` | Primer toggle correcto |
|
||||
| `GPIO 8 toggles ON and OFF` | 7 toggles en 2000 pasos (4 ON, 3 OFF) |
|
||||
| `PinManager.setPinState called` | Integración con el sistema de componentes |
|
||||
| `timestamps increase monotonically` | El tiempo simulado es consistente |
|
||||
| `reset() clears GPIO state` | Reset funcional |
|
||||
|
||||
**Resultado esperado:**
|
||||
```
|
||||
✓ esp32c3-simulation.test.ts (30 tests) ~500ms
|
||||
✓ esp32c3-blink.test.ts (8 tests) ~300ms
|
||||
```
|
||||
|
||||
### Binario de prueba bare-metal
|
||||
|
||||
```
|
||||
frontend/src/__tests__/fixtures/esp32c3-blink/
|
||||
├── blink.c ← código fuente bare-metal
|
||||
├── link.ld ← linker script (IROM @ 0x42000000, DRAM @ 0x3FC80000)
|
||||
├── build.sh ← script de compilación (usa toolchain de arduino-cli)
|
||||
├── blink.elf ← (generado) ELF con debug info
|
||||
├── blink.bin ← (generado) binario raw de 58 bytes
|
||||
└── blink.dis ← (generado) desensamblado para inspección
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Diferencias vs emulación Xtensa (ESP32 / ESP32-S3)
|
||||
|
||||
| Aspecto | ESP32-C3 (RISC-V) | ESP32 / ESP32-S3 (Xtensa) |
|
||||
|---------|-------------------|--------------------------|
|
||||
| Motor | `Esp32C3Simulator` (TypeScript, navegador) | `Esp32Bridge` + backend QEMU |
|
||||
| Dependencia backend | **No** — 100% en el navegador | Sí — WebSocket a proceso QEMU |
|
||||
| Arranque | Instantáneo | ~1-2 segundos |
|
||||
| GPIO | Via MMIO W1TS/W1TC | Via QEMU callbacks → WebSocket |
|
||||
| WiFi | No emulada | Emulada (SSIDs hardcoded) |
|
||||
| I2C/SPI hardware | No emulado | Emulado (callbacks síncronos) |
|
||||
| LEDC/PWM | No emulado | Emulado (poll periódico) |
|
||||
| NeoPixel/RMT | No emulado | Emulado (decodificador RMT) |
|
||||
| Arduino framework | Parcial (FreeRTOS no activo) | Completo |
|
||||
| Tests CI | Sí (Vitest) | No (requiere lib nativa) |
|
||||
|
||||
---
|
||||
|
||||
## 13. Archivos clave
|
||||
|
||||
| Archivo | Descripción |
|
||||
|---------|-------------|
|
||||
| `frontend/src/simulation/RiscVCore.ts` | Núcleo del emulador RV32IMC (I + M + C extensions) |
|
||||
| `frontend/src/simulation/Esp32C3Simulator.ts` | Mapa de memoria ESP32-C3, GPIO, UART0, ciclo RAF |
|
||||
| `frontend/src/utils/esp32ImageParser.ts` | Parser del formato imagen ESP32 (merged flash → segmentos) |
|
||||
| `frontend/src/store/useSimulatorStore.ts` | `ESP32_RISCV_KINDS`, `createSimulator()`, `compileBoardProgram()` |
|
||||
| `frontend/src/__tests__/esp32c3-simulation.test.ts` | Unit tests ISA (30 tests) |
|
||||
| `frontend/src/__tests__/esp32c3-blink.test.ts` | Integration test end-to-end (8 tests) |
|
||||
| `frontend/src/__tests__/fixtures/esp32c3-blink/` | Firmware bare-metal de prueba + toolchain script |
|
||||
|
|
@ -27,6 +27,8 @@ type SectionId =
|
|||
| 'intro'
|
||||
| 'getting-started'
|
||||
| 'emulator'
|
||||
| 'riscv-emulation'
|
||||
| 'esp32-emulation'
|
||||
| 'components'
|
||||
| 'roadmap'
|
||||
| 'architecture'
|
||||
|
|
@ -38,6 +40,8 @@ const VALID_SECTIONS: SectionId[] = [
|
|||
'intro',
|
||||
'getting-started',
|
||||
'emulator',
|
||||
'riscv-emulation',
|
||||
'esp32-emulation',
|
||||
'components',
|
||||
'roadmap',
|
||||
'architecture',
|
||||
|
|
@ -55,6 +59,8 @@ const NAV_ITEMS: NavItem[] = [
|
|||
{ id: 'intro', label: 'Introduction' },
|
||||
{ id: 'getting-started', label: 'Getting Started' },
|
||||
{ id: 'emulator', label: 'Emulator Architecture' },
|
||||
{ id: 'riscv-emulation', label: 'RISC-V Emulation (ESP32-C3)' },
|
||||
{ id: 'esp32-emulation', label: 'ESP32 Emulation (Xtensa)' },
|
||||
{ id: 'components', label: 'Components Reference' },
|
||||
{ id: 'architecture', label: 'Project Architecture' },
|
||||
{ id: 'wokwi-libs', label: 'Wokwi Libraries' },
|
||||
|
|
@ -76,7 +82,15 @@ const SECTION_META: Record<SectionId, SectionMeta> = {
|
|||
},
|
||||
'emulator': {
|
||||
title: 'Emulator Architecture — Velxio Documentation',
|
||||
description: 'How Velxio emulates AVR8 (ATmega328p) and RP2040 CPUs. Covers the execution loop, peripherals (GPIO, Timers, USART, ADC, SPI, I2C), and pin mapping.',
|
||||
description: 'How Velxio emulates AVR8 (ATmega328p), RP2040, and RISC-V (ESP32-C3) CPUs. Covers execution loops, peripherals, and pin mapping for all supported boards.',
|
||||
},
|
||||
'riscv-emulation': {
|
||||
title: 'RISC-V Emulation (ESP32-C3) — Velxio Documentation',
|
||||
description: 'Browser-side RV32IMC emulator for ESP32-C3, XIAO ESP32-C3, and C3 SuperMini. Covers memory map, GPIO, UART0, the ESP32 image parser, RV32IMC ISA, and test suite.',
|
||||
},
|
||||
'esp32-emulation': {
|
||||
title: 'ESP32 Emulation (Xtensa) — Velxio Documentation',
|
||||
description: 'QEMU-based emulation for ESP32 and ESP32-S3 (Xtensa LX6/LX7). Covers the lcgamboa fork, libqemu-xtensa, GPIO, WiFi, I2C, SPI, RMT/NeoPixel, and LEDC/PWM.',
|
||||
},
|
||||
'components': {
|
||||
title: 'Components Reference — Velxio Documentation',
|
||||
|
|
@ -134,6 +148,8 @@ const IntroSection: React.FC = () => (
|
|||
<tr><td>Arduino Nano</td><td>ATmega328p @ 16 MHz</td><td>avr8js</td></tr>
|
||||
<tr><td>Arduino Mega</td><td>ATmega2560 @ 16 MHz</td><td>avr8js</td></tr>
|
||||
<tr><td>Raspberry Pi Pico</td><td>RP2040 @ 133 MHz</td><td>rp2040js</td></tr>
|
||||
<tr><td>ESP32-C3 / XIAO C3 / C3 SuperMini</td><td>RV32IMC @ 160 MHz</td><td>Esp32C3Simulator (browser)</td></tr>
|
||||
<tr><td>ESP32 / ESP32-S3</td><td>Xtensa LX6/LX7 @ 240 MHz</td><td>QEMU (lcgamboa)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
|
@ -1019,10 +1035,179 @@ const SetupSection: React.FC = () => (
|
|||
</div>
|
||||
);
|
||||
|
||||
const RiscVEmulationSection: React.FC = () => (
|
||||
<div className="docs-section">
|
||||
<span className="docs-label">// risc-v</span>
|
||||
<h1>RISC-V Emulation (ESP32-C3)</h1>
|
||||
<p>
|
||||
ESP32-C3, XIAO ESP32-C3, and C3 SuperMini boards use a <strong>RISC-V RV32IMC</strong> core running at
|
||||
160 MHz. Velxio emulates them entirely in the browser — no backend, no QEMU, no WebAssembly pipeline.
|
||||
The emulator is written in pure TypeScript and runs at real-time speeds.
|
||||
</p>
|
||||
|
||||
<h2>Supported Boards</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Board</th><th>CPU</th><th>Flash</th><th>RAM</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>ESP32-C3</td><td>RV32IMC @ 160 MHz</td><td>4 MB</td><td>384 KB DRAM</td></tr>
|
||||
<tr><td>XIAO ESP32-C3</td><td>RV32IMC @ 160 MHz</td><td>4 MB</td><td>384 KB DRAM</td></tr>
|
||||
<tr><td>C3 SuperMini</td><td>RV32IMC @ 160 MHz</td><td>4 MB</td><td>384 KB DRAM</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Memory Map</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Region</th><th>Base Address</th><th>Size</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>IROM (Flash)</td><td><code>0x42000000</code></td><td>4 MB</td><td>Code stored in flash</td></tr>
|
||||
<tr><td>DROM (Flash R/O)</td><td><code>0x3C000000</code></td><td>4 MB</td><td>Read-only data in flash</td></tr>
|
||||
<tr><td>DRAM</td><td><code>0x3FC80000</code></td><td>384 KB</td><td>Data RAM (stack + heap)</td></tr>
|
||||
<tr><td>IRAM</td><td><code>0x4037C000</code></td><td>384 KB</td><td>Instruction RAM (copied from flash)</td></tr>
|
||||
<tr><td>UART0</td><td><code>0x60000000</code></td><td>1 KB</td><td>Serial port 0 (GPIO 20/21)</td></tr>
|
||||
<tr><td>GPIO</td><td><code>0x60004000</code></td><td>512 B</td><td>GPIO output / input / enable</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>ISA Support</h2>
|
||||
<ul>
|
||||
<li><strong>RV32I</strong> — Full base integer instruction set (ALU, load/store, branches, JAL/JALR)</li>
|
||||
<li><strong>RV32M</strong> — Multiply/divide: MUL, MULH, MULHSU, MULHU, DIV, DIVU, REM, REMU</li>
|
||||
<li><strong>RV32C</strong> — 16-bit compressed instructions: C.LI, C.ADDI, C.LUI, C.J, C.JAL, C.BEQZ,
|
||||
C.BNEZ, C.MV, C.ADD, C.JR, C.JALR, C.LW, C.SW, C.LWSP, C.SWSP, C.SLLI, C.ADDI4SPN</li>
|
||||
</ul>
|
||||
|
||||
<h2>Compilation Flow</h2>
|
||||
<p>When you click <strong>Compile + Run</strong> for an ESP32-C3 board:</p>
|
||||
<ol>
|
||||
<li>The backend compiles your sketch with <code>arduino-cli</code> using the <code>esp32:esp32</code> core.</li>
|
||||
<li>The resulting binary is a <strong>merged 4 MB flash image</strong>: bootloader at <code>0x1000</code>,
|
||||
partition table at <code>0x8000</code>, application at <code>0x10000</code>.</li>
|
||||
<li>The frontend's <code>esp32ImageParser.ts</code> finds the app at offset <code>0x10000</code>,
|
||||
reads the 24-byte ESP32 image header (magic <code>0xE9</code>), and extracts all segments
|
||||
(load address + data).</li>
|
||||
<li>Each segment is written into the correct memory region (IROM, DROM, DRAM, or IRAM) of
|
||||
the <code>Esp32C3Simulator</code>.</li>
|
||||
<li>The CPU starts executing from the entry point specified in the image header.</li>
|
||||
</ol>
|
||||
|
||||
<h2>GPIO Registers</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Register</th><th>Offset</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>GPIO_OUT_REG</code></td><td><code>+0x04</code></td><td>Current output value</td></tr>
|
||||
<tr><td><code>GPIO_OUT_W1TS</code></td><td><code>+0x08</code></td><td>Set bits (write 1 to set)</td></tr>
|
||||
<tr><td><code>GPIO_OUT_W1TC</code></td><td><code>+0x0C</code></td><td>Clear bits (write 1 to clear)</td></tr>
|
||||
<tr><td><code>GPIO_ENABLE_REG</code></td><td><code>+0x20</code></td><td>Output enable</td></tr>
|
||||
<tr><td><code>GPIO_IN_REG</code></td><td><code>+0x3C</code></td><td>Input pin states</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>UART0</h2>
|
||||
<p>
|
||||
Writing a byte to <code>0x60000000</code> (UART0 FIFO) triggers the <code>onSerialData</code> callback,
|
||||
which streams characters to the Serial Monitor. Reading from the same address pops from the receive
|
||||
FIFO (used by <code>Serial.read()</code>). The UART status register always returns 0 (TX ready).
|
||||
</p>
|
||||
|
||||
<h2>Key Source Files</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>File</th><th>Role</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>simulation/RiscVCore.ts</code></td><td>RV32IMC interpreter (step, MMIO hooks)</td></tr>
|
||||
<tr><td><code>simulation/Esp32C3Simulator.ts</code></td><td>ESP32-C3 peripherals, memory map, lifecycle</td></tr>
|
||||
<tr><td><code>utils/esp32ImageParser.ts</code></td><td>Parses merged flash image, extracts segments</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="docs-callout">
|
||||
<strong>Full details:</strong>{' '}
|
||||
<a href={`${GITHUB_URL}/blob/master/docs/RISCV_EMULATION.md`} target="_blank" rel="noopener noreferrer">
|
||||
docs/RISCV_EMULATION.md
|
||||
</a>{' '}
|
||||
in the repository.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Esp32EmulationSection: React.FC = () => (
|
||||
<div className="docs-section">
|
||||
<span className="docs-label">// xtensa</span>
|
||||
<h1>ESP32 Emulation (Xtensa)</h1>
|
||||
<p>
|
||||
ESP32 and ESP32-S3 boards use an <strong>Xtensa LX6 / LX7</strong> architecture. Because no
|
||||
production-quality Xtensa emulator is available as pure JavaScript, Velxio uses a
|
||||
<strong> QEMU-based backend</strong> for these boards — the lcgamboa fork with
|
||||
libqemu-xtensa, compiled to a native binary and served by the FastAPI backend.
|
||||
</p>
|
||||
|
||||
<div className="docs-callout">
|
||||
<strong>Note:</strong> This section applies only to <strong>ESP32</strong> and <strong>ESP32-S3</strong> (Xtensa).
|
||||
For ESP32-C3, XIAO ESP32-C3, and C3 SuperMini (RISC-V), see{' '}
|
||||
<strong>RISC-V Emulation (ESP32-C3)</strong> in the sidebar — those boards run entirely in the browser.
|
||||
</div>
|
||||
|
||||
<h2>How It Works</h2>
|
||||
<ol>
|
||||
<li>Arduino sketch is compiled by <code>arduino-cli</code> to an ESP32 <code>.bin</code> flash image.</li>
|
||||
<li>Frontend sends the binary to the backend via WebSocket (<code>Esp32Bridge</code>).</li>
|
||||
<li>Backend spawns a QEMU process with the lcgamboa Xtensa plugin, loads the image.</li>
|
||||
<li>GPIO and UART events are forwarded over the WebSocket back to the browser.</li>
|
||||
<li>Frontend updates component states (LEDs, display, etc.) in real time.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Supported Boards</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Board</th><th>CPU</th><th>Emulation</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>ESP32</td><td>Xtensa LX6 dual-core @ 240 MHz</td><td>QEMU (lcgamboa)</td></tr>
|
||||
<tr><td>ESP32-S3</td><td>Xtensa LX7 dual-core @ 240 MHz</td><td>QEMU (lcgamboa)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Peripheral Support</h2>
|
||||
<ul>
|
||||
<li><strong>GPIO</strong> — digital output / input, LED control</li>
|
||||
<li><strong>UART</strong> — Serial Monitor via <code>Serial.print()</code></li>
|
||||
<li><strong>I2C / SPI</strong> — peripheral communication</li>
|
||||
<li><strong>RMT / NeoPixel</strong> — addressable LED strips</li>
|
||||
<li><strong>LEDC / PWM</strong> — hardware PWM channels</li>
|
||||
<li><strong>WiFi</strong> — partial (connection events forwarded)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Requirements</h2>
|
||||
<p>
|
||||
QEMU-based emulation requires the Velxio backend to be running. This means it works with
|
||||
the <strong>hosted version</strong> at{' '}
|
||||
<a href="https://velxio.dev" target="_blank" rel="noopener noreferrer">velxio.dev</a>{' '}
|
||||
and with <strong>Docker self-hosting</strong>, but not in a pure static frontend deployment.
|
||||
</p>
|
||||
|
||||
<div className="docs-callout">
|
||||
<strong>Full details:</strong>{' '}
|
||||
<a href={`${GITHUB_URL}/blob/master/docs/ESP32_EMULATION.md`} target="_blank" rel="noopener noreferrer">
|
||||
docs/ESP32_EMULATION.md
|
||||
</a>{' '}
|
||||
in the repository.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SECTION_MAP: Record<SectionId, React.FC> = {
|
||||
intro: IntroSection,
|
||||
'getting-started': GettingStartedSection,
|
||||
emulator: EmulatorSection,
|
||||
'riscv-emulation': RiscVEmulationSection,
|
||||
'esp32-emulation': Esp32EmulationSection,
|
||||
components: ComponentsSection,
|
||||
roadmap: RoadmapSection,
|
||||
architecture: ArchitectureSection,
|
||||
|
|
|
|||
Loading…
Reference in New Issue