feat: update ESP32-C3 simulator and add emulation tests
- Updated components-metadata.json with new generated timestamp. - Refactored Esp32C3Simulator.ts to remove unnecessary debug variables and logging, and added support for additional ROM functions. - Modified useSimulatorStore.ts to clarify bridge usage for ESP32 boards. - Updated submodules for QEMU and other libraries to indicate dirty state. - Added test_esp32c3_emulation.py for end-to-end testing of ESP32-C3 emulation, including compilation, flash image merging, and GPIO event checking.pull/47/head
parent
372e0f1d00
commit
7053b6f2c8
|
|
@ -82,12 +82,15 @@ data/*
|
|||
.publicar_discord/*
|
||||
img/*
|
||||
|
||||
# ESP32 QEMU runtime binaries (too large for git — build/download separately)
|
||||
# ESP32 QEMU runtime binaries (too large for git — build/copy from MSYS2/qemu-lcgamboa)
|
||||
backend/app/services/libqemu-xtensa.dll
|
||||
backend/app/services/libqemu-riscv32.dll
|
||||
backend/app/services/esp32-v3-rom.bin
|
||||
backend/app/services/esp32-v3-rom-app.bin
|
||||
backend/app/services/esp32c3-rom.bin
|
||||
|
||||
# ESP32 build artifacts (ELF/MAP debug symbols — large, not needed for tests)
|
||||
test/esp32-emulator/**/*.elf
|
||||
test/esp32-emulator/**/*.map
|
||||
test/esp32-emulator/out_*/
|
||||
.claude/settings.json
|
||||
|
|
|
|||
|
|
@ -75,13 +75,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||
RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
|
||||
| BINDIR=/usr/local/bin sh
|
||||
|
||||
# Initialize arduino-cli config, add RP2040 board manager URL, then install cores
|
||||
# Initialize arduino-cli config, add board manager URLs, then install cores
|
||||
# - RP2040: earlephilhower fork for Raspberry Pi Pico
|
||||
# - ESP32: Espressif official (covers ESP32, ESP32-S3, ESP32-C3, etc.)
|
||||
RUN arduino-cli config init \
|
||||
&& arduino-cli config add board_manager.additional_urls \
|
||||
https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json \
|
||||
&& arduino-cli config add board_manager.additional_urls \
|
||||
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json \
|
||||
&& arduino-cli core update-index \
|
||||
&& arduino-cli core install arduino:avr \
|
||||
&& arduino-cli core install rp2040:rp2040
|
||||
&& arduino-cli core install rp2040:rp2040 \
|
||||
&& arduino-cli core install esp32:esp32
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
@ -107,16 +112,23 @@ RUN chmod +x /app/entrypoint.sh
|
|||
|
||||
# ── ESP32 emulation: lcgamboa QEMU .so + ROM binaries ────────────────────────
|
||||
# libqemu-xtensa.so → ESP32 / ESP32-S3 (Xtensa LX6/LX7)
|
||||
# libqemu-riscv32.so → ESP32-C3 (RISC-V)
|
||||
# libqemu-riscv32.so → ESP32-C3 (RISC-V RV32IMC)
|
||||
# esp32-v3-rom*.bin → boot/app ROM images required by esp32-picsimlab machine
|
||||
# esp32c3-rom.bin → ROM image required by esp32c3-picsimlab machine
|
||||
# NOTE: ROM files must live in the same directory as the .so (worker passes -L
|
||||
# to QEMU pointing at os.path.dirname(lib_path))
|
||||
RUN mkdir -p /app/lib
|
||||
COPY --from=qemu-builder /qemu-lcgamboa/build/libqemu-xtensa.so /app/lib/
|
||||
COPY --from=qemu-builder /qemu-lcgamboa/build/libqemu-riscv32.so /app/lib/
|
||||
COPY --from=qemu-builder /qemu-lcgamboa/pc-bios/esp32-v3-rom.bin /app/lib/
|
||||
COPY --from=qemu-builder /qemu-lcgamboa/pc-bios/esp32-v3-rom-app.bin /app/lib/
|
||||
COPY --from=qemu-builder /qemu-lcgamboa/pc-bios/esp32c3-rom.bin /app/lib/
|
||||
|
||||
# Activate full ESP32 emulation (GPIO + ADC + UART + PWM + NeoPixel + WiFi)
|
||||
# Activate ESP32 emulation
|
||||
# QEMU_ESP32_LIB → Xtensa library (ESP32, ESP32-S3)
|
||||
# QEMU_RISCV32_LIB → RISC-V library (ESP32-C3 and variants)
|
||||
ENV QEMU_ESP32_LIB=/app/lib/libqemu-xtensa.so
|
||||
ENV QEMU_RISCV32_LIB=/app/lib/libqemu-riscv32.so
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
|
|
|
|||
|
|
@ -35,14 +35,14 @@ async def simulation_websocket(websocket: WebSocket, client_id: str):
|
|||
|
||||
async def qemu_callback(event_type: str, data: dict) -> None:
|
||||
if event_type == 'gpio_change':
|
||||
logger.info('[%s] gpio_change pin=%s state=%s', client_id, data.get('pin'), data.get('state'))
|
||||
logger.debug('[%s] gpio_change pin=%s state=%s', client_id, data.get('pin'), data.get('state'))
|
||||
elif event_type == 'system':
|
||||
logger.info('[%s] system event: %s', client_id, data.get('event'))
|
||||
logger.debug('[%s] system event: %s', client_id, data.get('event'))
|
||||
elif event_type == 'error':
|
||||
logger.error('[%s] error: %s', client_id, data.get('message'))
|
||||
elif event_type == 'serial_output':
|
||||
text = data.get('data', '')
|
||||
logger.info('[%s] serial_output uart=%s len=%d: %r', client_id, data.get('uart', 0), len(text), text[:80])
|
||||
logger.debug('[%s] serial_output uart=%s len=%d: %r', client_id, data.get('uart', 0), len(text), text[:80])
|
||||
payload = json.dumps({'type': event_type, 'data': data})
|
||||
try:
|
||||
await manager.send(client_id, payload)
|
||||
|
|
|
|||
|
|
@ -209,6 +209,14 @@ class ArduinoCLIService:
|
|||
"""Return True if the FQBN targets an ESP32 family board."""
|
||||
return fqbn.startswith("esp32:")
|
||||
|
||||
def _is_esp32c3_board(self, fqbn: str) -> bool:
|
||||
"""Return True if the FQBN targets an ESP32-C3 (RISC-V) board.
|
||||
|
||||
ESP32-C3 places the bootloader at flash offset 0x0000, unlike Xtensa
|
||||
boards (ESP32, ESP32-S3) which use 0x1000.
|
||||
"""
|
||||
return "esp32c3" in fqbn or "xiao-esp32-c3" in fqbn or "aitewinrobot-esp32c3-supermini" in fqbn
|
||||
|
||||
async def compile(self, files: list[dict], board_fqbn: str = "arduino:avr:uno") -> dict:
|
||||
"""
|
||||
Compile Arduino sketch using arduino-cli.
|
||||
|
|
@ -345,22 +353,25 @@ class ArduinoCLIService:
|
|||
print(f"[ESP32] Build dir contents: {[f.name for f in build_dir.iterdir()]}")
|
||||
|
||||
# Merge individual .bin files into a single 4MB flash image in pure Python.
|
||||
# ESP32 default flash layout: 0x1000 bootloader | 0x8000 partitions | 0x10000 app
|
||||
# Flash layout differs by chip:
|
||||
# ESP32 / ESP32-S3 (Xtensa): 0x1000 bootloader | 0x8000 partitions | 0x10000 app
|
||||
# ESP32-C3 (RISC-V): 0x0000 bootloader | 0x8000 partitions | 0x10000 app
|
||||
# QEMU lcgamboa requires exactly 2/4/8/16 MB flash — raw app binary won't boot.
|
||||
if not merged_file.exists() and bin_file.exists() and bootloader_file.exists() and partitions_file.exists():
|
||||
print("[ESP32] Merging binaries into 4MB flash image (pure Python)...")
|
||||
try:
|
||||
FLASH_SIZE = 4 * 1024 * 1024 # 4 MB
|
||||
flash = bytearray(b'\xff' * FLASH_SIZE)
|
||||
bootloader_offset = 0x0000 if self._is_esp32c3_board(board_fqbn) else 0x1000
|
||||
for offset, path in [
|
||||
(0x1000, bootloader_file),
|
||||
(bootloader_offset, bootloader_file),
|
||||
(0x8000, partitions_file),
|
||||
(0x10000, bin_file),
|
||||
]:
|
||||
data = path.read_bytes()
|
||||
flash[offset:offset + len(data)] = data
|
||||
merged_file.write_bytes(bytes(flash))
|
||||
print(f"[ESP32] Merged image: {merged_file.stat().st_size} bytes")
|
||||
print(f"[ESP32] Merged image: {merged_file.stat().st_size} bytes (bootloader @ 0x{bootloader_offset:04X})")
|
||||
except Exception as e:
|
||||
print(f"[ESP32] Merge failed: {e} — falling back to raw app binary")
|
||||
|
||||
|
|
|
|||
|
|
@ -47,24 +47,38 @@ from typing import Callable, Awaitable
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── Library path detection ────────────────────────────────────────────────────
|
||||
_LIB_NAME = 'libqemu-xtensa.dll' if sys.platform == 'win32' else 'libqemu-xtensa.so'
|
||||
_DEFAULT_LIB = str(pathlib.Path(__file__).parent / _LIB_NAME)
|
||||
_SERVICES_DIR = pathlib.Path(__file__).parent
|
||||
|
||||
# Xtensa library (ESP32, ESP32-S3)
|
||||
_LIB_XTENSA_NAME = 'libqemu-xtensa.dll' if sys.platform == 'win32' else 'libqemu-xtensa.so'
|
||||
_DEFAULT_LIB_XTENSA = str(_SERVICES_DIR / _LIB_XTENSA_NAME)
|
||||
LIB_PATH: str = os.environ.get('QEMU_ESP32_LIB', '') or (
|
||||
_DEFAULT_LIB if os.path.isfile(_DEFAULT_LIB) else ''
|
||||
_DEFAULT_LIB_XTENSA if os.path.isfile(_DEFAULT_LIB_XTENSA) else ''
|
||||
)
|
||||
|
||||
_WORKER_SCRIPT = pathlib.Path(__file__).parent / 'esp32_worker.py'
|
||||
# RISC-V library (ESP32-C3)
|
||||
_LIB_RISCV_NAME = 'libqemu-riscv32.dll' if sys.platform == 'win32' else 'libqemu-riscv32.so'
|
||||
_DEFAULT_LIB_RISCV = str(_SERVICES_DIR / _LIB_RISCV_NAME)
|
||||
LIB_RISCV_PATH: str = os.environ.get('QEMU_RISCV32_LIB', '') or (
|
||||
_DEFAULT_LIB_RISCV if os.path.isfile(_DEFAULT_LIB_RISCV) else ''
|
||||
)
|
||||
|
||||
_WORKER_SCRIPT = _SERVICES_DIR / 'esp32_worker.py'
|
||||
|
||||
EventCallback = Callable[[str, dict], Awaitable[None]]
|
||||
|
||||
# lcgamboa machine names
|
||||
# lcgamboa machine names and which DLL each board requires
|
||||
_MACHINE: dict[str, str] = {
|
||||
'esp32': 'esp32-picsimlab',
|
||||
'esp32-s3': 'esp32s3-picsimlab',
|
||||
'esp32-c3': 'esp32c3-picsimlab',
|
||||
'xiao-esp32-c3': 'esp32c3-picsimlab',
|
||||
'aitewinrobot-esp32c3-supermini': 'esp32c3-picsimlab',
|
||||
}
|
||||
|
||||
# Board types that require the RISC-V library instead of the Xtensa one
|
||||
_RISCV_BOARDS = {'esp32-c3', 'xiao-esp32-c3', 'aitewinrobot-esp32c3-supermini'}
|
||||
|
||||
|
||||
# ── UART buffer ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -127,12 +141,18 @@ class EspLibManager:
|
|||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
"""Returns True if the Xtensa DLL is present (minimum for ESP32/ESP32-S3)."""
|
||||
return (
|
||||
bool(LIB_PATH)
|
||||
and os.path.isfile(LIB_PATH)
|
||||
and _WORKER_SCRIPT.exists()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_riscv_available() -> bool:
|
||||
"""Returns True if the RISC-V DLL is present (required for ESP32-C3)."""
|
||||
return bool(LIB_RISCV_PATH) and os.path.isfile(LIB_RISCV_PATH)
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
async def start_instance(
|
||||
|
|
@ -152,8 +172,9 @@ class EspLibManager:
|
|||
return
|
||||
|
||||
machine = _MACHINE.get(board_type, 'esp32-picsimlab')
|
||||
lib_path = LIB_RISCV_PATH if board_type in _RISCV_BOARDS else LIB_PATH
|
||||
config = json.dumps({
|
||||
'lib_path': LIB_PATH,
|
||||
'lib_path': lib_path,
|
||||
'firmware_b64': firmware_b64,
|
||||
'machine': machine,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ def _log(msg: str) -> None:
|
|||
|
||||
|
||||
# ─── GPIO pinmap (identity: slot i → GPIO i-1) ──────────────────────────────
|
||||
# ESP32 has 40 GPIOs (0-39), ESP32-C3 only has 22 (0-21).
|
||||
# The pinmap is rebuilt after reading config (see main()), defaulting to ESP32.
|
||||
|
||||
_GPIO_COUNT = 40
|
||||
_PINMAP = (ctypes.c_int16 * (_GPIO_COUNT + 1))(
|
||||
|
|
@ -67,6 +69,16 @@ _PINMAP = (ctypes.c_int16 * (_GPIO_COUNT + 1))(
|
|||
*range(_GPIO_COUNT),
|
||||
)
|
||||
|
||||
|
||||
def _build_pinmap(gpio_count: int):
|
||||
"""Build a pinmap array for the given GPIO count."""
|
||||
global _GPIO_COUNT, _PINMAP
|
||||
_GPIO_COUNT = gpio_count
|
||||
_PINMAP = (ctypes.c_int16 * (gpio_count + 1))(
|
||||
gpio_count,
|
||||
*range(gpio_count),
|
||||
)
|
||||
|
||||
# ─── ctypes callback types ───────────────────────────────────────────────────
|
||||
|
||||
_WRITE_PIN = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int)
|
||||
|
|
@ -165,6 +177,10 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
firmware_b64 = cfg['firmware_b64']
|
||||
machine = cfg.get('machine', 'esp32-picsimlab')
|
||||
|
||||
# Adjust GPIO pinmap based on chip: ESP32-C3 has only 22 GPIOs
|
||||
if 'c3' in machine:
|
||||
_build_pinmap(22)
|
||||
|
||||
# ── 2. Load DLL ───────────────────────────────────────────────────────────
|
||||
_MINGW64_BIN = r'C:\msys64\mingw64\bin'
|
||||
if os.name == 'nt' and os.path.isdir(_MINGW64_BIN):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,253 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
test_esp32c3_emulation.py — Direct test of ESP32-C3 emulation via libqemu-riscv32.
|
||||
|
||||
Steps:
|
||||
1. Compile a minimal blink sketch for ESP32-C3 via arduino-cli
|
||||
2. Merge .bin files into a 4 MB flash image
|
||||
3. Launch esp32_worker.py with machine=esp32c3-picsimlab
|
||||
4. Wait for gpio_change events on GPIO8 (blink period ~500ms)
|
||||
5. PASS if at least 2 toggles seen within 30 s, FAIL otherwise
|
||||
|
||||
Run from the backend directory:
|
||||
python test_esp32c3_emulation.py
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# ── paths ────────────────────────────────────────────────────────────────────
|
||||
BACKEND_DIR = Path(__file__).parent
|
||||
SERVICES_DIR = BACKEND_DIR / 'app' / 'services'
|
||||
WORKER = SERVICES_DIR / 'esp32_worker.py'
|
||||
LIB_PATH = SERVICES_DIR / 'libqemu-riscv32.dll'
|
||||
|
||||
BLINK_SKETCH = """\
|
||||
#define LED_PIN 8
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
Serial.println("ESP32-C3 blink test started");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
Serial.println("LED ON");
|
||||
delay(500);
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
Serial.println("LED OFF");
|
||||
delay(500);
|
||||
}
|
||||
"""
|
||||
|
||||
FQBN = 'esp32:esp32:esp32c3:FlashMode=dio'
|
||||
TIMEOUT_S = 45 # seconds to wait for GPIO toggles
|
||||
MIN_TOGGLES = 2 # minimum gpio_change events expected
|
||||
|
||||
|
||||
def _fail(msg: str) -> None:
|
||||
print(f'\nFAIL: {msg}')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _ok(msg: str) -> None:
|
||||
print(f'OK {msg}')
|
||||
|
||||
|
||||
# ── Step 1: compile ───────────────────────────────────────────────────────────
|
||||
|
||||
def compile_sketch(sketch_dir: Path, build_dir: Path) -> Path:
|
||||
print('\n=== Step 1: Compiling ESP32-C3 blink sketch ===')
|
||||
sketch_file = sketch_dir / 'sketch.ino'
|
||||
sketch_file.write_text(BLINK_SKETCH)
|
||||
|
||||
cmd = [
|
||||
'arduino-cli', 'compile',
|
||||
'--fqbn', FQBN,
|
||||
'--build-property', 'build.extra_flags=-DARDUINO_ESP32_LCGAMBOA=1',
|
||||
'--output-dir', str(build_dir),
|
||||
str(sketch_dir),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print('STDOUT:', result.stdout)
|
||||
print('STDERR:', result.stderr)
|
||||
_fail('arduino-cli compilation failed')
|
||||
|
||||
_ok(f'Compilation successful (rc={result.returncode})')
|
||||
return build_dir / 'sketch.ino.bin'
|
||||
|
||||
|
||||
# ── Step 2: merge flash image ─────────────────────────────────────────────────
|
||||
|
||||
def merge_flash_image(build_dir: Path) -> bytes:
|
||||
"""Merge bootloader + partition table + app into a 4 MB flash image."""
|
||||
print('\n=== Step 2: Merging flash image ===')
|
||||
FLASH_SIZE = 4 * 1024 * 1024
|
||||
image = bytearray(b'\xFF' * FLASH_SIZE)
|
||||
|
||||
parts = [
|
||||
(0x00000, build_dir / 'sketch.ino.bootloader.bin'),
|
||||
(0x08000, build_dir / 'sketch.ino.partitions.bin'),
|
||||
(0x10000, build_dir / 'sketch.ino.bin'),
|
||||
]
|
||||
for offset, path in parts:
|
||||
if not path.exists():
|
||||
_fail(f'Missing file: {path}')
|
||||
data = path.read_bytes()
|
||||
image[offset:offset + len(data)] = data
|
||||
_ok(f'{path.name} -> 0x{offset:05X} ({len(data)} bytes)')
|
||||
|
||||
_ok(f'Merged image: {len(image)} bytes')
|
||||
return bytes(image)
|
||||
|
||||
|
||||
# ── Step 3+4: run worker and watch for GPIO events ────────────────────────────
|
||||
|
||||
def run_worker_and_check_gpio(firmware_bytes: bytes) -> None:
|
||||
print('\n=== Step 3: Launching esp32_worker.py ===')
|
||||
|
||||
if not WORKER.exists():
|
||||
_fail(f'Worker not found: {WORKER}')
|
||||
if not LIB_PATH.exists():
|
||||
_fail(f'DLL not found: {LIB_PATH}')
|
||||
|
||||
firmware_b64 = base64.b64encode(firmware_bytes).decode()
|
||||
cfg = {
|
||||
'lib_path': str(LIB_PATH),
|
||||
'firmware_b64': firmware_b64,
|
||||
'machine': 'esp32c3-picsimlab',
|
||||
}
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(WORKER)],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=False,
|
||||
)
|
||||
|
||||
# Send config on stdin
|
||||
cfg_line = (json.dumps(cfg) + '\n').encode()
|
||||
proc.stdin.write(cfg_line)
|
||||
proc.stdin.flush()
|
||||
|
||||
print(f'Worker PID {proc.pid} started, waiting up to {TIMEOUT_S}s for GPIO toggles on GPIO8...')
|
||||
print('(All worker stderr will be shown below)\n')
|
||||
|
||||
# ── Read stdout in a non-blocking fashion ────────────────────────────────
|
||||
import threading
|
||||
|
||||
gpio8_states: list[int] = []
|
||||
uart_output: list[str] = []
|
||||
errors: list[str] = []
|
||||
all_events: list[dict] = []
|
||||
|
||||
def _read_stdout() -> None:
|
||||
for raw_line in proc.stdout:
|
||||
line = raw_line.decode('utf-8', errors='replace').strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
evt = json.loads(line)
|
||||
except Exception:
|
||||
print(f' [worker non-JSON] {line}')
|
||||
continue
|
||||
all_events.append(evt)
|
||||
t = evt.get('type')
|
||||
if t == 'gpio_change':
|
||||
pin = evt.get('pin')
|
||||
state = evt.get('state')
|
||||
if pin == 8:
|
||||
gpio8_states.append(state)
|
||||
print(f' [GPIO8] -> {"HIGH" if state else "LOW"} (toggle #{len(gpio8_states)})')
|
||||
elif t == 'uart_tx':
|
||||
ch = chr(evt.get('byte', 0))
|
||||
uart_output.append(ch)
|
||||
line_so_far = ''.join(uart_output)
|
||||
if line_so_far.endswith('\n'):
|
||||
print(f' [UART0] {"".join(uart_output).rstrip()}')
|
||||
uart_output.clear()
|
||||
elif t == 'system':
|
||||
print(f' [system] {evt.get("event")}')
|
||||
elif t == 'error':
|
||||
msg = evt.get('message', '')
|
||||
errors.append(msg)
|
||||
print(f' [ERROR] {msg}')
|
||||
|
||||
def _read_stderr() -> None:
|
||||
for raw_line in proc.stderr:
|
||||
print(f' [stderr] {raw_line.decode("utf-8", errors="replace").rstrip()}')
|
||||
|
||||
t_out = threading.Thread(target=_read_stdout, daemon=True)
|
||||
t_err = threading.Thread(target=_read_stderr, daemon=True)
|
||||
t_out.start()
|
||||
t_err.start()
|
||||
|
||||
# Wait up to TIMEOUT_S seconds or until we have MIN_TOGGLES
|
||||
deadline = time.monotonic() + TIMEOUT_S
|
||||
while time.monotonic() < deadline:
|
||||
if len(gpio8_states) >= MIN_TOGGLES:
|
||||
break
|
||||
if proc.poll() is not None:
|
||||
print(f' Worker exited early with rc={proc.returncode}')
|
||||
break
|
||||
time.sleep(0.2)
|
||||
|
||||
# Terminate worker
|
||||
try:
|
||||
proc.stdin.write((json.dumps({'cmd': 'stop'}) + '\n').encode())
|
||||
proc.stdin.flush()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
|
||||
print(f'\n=== Step 4: Results ===')
|
||||
print(f' GPIO8 toggles observed : {len(gpio8_states)}')
|
||||
print(f' UART output chars : {len("".join(uart_output))}')
|
||||
print(f' Total events : {len(all_events)}')
|
||||
print(f' Errors : {errors}')
|
||||
|
||||
if errors and not gpio8_states:
|
||||
_fail(f'Worker reported errors and no GPIO activity: {errors}')
|
||||
if len(gpio8_states) < MIN_TOGGLES:
|
||||
_fail(
|
||||
f'Expected at least {MIN_TOGGLES} GPIO8 toggles within {TIMEOUT_S}s, '
|
||||
f'got {len(gpio8_states)}.\n'
|
||||
f' First 10 events: {all_events[:10]}'
|
||||
)
|
||||
|
||||
_ok(f'GPIO8 toggled {len(gpio8_states)} times — ESP32-C3 emulation is working!')
|
||||
|
||||
|
||||
# ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> None:
|
||||
print('=' * 60)
|
||||
print('ESP32-C3 EMULATION END-TO-END TEST')
|
||||
print('=' * 60)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sketch_dir = Path(tmpdir) / 'sketch'
|
||||
build_dir = Path(tmpdir) / 'build'
|
||||
sketch_dir.mkdir()
|
||||
build_dir.mkdir()
|
||||
|
||||
compile_sketch(sketch_dir, build_dir)
|
||||
firmware_bytes = merge_flash_image(build_dir)
|
||||
run_worker_and_check_gpio(firmware_bytes)
|
||||
|
||||
print('\nPASS: ESP32-C3 emulation test PASSED')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
> Available on: **Windows** (`.dll`) · **Linux / Docker** (`.so`, included in the official image)
|
||||
> Applies to: **ESP32, ESP32-S3** (Xtensa LX6/LX7 architecture)
|
||||
|
||||
> **Note on ESP32-C3:** The ESP32-C3, XIAO ESP32-C3, and ESP32-C3 SuperMini boards use the **RISC-V RV32IMC** architecture and have their own in-browser emulator. See → [RISCV_EMULATION.md](./RISCV_EMULATION.md)
|
||||
> **Note on ESP32-C3:** The ESP32-C3, XIAO ESP32-C3, and ESP32-C3 SuperMini boards use the **RISC-V RV32IMC** architecture and are emulated via `libqemu-riscv32` (same backend pattern as Xtensa, different library and machine). See → [RISCV_EMULATION.md](./RISCV_EMULATION.md)
|
||||
|
||||
## Supported Boards
|
||||
|
||||
|
|
@ -376,15 +376,18 @@ libglib2.0-0, libgcrypt20, libslirp0, libpixman-1-0
|
|||
**Required ROM binaries** (in the same folder as the lib):
|
||||
```
|
||||
# Windows (backend/app/services/):
|
||||
libqemu-xtensa.dll ← main engine (not in git — 43 MB)
|
||||
libqemu-xtensa.dll ← Xtensa engine for ESP32/S3 (not in git — 43 MB)
|
||||
libqemu-riscv32.dll ← RISC-V engine for ESP32-C3 (not in git — 58 MB)
|
||||
esp32-v3-rom.bin ← ESP32 boot ROM (not in git — 446 KB)
|
||||
esp32-v3-rom-app.bin ← application ROM (not in git — 446 KB)
|
||||
esp32c3-rom.bin ← ESP32-C3 boot ROM (not in git — 384 KB)
|
||||
|
||||
# Docker (/app/lib/):
|
||||
libqemu-xtensa.so ← compiled in Stage 0 of the Dockerfile
|
||||
libqemu-riscv32.so ← ESP32-C3 (RISC-V)
|
||||
libqemu-riscv32.so ← ESP32-C3 (RISC-V) — same build stage
|
||||
esp32-v3-rom.bin ← copied from the lcgamboa repo's pc-bios/
|
||||
esp32-v3-rom-app.bin
|
||||
esp32c3-rom.bin ← ESP32-C3 ROM
|
||||
```
|
||||
|
||||
> On Windows these files are in `.gitignore` due to their size. Each developer generates them locally.
|
||||
|
|
@ -791,13 +794,15 @@ The original problem was that `ledc_update {channel: N}` arrived at the frontend
|
|||
|
||||
### 10.1 Windows (MSYS2 MINGW64)
|
||||
|
||||
#### Xtensa (ESP32 / ESP32-S3)
|
||||
|
||||
The `build_libqemu-esp32-win.sh` script in `wokwi-libs/qemu-lcgamboa/` automates the process:
|
||||
|
||||
```bash
|
||||
# In MSYS2 MINGW64:
|
||||
cd wokwi-libs/qemu-lcgamboa
|
||||
bash build_libqemu-esp32-win.sh
|
||||
# Produces: build/libqemu-xtensa.dll and build/libqemu-riscv32.dll
|
||||
# Produces: build/libqemu-xtensa.dll
|
||||
```
|
||||
|
||||
The script configures QEMU with `--extra-cflags=-fPIC` (required for Windows/PE with ASLR), compiles the full binary, and then relinks removing `softmmu_main.c.obj` (which contains `main()`):
|
||||
|
|
@ -810,6 +815,41 @@ cc -m64 -mcx16 -shared \
|
|||
@dll_link.rsp # all .obj files except softmmu_main
|
||||
```
|
||||
|
||||
#### RISC-V (ESP32-C3)
|
||||
|
||||
Building `libqemu-riscv32.dll` requires a **separate build directory** because the configure flags differ from Xtensa (notably `--disable-slirp`, which is required because GCC 15.x rejects incompatible pointer types in `net/slirp.c` for the riscv32 target):
|
||||
|
||||
```bash
|
||||
# In MSYS2 MINGW64:
|
||||
cd wokwi-libs/qemu-lcgamboa
|
||||
mkdir build-riscv && cd build-riscv
|
||||
|
||||
../configure \
|
||||
--target-list=riscv32-softmmu \
|
||||
--disable-werror \
|
||||
--enable-gcrypt \
|
||||
--disable-slirp \
|
||||
--without-default-features \
|
||||
--disable-docs
|
||||
|
||||
ninja # ~15-30 min first time
|
||||
|
||||
# Once built, create the DLL using the keeprsp technique:
|
||||
# 1. Build the full executable first
|
||||
ninja qemu-system-riscv32.exe
|
||||
# 2. Edit build/riscv32-softmmu/dll_link.rsp:
|
||||
# - Change -o qemu-system-riscv32.exe → -o libqemu-riscv32.dll
|
||||
# - Add -shared flag
|
||||
# - Remove softmmu_main.c.obj from the object list
|
||||
gcc -shared -o libqemu-riscv32.dll @dll_link.rsp
|
||||
|
||||
# Deploy to backend:
|
||||
cp libqemu-riscv32.dll /e/Hardware/wokwi_clon/backend/app/services/
|
||||
cp ../pc-bios/esp32c3-rom.bin /e/Hardware/wokwi_clon/backend/app/services/
|
||||
```
|
||||
|
||||
See [RISCV_EMULATION.md §4](./RISCV_EMULATION.md) for full step-by-step instructions.
|
||||
|
||||
### 10.2 Linux
|
||||
|
||||
The `build_libqemu-esp32.sh` script produces a `.so`:
|
||||
|
|
@ -1050,7 +1090,8 @@ The connection logic lives in `SimulatorCanvas.tsx`: it detects the tag of the w
|
|||
|
||||
| Variable | Example Value | Effect |
|
||||
|----------|--------------|--------|
|
||||
| `QEMU_ESP32_LIB` | `/app/lib/libqemu-xtensa.so` | Force lib path (overrides auto-detect) |
|
||||
| `QEMU_ESP32_LIB` | `/app/lib/libqemu-xtensa.so` | Force Xtensa lib path (ESP32/S3) |
|
||||
| `QEMU_RISCV32_LIB` | `/app/lib/libqemu-riscv32.so` | Force RISC-V lib path (ESP32-C3) |
|
||||
| `QEMU_ESP32_BINARY` | `/usr/bin/qemu-system-xtensa` | Subprocess fallback (without lib) |
|
||||
| `SKIP_LIB_INTEGRATION` | `1` | Skip QEMU integration tests in pytest |
|
||||
|
||||
|
|
@ -1058,9 +1099,10 @@ The connection logic lives in `SimulatorCanvas.tsx`: it detects the tag of the w
|
|||
|
||||
| Platform | Library auto-searched |
|
||||
|----------|-----------------------|
|
||||
| Docker / Linux | `/app/lib/libqemu-xtensa.so` (via `QEMU_ESP32_LIB`) |
|
||||
| Windows | `backend/app/services/libqemu-xtensa.dll` |
|
||||
| Custom | `$QEMU_ESP32_LIB` (if set, takes priority) |
|
||||
| Docker / Linux | `/app/lib/libqemu-xtensa.so` (Xtensa) + `/app/lib/libqemu-riscv32.so` (RISC-V) |
|
||||
| Windows | `backend/app/services/libqemu-xtensa.dll` + `backend/app/services/libqemu-riscv32.dll` |
|
||||
| Custom Xtensa | `$QEMU_ESP32_LIB` (if set, takes priority) |
|
||||
| Custom RISC-V | `$QEMU_RISCV32_LIB` (if set, takes priority) |
|
||||
|
||||
**Startup examples:**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# RISC-V Emulation (ESP32-C3 / XIAO-C3 / C3 SuperMini)
|
||||
|
||||
> Status: **Functional** · In-browser emulation · No backend dependencies
|
||||
> Engine: **RiscVCore (RV32IMC)** — implemented in TypeScript
|
||||
> Platform: **ESP32-C3 @ 160 MHz** — 32-bit RISC-V architecture
|
||||
> Status: **Functional** · Backend QEMU (`libqemu-riscv32`) — same pattern as ESP32/ESP32-S3
|
||||
> Engine: **QEMU lcgamboa — riscv32-softmmu** compiled with `esp32c3-picsimlab` machine
|
||||
> Platform: **ESP32-C3 @ 160 MHz** — 32-bit RISC-V RV32IMC architecture
|
||||
|
||||
> **Unit-test / ISA layer:** `RiscVCore.ts` and `Esp32C3Simulator.ts` are TypeScript implementations
|
||||
> used exclusively for Vitest unit tests; they are **not** the production emulation path.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -10,39 +13,45 @@
|
|||
|
||||
1. [Overview](#1-overview)
|
||||
2. [Supported Boards](#2-supported-boards)
|
||||
3. [Emulator Architecture](#3-emulator-architecture)
|
||||
4. [Emulated Memory and Peripherals](#4-emulated-memory-and-peripherals)
|
||||
5. [Full Flow: Compile and Run](#5-full-flow-compile-and-run)
|
||||
6. [ESP32 Image Format](#6-esp32-image-format)
|
||||
7. [Supported ISA — RV32IMC](#7-supported-isa--rv32imc)
|
||||
8. [GPIO](#8-gpio)
|
||||
9. [UART0 — Serial Monitor](#9-uart0--serial-monitor)
|
||||
10. [Limitations](#10-limitations)
|
||||
11. [Tests](#11-tests)
|
||||
12. [Differences vs Xtensa Emulation (ESP32 / ESP32-S3)](#12-differences-vs-xtensa-emulation-esp32--esp32-s3)
|
||||
13. [Key Files](#13-key-files)
|
||||
3. [Architecture — QEMU Backend Path](#3-architecture--qemu-backend-path)
|
||||
4. [Setup on Windows — Building `libqemu-riscv32.dll`](#4-setup-on-windows--building-libqemu-riscv32dll)
|
||||
5. [Setup on Linux / Docker](#5-setup-on-linux--docker)
|
||||
6. [GPIO Pinmap — 22 GPIOs](#6-gpio-pinmap--22-gpios)
|
||||
7. [Full Flow: Compile and Run](#7-full-flow-compile-and-run)
|
||||
8. [ESP32 Image Format](#8-esp32-image-format)
|
||||
9. [Supported ISA — RV32IMC](#9-supported-isa--rv32imc)
|
||||
10. [GPIO — MMIO Registers](#10-gpio--mmio-registers)
|
||||
11. [UART0 — Serial Monitor](#11-uart0--serial-monitor)
|
||||
12. [Limitations of the riscv32 QEMU machine](#12-limitations-of-the-riscv32-qemu-machine)
|
||||
13. [Tests](#13-tests)
|
||||
14. [Differences vs Xtensa Emulation (ESP32 / ESP32-S3)](#14-differences-vs-xtensa-emulation-esp32--esp32-s3)
|
||||
15. [Key Files](#15-key-files)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Boards based on **ESP32-C3** use Espressif's **ESP32-C3** processor, which implements the **RISC-V RV32IMC** architecture (32-bit, Multiply, Compressed instructions). Unlike the ESP32 and ESP32-S3 (Xtensa LX6/LX7), the C3 **does not require QEMU or a backend** to be emulated.
|
||||
Boards based on **ESP32-C3** use Espressif's **ESP32-C3** processor, implementing the **RISC-V RV32IMC** architecture. In production the system uses the same QEMU backend pattern as ESP32/ESP32-S3, but with a different library (`libqemu-riscv32`) and a different machine target (`esp32c3-picsimlab`).
|
||||
|
||||
> The browser-side TypeScript emulator (`RiscVCore.ts` + `Esp32C3Simulator.ts`) cannot handle the 150+ ROM functions that ESP-IDF needs during initialization. All production runs go through the QEMU backend. The TypeScript layer is kept as unit-test infrastructure for the RV32IMC ISA.
|
||||
|
||||
### Emulation Engine Comparison
|
||||
|
||||
| Board | CPU | Engine |
|
||||
| ----- | --- | ------ |
|
||||
| ESP32, ESP32-S3 | Xtensa LX6/LX7 | QEMU lcgamboa (backend WebSocket) |
|
||||
| **ESP32-C3, XIAO-C3, C3 SuperMini** | **RV32IMC @ 160 MHz** | **RiscVCore.ts (browser, no backend)** |
|
||||
| Arduino Uno/Nano/Mega | AVR ATmega | avr8js (browser) |
|
||||
| Raspberry Pi Pico | RP2040 | rp2040js (browser) |
|
||||
| Board | CPU | Production Engine | Unit-Test Engine |
|
||||
| ----- | --- | ----------------- | ---------------- |
|
||||
| ESP32, ESP32-S3 | Xtensa LX6/LX7 | QEMU lcgamboa `libqemu-xtensa` | — |
|
||||
| **ESP32-C3, XIAO-C3, C3 SuperMini** | **RV32IMC @ 160 MHz** | **QEMU lcgamboa `libqemu-riscv32`** | RiscVCore.ts (Vitest) |
|
||||
| Arduino Uno/Nano/Mega | AVR ATmega | avr8js (browser) | — |
|
||||
| Raspberry Pi Pico | RP2040 | rp2040js (browser) | — |
|
||||
|
||||
### Advantages of the JS Emulator
|
||||
### Key differences vs Xtensa (ESP32)
|
||||
|
||||
- **No network dependencies** — works offline, no WebSocket connection to the backend
|
||||
- **Instant startup** — no QEMU process to launch (0 ms latency)
|
||||
- **Testable with Vitest** — the same TypeScript code that runs in production can be tested in CI
|
||||
- **Cross-platform** — works the same on Windows, macOS, Linux, and Docker
|
||||
- Different library: `libqemu-riscv32.dll/.so` instead of `libqemu-xtensa`
|
||||
- Different machine: `esp32c3-picsimlab` instead of `esp32-picsimlab`
|
||||
- 22 GPIOs (GPIO 0–21) instead of 40; worker auto-adjusts pinmap
|
||||
- ROM file: `esp32c3-rom.bin` (384 KB) instead of `esp32-v3-rom.bin`
|
||||
- WiFi, LEDC/PWM, RMT/NeoPixel: **not yet emulated** in the riscv32 machine
|
||||
- Build flag: `--disable-slirp` required (riscv32 target has incompatible pointer types in `net/slirp.c`)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -64,93 +73,243 @@ Boards based on **ESP32-C3** use Espressif's **ESP32-C3** processor, which imple
|
|||
|
||||
---
|
||||
|
||||
## 3. Emulator Architecture
|
||||
## 3. Architecture — QEMU Backend Path
|
||||
|
||||
```text
|
||||
Arduino Sketch (.ino)
|
||||
User (browser)
|
||||
└── WebSocket (/ws/{client_id})
|
||||
└── simulation.py (FastAPI router)
|
||||
└── EspLibManager
|
||||
│
|
||||
▼ arduino-cli (backend)
|
||||
sketch.ino.bin ← ESP32 image format (IROM/DRAM/IRAM segments)
|
||||
board_type in _RISCV_BOARDS?
|
||||
├── YES → lib_path = LIB_RISCV_PATH (libqemu-riscv32.dll/.so)
|
||||
│ machine = 'esp32c3-picsimlab'
|
||||
│ _build_pinmap(22) ← 22 GPIOs, not 40
|
||||
└── NO → lib_path = LIB_PATH (libqemu-xtensa.dll/.so)
|
||||
machine = 'esp32-picsimlab' or 'esp32s3-picsimlab'
|
||||
pinmap = 40 GPIOs
|
||||
│
|
||||
▼ base64 → frontend
|
||||
compileBoardProgram(boardId, base64)
|
||||
esp32_worker.py (subprocess)
|
||||
│
|
||||
▼ Esp32C3Simulator.loadFlashImage(base64)
|
||||
parseMergedFlashImage() ← reads segments from the 4MB image
|
||||
ctypes.CDLL(libqemu-riscv32.dll)
|
||||
│
|
||||
├── IROM segment → flash buffer (0x42000000)
|
||||
├── DROM segment → flash buffer (0x3C000000, alias)
|
||||
├── DRAM segment → dram buffer (0x3FC80000)
|
||||
└── IRAM segment → iram buffer (0x4037C000)
|
||||
Machine: esp32c3-picsimlab
|
||||
CPU: RISC-V RV32IMC @ 160 MHz
|
||||
│
|
||||
▼ core.reset(entryPoint)
|
||||
RiscVCore.step() ← requestAnimationFrame @ 60 FPS
|
||||
│ 2,666,667 cycles/frame (160 MHz ÷ 60)
|
||||
├── MMIO GPIO_W1TS/W1TC → onPinChangeWithTime → visual components
|
||||
└── MMIO UART0 FIFO → onSerialData → Serial Monitor
|
||||
┌──────────┴──────────┐
|
||||
ESP32-C3 core emulated peripherals
|
||||
(single core) GPIO (22 pins) · UART0 · SPI Flash
|
||||
```
|
||||
|
||||
### Main Classes
|
||||
**Required files (same directory as the lib):**
|
||||
|
||||
| Class | File | Responsibility |
|
||||
| ----- | ---- | -------------- |
|
||||
| `RiscVCore` | `simulation/RiscVCore.ts` | RV32IMC decoder/executor, generic MMIO |
|
||||
| `Esp32C3Simulator` | `simulation/Esp32C3Simulator.ts` | ESP32-C3 memory map, GPIO, UART0, RAF loop |
|
||||
| `parseMergedFlashImage` | `utils/esp32ImageParser.ts` | ESP32 image format parsing (segments, entry point) |
|
||||
| File | Size | Source |
|
||||
|------|------|--------|
|
||||
| `libqemu-riscv32.dll` | ~58 MB | Compiled from `wokwi-libs/qemu-lcgamboa` (see §4) |
|
||||
| `esp32c3-rom.bin` | 384 KB | `wokwi-libs/qemu-lcgamboa/pc-bios/esp32c3-rom.bin` |
|
||||
|
||||
### TypeScript / Browser layer (unit tests only)
|
||||
|
||||
The `RiscVCore.ts` + `Esp32C3Simulator.ts` classes exist for **Vitest unit tests only**. They provide a fast, offline RV32IMC interpreter that can run bare-metal binaries. They cannot handle the full ESP-IDF initialization sequence needed by real Arduino sketches.
|
||||
|
||||
| Class | File | Used in |
|
||||
| ----- | ---- | ------- |
|
||||
| `RiscVCore` | `simulation/RiscVCore.ts` | Vitest ISA unit tests |
|
||||
| `Esp32C3Simulator` | `simulation/Esp32C3Simulator.ts` | Vitest end-to-end tests |
|
||||
| `parseMergedFlashImage` | `utils/esp32ImageParser.ts` | Vitest + compile flow |
|
||||
|
||||
---
|
||||
|
||||
## 4. Emulated Memory and Peripherals
|
||||
## 4. Setup on Windows — Building `libqemu-riscv32.dll`
|
||||
|
||||
### Memory Map
|
||||
This section covers building the RISC-V QEMU library from source on Windows with MSYS2.
|
||||
|
||||
| Region | Base Address | Size | Description |
|
||||
| ------ | ------------ | ---- | ----------- |
|
||||
| Flash IROM | `0x42000000` | 4 MB | Executable code (core's main buffer) |
|
||||
| Flash DROM | `0x3C000000` | 4 MB | Read-only data (alias of the same buffer) |
|
||||
| DRAM | `0x3FC80000` | 384 KB | Data RAM (stack, global variables) |
|
||||
| IRAM | `0x4037C000` | 384 KB | Instruction RAM (ISR, time-critical code) |
|
||||
| UART0 | `0x60000000` | 1 KB | Serial port 0 |
|
||||
| GPIO | `0x60004000` | 512 B | GPIO registers |
|
||||
### 4.1 Prerequisites
|
||||
|
||||
### GPIO — Implemented Registers
|
||||
Same as the Xtensa build (see [ESP32_EMULATION.md §1.1–1.4](./ESP32_EMULATION.md)), except:
|
||||
- `--disable-slirp` is **required** — `net/slirp.c` has incompatible pointer types in the riscv32-softmmu target that cause a compile error with GCC 15.x.
|
||||
- `--enable-gcrypt` is **required** — matches the working Xtensa DLL linking pattern and avoids GCC emutls/pthread crash on Windows.
|
||||
|
||||
| Register | Offset | Function |
|
||||
| -------- | ------ | -------- |
|
||||
| `GPIO_OUT_REG` | `+0x04` | Read/write output state of all pins |
|
||||
| `GPIO_OUT_W1TS_REG` | `+0x08` | **Set bits** — drive pins HIGH (write-only) |
|
||||
| `GPIO_OUT_W1TC_REG` | `+0x0C` | **Clear bits** — drive pins LOW (write-only) |
|
||||
| `GPIO_IN_REG` | `+0x3C` | Read input state of pins |
|
||||
| `GPIO_ENABLE_REG` | `+0x20` | Pin direction (always returns `0xFF`) |
|
||||
Install MSYS2 dependencies (same as Xtensa, **without** `libslirp`):
|
||||
|
||||
Covers **GPIO 0–21** (all available on ESP32-C3).
|
||||
```bash
|
||||
pacman -S \
|
||||
mingw-w64-x86_64-gcc \
|
||||
mingw-w64-x86_64-glib2 \
|
||||
mingw-w64-x86_64-libgcrypt \
|
||||
mingw-w64-x86_64-pixman \
|
||||
mingw-w64-x86_64-ninja \
|
||||
mingw-w64-x86_64-meson \
|
||||
mingw-w64-x86_64-python \
|
||||
mingw-w64-x86_64-pkg-config \
|
||||
git diffutils
|
||||
```
|
||||
|
||||
### UART0 — Implemented Registers
|
||||
### 4.2 Configure and Build
|
||||
|
||||
| Register | Offset | Function |
|
||||
| -------- | ------ | -------- |
|
||||
| `UART_FIFO_REG` | `+0x00` | Write TX byte / read RX byte |
|
||||
| `UART_STATUS_REG` | `+0x1C` | FIFO status (always returns `0` = ready) |
|
||||
```bash
|
||||
# In MSYS2 MINGW64:
|
||||
cd /e/Hardware/wokwi_clon/wokwi-libs/qemu-lcgamboa
|
||||
|
||||
RX byte reading is available to simulate input from the Serial Monitor.
|
||||
mkdir build-riscv && cd build-riscv
|
||||
|
||||
### Peripherals NOT Emulated (return 0 on read)
|
||||
../configure \
|
||||
--target-list=riscv32-softmmu \
|
||||
--disable-werror \
|
||||
--disable-alsa \
|
||||
--enable-tcg \
|
||||
--enable-system \
|
||||
--enable-gcrypt \
|
||||
--disable-slirp \
|
||||
--enable-iconv \
|
||||
--without-default-features \
|
||||
--disable-docs
|
||||
|
||||
- Interrupt Matrix (`0x600C2000`)
|
||||
- System / Clock (`0x600C0000`, `0x60008000`)
|
||||
- Cache controller (`0x600C4000`)
|
||||
- Timer Group 0/1
|
||||
- SPI flash controller
|
||||
- BLE / WiFi MAC
|
||||
- ADC / DAC
|
||||
ninja # compiles ~1430 objects, takes 15-30 min first time
|
||||
```
|
||||
|
||||
> These peripherals return `0` by default. Code that depends on them may not function correctly (see [Limitations](#10-limitations)).
|
||||
> **Why `--disable-slirp`?** GCC 15.x rejects the `net/slirp.c` code in the riscv32 machine due to incompatible pointer types in `slirp_smb_init`. Adding `-fpermissive` or `-Wno-incompatible-pointer-types` is also an option, but `--disable-slirp` is cleaner since this machine does not need network emulation.
|
||||
|
||||
### 4.3 Create the DLL (keeprsp trick)
|
||||
|
||||
QEMU's build system produces an executable (`qemu-system-riscv32.exe`) rather than a shared library. To get a DLL, relink all objects and exclude `softmmu_main.c.obj` (which contains `main()`):
|
||||
|
||||
```bash
|
||||
# From build-riscv/:
|
||||
|
||||
# 1. Build the full exe first to generate the link response file:
|
||||
ninja qemu-system-riscv32.exe
|
||||
|
||||
# 2. Capture the link command from ninja's database:
|
||||
ninja -t commands qemu-system-riscv32.exe | tail -1 > dll_link.rsp
|
||||
|
||||
# 3. Edit dll_link.rsp:
|
||||
# - Replace -o qemu-system-riscv32.exe with: -shared -o libqemu-riscv32.dll
|
||||
# - Remove the softmmu_main.c.obj entry
|
||||
# - Add: -Wl,--export-all-symbols -Wl,--allow-multiple-definition
|
||||
|
||||
# 4. Relink:
|
||||
gcc @dll_link.rsp
|
||||
|
||||
# 5. Verify:
|
||||
ls -lh libqemu-riscv32.dll # should be ~58 MB
|
||||
objdump -p libqemu-riscv32.dll | grep qemu_picsimlab
|
||||
```
|
||||
|
||||
### 4.4 Copy to Backend
|
||||
|
||||
```bash
|
||||
cp libqemu-riscv32.dll /e/Hardware/wokwi_clon/backend/app/services/
|
||||
cp ../pc-bios/esp32c3-rom.bin /e/Hardware/wokwi_clon/backend/app/services/
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
```bash
|
||||
ls -lh /e/Hardware/wokwi_clon/backend/app/services/libqemu-riscv32.dll
|
||||
ls -lh /e/Hardware/wokwi_clon/backend/app/services/esp32c3-rom.bin
|
||||
# libqemu-riscv32.dll ~58 MB
|
||||
# esp32c3-rom.bin 384 KB
|
||||
```
|
||||
|
||||
### 4.5 Verify DLL Loads
|
||||
|
||||
```python
|
||||
# Run from backend/ with venv active:
|
||||
python -c "
|
||||
import ctypes, os
|
||||
os.add_dll_directory(r'C:\msys64\mingw64\bin')
|
||||
lib = ctypes.CDLL(r'app/services/libqemu-riscv32.dll')
|
||||
print('qemu_init:', lib.qemu_init)
|
||||
print('qemu_picsimlab_register_callbacks:', lib.qemu_picsimlab_register_callbacks)
|
||||
print('OK — DLL loaded successfully')
|
||||
"
|
||||
```
|
||||
|
||||
### 4.6 Verify Emulation End-to-End
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python test_esp32c3_emulation.py
|
||||
# Expected: PASS — GPIO8 toggled 2 times — ESP32-C3 emulation is working!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Full Flow: Compile and Run
|
||||
## 5. Setup on Linux / Docker
|
||||
|
||||
### 5.1 Compile the Sketch
|
||||
### 5.1 Docker (automatic)
|
||||
|
||||
The Dockerfile should compile `libqemu-riscv32.so` alongside `libqemu-xtensa.so`. Add the riscv32 target to the configure step:
|
||||
|
||||
```dockerfile
|
||||
RUN cd /tmp/qemu-lcgamboa && ../configure \
|
||||
--target-list=xtensa-softmmu,riscv32-softmmu \
|
||||
--enable-gcrypt \
|
||||
--disable-slirp \
|
||||
... \
|
||||
&& ninja \
|
||||
&& cp build/libqemu-xtensa.so /app/lib/ \
|
||||
&& cp build/libqemu-riscv32.so /app/lib/ \
|
||||
&& cp pc-bios/esp32c3-rom.bin /app/lib/
|
||||
```
|
||||
|
||||
### 5.2 Linux (manual)
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libglib2.0-dev libgcrypt20-dev libpixman-1-dev libfdt-dev
|
||||
|
||||
cd wokwi-libs/qemu-lcgamboa
|
||||
mkdir build-riscv && cd build-riscv
|
||||
|
||||
../configure \
|
||||
--target-list=riscv32-softmmu \
|
||||
--enable-gcrypt \
|
||||
--disable-slirp \
|
||||
--without-default-features \
|
||||
--disable-docs
|
||||
|
||||
ninja
|
||||
cp libqemu-riscv32.so ../../backend/app/services/
|
||||
cp ../pc-bios/esp32c3-rom.bin ../../backend/app/services/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. GPIO Pinmap — 22 GPIOs
|
||||
|
||||
The ESP32-C3 has **22 GPIOs (GPIO 0–21)**, fewer than the 40 on the ESP32. The QEMU machine (`esp32c3-picsimlab`) registers only 22 GPIO output lines in the QOM model. Sending a pinmap with more entries causes a QEMU property lookup error:
|
||||
|
||||
```
|
||||
qemu: Property 'esp32c3.gpio.esp32_gpios[22]' not found
|
||||
```
|
||||
|
||||
The worker (`esp32_worker.py`) detects the machine and rebuilds the pinmap before registering callbacks:
|
||||
|
||||
```python
|
||||
# In main(), right after reading config:
|
||||
if 'c3' in machine: # 'esp32c3-picsimlab'
|
||||
_build_pinmap(22) # GPIO 0..21 only
|
||||
# else: default pinmap (40 GPIOs for ESP32/ESP32-S3)
|
||||
```
|
||||
|
||||
`_build_pinmap(n)` creates a `ctypes.c_int16` array of `n+1` elements:
|
||||
```python
|
||||
def _build_pinmap(gpio_count: int):
|
||||
global _GPIO_COUNT, _PINMAP
|
||||
_GPIO_COUNT = gpio_count
|
||||
_PINMAP = (ctypes.c_int16 * (gpio_count + 1))(
|
||||
gpio_count, # [0] = count
|
||||
*range(gpio_count) # [1..n] = GPIO numbers 0..n-1
|
||||
)
|
||||
```
|
||||
|
||||
This is passed to QEMU via `_cbs_ref.pinmap`. When GPIO N changes, QEMU calls `picsimlab_write_pin(slot=N+1, value)` and the worker translates `slot → N` before emitting `{type: gpio_change, pin: N, state: value}`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Full Flow: Compile and Run
|
||||
|
||||
### 7.1 Compile the Sketch
|
||||
|
||||
```bash
|
||||
# arduino-cli compiles for ESP32-C3:
|
||||
|
|
@ -168,7 +327,7 @@ arduino-cli compile \
|
|||
|
||||
The Velxio backend produces this image automatically and sends it to the frontend as base64.
|
||||
|
||||
### 5.2 Minimal Sketch for ESP32-C3
|
||||
### 7.2 Minimal Sketch for ESP32-C3
|
||||
|
||||
```cpp
|
||||
// LED on GPIO 8 (ESP32-C3 DevKit)
|
||||
|
|
@ -190,7 +349,7 @@ void loop() {
|
|||
}
|
||||
```
|
||||
|
||||
### 5.3 Bare-Metal Sketch (for direct emulation tests)
|
||||
### 7.3 Bare-Metal Sketch (for unit-test runner only)
|
||||
|
||||
To verify the emulation without the Arduino framework, you can compile directly with the RISC-V toolchain:
|
||||
|
||||
|
|
@ -229,7 +388,7 @@ See full script: `frontend/src/__tests__/fixtures/esp32c3-blink/build.sh`
|
|||
|
||||
---
|
||||
|
||||
## 6. ESP32 Image Format
|
||||
## 8. ESP32 Image Format
|
||||
|
||||
The backend produces a merged **4 MB** image:
|
||||
|
||||
|
|
@ -263,7 +422,7 @@ The `parseMergedFlashImage()` parser in `utils/esp32ImageParser.ts` extracts all
|
|||
|
||||
---
|
||||
|
||||
## 7. Supported ISA — RV32IMC
|
||||
## 9. Supported ISA — RV32IMC
|
||||
|
||||
`RiscVCore.ts` implements the three extensions required to run code compiled for ESP32-C3:
|
||||
|
||||
|
|
@ -290,9 +449,11 @@ All 16-bit instructions from the standard C extension are supported. They are de
|
|||
|
||||
---
|
||||
|
||||
## 8. GPIO
|
||||
## 10. GPIO — MMIO Registers
|
||||
|
||||
GPIO handling follows the W1TS/W1TC register model of the ESP32-C3:
|
||||
> Note: these registers are used by the TypeScript unit-test layer (`RiscVCore.ts`). The QEMU backend handles GPIO via the `picsimlab_write_pin` callback, not MMIO polling.
|
||||
|
||||
GPIO MMIO layout on the ESP32-C3:
|
||||
|
||||
```typescript
|
||||
// Arduino sketch:
|
||||
|
|
@ -309,7 +470,7 @@ The callback `onPinChangeWithTime(pin, state, timeMs)` is the integration point
|
|||
|
||||
---
|
||||
|
||||
## 9. UART0 — Serial Monitor
|
||||
## 11. UART0 — Serial Monitor
|
||||
|
||||
Any byte written to `UART0_FIFO_REG` (0x60000000) calls the `onSerialData(char)` callback:
|
||||
|
||||
|
|
@ -331,48 +492,40 @@ sim.serialWrite("COMMAND\n");
|
|||
|
||||
---
|
||||
|
||||
## 10. Limitations
|
||||
## 12. Limitations of the riscv32 QEMU machine
|
||||
|
||||
### ESP-IDF / Arduino Framework
|
||||
The `esp32c3-picsimlab` QEMU machine in the lcgamboa fork emulates the core CPU and GPIO. The following are **not yet implemented** in the QEMU machine (as of 2026-03):
|
||||
|
||||
The Arduino framework for ESP32-C3 (based on ESP-IDF 4.4.x) has a complex initialization sequence that accesses non-emulated peripherals:
|
||||
| Feature | Status | Notes |
|
||||
| ------- | ------ | ----- |
|
||||
| WiFi / BLE | Not emulated | No slirp networking in riscv32 machine |
|
||||
| LEDC / PWM | Not emulated | No rmt/ledc callbacks yet |
|
||||
| RMT / NeoPixel | Not emulated | No RMT peripheral in esp32c3-picsimlab |
|
||||
| ADC | Not emulated | GPIO 0-5 ADC channels not wired |
|
||||
| Hardware I2C / SPI | Not emulated | Callbacks not registered |
|
||||
| Interrupts (`attachInterrupt`) | Not emulated | GPIO interrupt lines not connected |
|
||||
| NVS / SPIFFS | Not emulated | Flash writes not persisted |
|
||||
| arduino-esp32 3.x (IDF 5.x) | **Unsupported** | Use 2.0.17 (IDF 4.4.x) — same as Xtensa |
|
||||
|
||||
| Peripheral | Why ESP-IDF accesses it | Effect in emulator |
|
||||
| ---------- | ----------------------- | ------------------ |
|
||||
| Cache controller | Configures MMU for flash/DRAM mapping | Reads 0, may not loop |
|
||||
| Interrupt Matrix | Registers ISR vectors | No effect (silenced) |
|
||||
| System registers | Configures PLLs and clocks | Reads 0 (assumes default speed) |
|
||||
| FreeRTOS tick timer | Timer 0 → periodic interrupt | No interrupt = tasks not scheduled |
|
||||
|
||||
As a result, an Arduino sketch compiled with the full framework may execute partially — code prior to FreeRTOS initialization may work, but `setup()` and `loop()` depend on FreeRTOS running.
|
||||
|
||||
**Scenarios that DO work:**
|
||||
|
||||
- Bare-metal code (no framework, direct GPIO MMIO access)
|
||||
- Code fragments that do not use FreeRTOS (`delay()`, `millis()`, `digitalWrite()` require FreeRTOS)
|
||||
- ISA test programs (arithmetic operations, branches, loads/stores to DRAM)
|
||||
|
||||
**Roadmap for full support:**
|
||||
|
||||
1. Cache controller stub (return values indicating "cache already configured")
|
||||
2. Interrupt matrix stub (accept writes, ignore)
|
||||
3. Basic timer peripheral (generate FreeRTOS tick periodically)
|
||||
4. Once FreeRTOS is active: normal Arduino sketches should work
|
||||
|
||||
### Other Limitations
|
||||
|
||||
| Limitation | Detail |
|
||||
| ---------- | ------ |
|
||||
| No WiFi | ESP32-C3 has BLE/WiFi radio; not emulated |
|
||||
| No ADC | GPIO 0-5 as ADC not implemented |
|
||||
| No hardware SPI/I2C | Hardware SPI/I2C peripherals return 0 |
|
||||
| No interrupts | `attachInterrupt()` does not work |
|
||||
| No RTC | `esp_sleep_*`, `rtc_*` not implemented |
|
||||
| No NVS/Flash writes | `Preferences`, `SPIFFS` not implemented |
|
||||
> The QEMU machine boots the ESP-IDF bootloader and transitions to the Arduino `setup()`/`loop()` correctly for GPIO and UART sketches. Sketches using WiFi, LEDC, or RMT will boot but those peripherals will silently do nothing.
|
||||
|
||||
---
|
||||
|
||||
## 11. Tests
|
||||
## 13. Tests
|
||||
|
||||
### 13.1 End-to-End Test (Backend QEMU)
|
||||
|
||||
File: `backend/test_esp32c3_emulation.py`
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python test_esp32c3_emulation.py
|
||||
# Expected: PASS — GPIO8 toggled 2 times — ESP32-C3 emulation is working!
|
||||
```
|
||||
|
||||
Compiles a blink sketch via arduino-cli, merges a 4 MB flash image, launches the worker, and checks that GPIO8 toggles at least twice within 45 s.
|
||||
|
||||
### 13.2 RiscVCore ISA Unit Tests (TypeScript)
|
||||
|
||||
RISC-V emulation tests are in `frontend/src/__tests__/`:
|
||||
|
||||
|
|
@ -410,6 +563,8 @@ Compiles `blink.c` with `riscv32-esp-elf-gcc` (the arduino-cli toolchain) and ve
|
|||
|
||||
**Expected result:**
|
||||
|
||||
> Note: these TypeScript tests run against `RiscVCore.ts` (the in-browser interpreter), not against the QEMU backend.
|
||||
|
||||
```text
|
||||
✓ esp32c3-simulation.test.ts (30 tests) ~500ms
|
||||
✓ esp32c3-blink.test.ts (8 tests) ~300ms
|
||||
|
|
@ -429,31 +584,54 @@ frontend/src/__tests__/fixtures/esp32c3-blink/
|
|||
|
||||
---
|
||||
|
||||
## 12. Differences vs Xtensa Emulation (ESP32 / ESP32-S3)
|
||||
## 14. Differences vs Xtensa Emulation (ESP32 / ESP32-S3)
|
||||
|
||||
| Aspect | ESP32-C3 (RISC-V) | ESP32 / ESP32-S3 (Xtensa) |
|
||||
| ------ | ----------------- | ------------------------- |
|
||||
| Engine | `Esp32C3Simulator` (TypeScript, browser) | `Esp32Bridge` + backend QEMU |
|
||||
| Backend dependency | **No** — 100% in the browser | Yes — WebSocket to QEMU process |
|
||||
| Startup | Instant | ~1-2 seconds |
|
||||
| GPIO | Via MMIO W1TS/W1TC | Via QEMU callbacks → WebSocket |
|
||||
| WiFi | Not emulated | Emulated (hardcoded SSIDs) |
|
||||
| Hardware I2C/SPI | Not emulated | Emulated (synchronous callbacks) |
|
||||
| LEDC/PWM | Not emulated | Emulated (periodic polling) |
|
||||
| NeoPixel/RMT | Not emulated | Emulated (RMT decoder) |
|
||||
| Arduino framework | Partial (FreeRTOS not active) | Full |
|
||||
| CI tests | Yes (Vitest) | No (requires native lib) |
|
||||
| QEMU library | `libqemu-riscv32.dll/.so` | `libqemu-xtensa.dll/.so` |
|
||||
| QEMU machine | `esp32c3-picsimlab` | `esp32-picsimlab` / `esp32s3-picsimlab` |
|
||||
| ROM file | `esp32c3-rom.bin` (384 KB) | `esp32-v3-rom.bin` + `esp32-v3-rom-app.bin` |
|
||||
| GPIO count | **22** (GPIO 0–21) | **40** (GPIO 0–39) |
|
||||
| Build target | `--target-list=riscv32-softmmu` | `--target-list=xtensa-softmmu` |
|
||||
| `--disable-slirp` required | **Yes** (slirp.c incompatible pointer types with riscv32) | No (slirp works with xtensa) |
|
||||
| WiFi | Not emulated | Emulated (hardcoded SSIDs via SLIRP) |
|
||||
| LEDC/PWM | Not emulated | Emulated (periodic polling via internals) |
|
||||
| NeoPixel/RMT | Not emulated | Emulated (RMT decoder in worker) |
|
||||
| I2C / SPI | Not emulated | Emulated (synchronous QEMU callbacks) |
|
||||
| GPIO32–39 fix | N/A (only 22 GPIOs) | Required (bank 1 register) |
|
||||
| Arduino framework | Full (IDF 4.4.x) | Full (IDF 4.4.x) |
|
||||
| ISA unit tests | Yes (Vitest — RiscVCore.ts) | No (requires native lib) |
|
||||
|
||||
---
|
||||
|
||||
## 13. Key Files
|
||||
## 15. Key Files
|
||||
|
||||
### Backend
|
||||
|
||||
| File | Description |
|
||||
| ---- | ----------- |
|
||||
| `frontend/src/simulation/RiscVCore.ts` | RV32IMC emulator core (I + M + C extensions) |
|
||||
| `frontend/src/simulation/Esp32C3Simulator.ts` | ESP32-C3 memory map, GPIO, UART0, RAF loop |
|
||||
| `backend/app/services/libqemu-riscv32.dll` | QEMU riscv32 shared library (**not in git** — build locally) |
|
||||
| `backend/app/services/esp32c3-rom.bin` | ESP32-C3 boot ROM (**not in git** — copy from pc-bios/) |
|
||||
| `backend/app/services/esp32_worker.py` | QEMU subprocess worker; calls `_build_pinmap(22)` for C3 |
|
||||
| `backend/app/services/esp32_lib_manager.py` | `_RISCV_BOARDS`, `_MACHINE`, `LIB_RISCV_PATH`; selects right DLL |
|
||||
| `backend/test_esp32c3_emulation.py` | End-to-end test: compile → flash → QEMU worker → GPIO toggle |
|
||||
|
||||
### Frontend
|
||||
|
||||
| File | Description |
|
||||
| ---- | ----------- |
|
||||
| `frontend/src/store/useSimulatorStore.ts` | `ESP32_RISCV_KINDS` set; `isRiscVEsp32Kind()` routes to QEMU bridge |
|
||||
| `frontend/src/simulation/RiscVCore.ts` | RV32IMC interpreter (unit-test layer only) |
|
||||
| `frontend/src/simulation/Esp32C3Simulator.ts` | ESP32-C3 SoC wrapper (unit-test layer only) |
|
||||
| `frontend/src/utils/esp32ImageParser.ts` | ESP32 image format parser (merged flash → segments) |
|
||||
| `frontend/src/store/useSimulatorStore.ts` | `ESP32_RISCV_KINDS`, `createSimulator()`, `compileBoardProgram()` |
|
||||
| `frontend/src/__tests__/esp32c3-simulation.test.ts` | ISA unit tests (30 tests) |
|
||||
| `frontend/src/__tests__/esp32c3-blink.test.ts` | End-to-end integration test (8 tests) |
|
||||
| `frontend/src/__tests__/esp32c3-simulation.test.ts` | RV32IMC ISA unit tests (30 tests — TypeScript only) |
|
||||
| `frontend/src/__tests__/esp32c3-blink.test.ts` | End-to-end bare-metal test (8 tests — TypeScript only) |
|
||||
| `frontend/src/__tests__/fixtures/esp32c3-blink/` | Bare-metal test firmware + toolchain script |
|
||||
|
||||
### QEMU Source
|
||||
|
||||
| File | Description |
|
||||
| ---- | ----------- |
|
||||
| `wokwi-libs/qemu-lcgamboa/hw/riscv/esp32c3_picsimlab.c` | ESP32-C3 PICSimLab machine definition |
|
||||
| `wokwi-libs/qemu-lcgamboa/hw/gpio/esp32c3_gpio.c` | ESP32-C3 GPIO model (inherits esp32_gpio, 22 outputs) |
|
||||
| `wokwi-libs/qemu-lcgamboa/pc-bios/esp32c3-rom.bin` | ROM binary to copy to backend/app/services/ |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"generatedAt": "2026-03-16T21:03:46.561Z",
|
||||
"generatedAt": "2026-03-19T00:06:03.160Z",
|
||||
"components": [
|
||||
{
|
||||
"id": "arduino-mega",
|
||||
|
|
|
|||
|
|
@ -152,21 +152,8 @@ export class Esp32C3Simulator {
|
|||
*/
|
||||
private _periRegs = new Map<number, number>();
|
||||
|
||||
// ── Diagnostic state ─────────────────────────────────────────────────────
|
||||
private _dbgFrameCount = 0;
|
||||
private _dbgTickCount = 0;
|
||||
private _dbgLastMtvec = 0;
|
||||
private _dbgMieEnabled = false;
|
||||
/** Track PC at the start of each tick for stuck-loop detection. */
|
||||
private _dbgPrevTickPc = -1;
|
||||
private _dbgSamePcCount = 0;
|
||||
private _dbgStuckDumped = false;
|
||||
/** Ring buffer of sampled PCs — dumped when stuck detector fires. */
|
||||
private _pcTrace = new Uint32Array(128);
|
||||
private _pcTraceIdx = 0;
|
||||
private _pcTraceStep = 0;
|
||||
/** Count of ROM function calls (for logging first N). */
|
||||
private _romCallCount = 0;
|
||||
/** CPU ticks per µs — updated by ets_update_cpu_frequency(). */
|
||||
private _ticksPerUs = 160;
|
||||
|
||||
public pinManager: PinManager;
|
||||
public onSerialData: ((ch: string) => void) | null = null;
|
||||
|
|
@ -382,7 +369,6 @@ export class Esp32C3Simulator {
|
|||
private _registerIntMatrix(): void {
|
||||
const peri = this._periRegs;
|
||||
const BASE = INTMATRIX_BASE;
|
||||
let logCount = 0;
|
||||
|
||||
this.core.addMmio(BASE, INTMATRIX_SIZE,
|
||||
(addr) => {
|
||||
|
|
@ -428,28 +414,15 @@ export class Esp32C3Simulator {
|
|||
if (off <= 0x0F8) {
|
||||
const src = off >> 2;
|
||||
if (src < 62) {
|
||||
const oldLine = this._intSrcMap[src];
|
||||
this._intSrcMap[src] = newWord & 0x1F;
|
||||
if ((newWord & 0x1F) !== oldLine && logCount < 30) {
|
||||
logCount++;
|
||||
console.log(`[INTMATRIX] src ${src} → CPU line ${newWord & 0x1F}`);
|
||||
}
|
||||
}
|
||||
} else if (off === 0x104) {
|
||||
this._intLineEnable = newWord;
|
||||
if (logCount < 30) {
|
||||
logCount++;
|
||||
console.log(`[INTMATRIX] ENABLE = 0x${newWord.toString(16)}`);
|
||||
}
|
||||
} else if (off >= 0x114 && off <= 0x190) {
|
||||
const line = (off - 0x114) >> 2;
|
||||
if (line < 32) this._intLinePrio[line] = newWord & 0xF;
|
||||
} else if (off === 0x194) {
|
||||
this._intThreshold = newWord & 0xF;
|
||||
if (logCount < 30) {
|
||||
logCount++;
|
||||
console.log(`[INTMATRIX] THRESH = ${newWord & 0xF}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -615,15 +588,6 @@ export class Esp32C3Simulator {
|
|||
const r = this.core.regs;
|
||||
const c = this.core;
|
||||
|
||||
if (++this._romCallCount <= 50) {
|
||||
console.log(
|
||||
`[ROM] #${this._romCallCount} 0x${addr.toString(16)}` +
|
||||
` ra=0x${(r[1]>>>0).toString(16)}` +
|
||||
` a0=0x${(r[10]>>>0).toString(16)} a1=0x${(r[11]>>>0).toString(16)}` +
|
||||
` a2=0x${(r[12]>>>0).toString(16)} a3=0x${(r[13]>>>0).toString(16)}`
|
||||
);
|
||||
}
|
||||
|
||||
switch (addr) {
|
||||
// ── C library functions ──────────────────────────────────────────
|
||||
case 0x40000354: { // memset(dest, val, n) → dest
|
||||
|
|
@ -805,6 +769,116 @@ export class Esp32C3Simulator {
|
|||
break;
|
||||
}
|
||||
|
||||
// ── libgcc shift / bit operations ────────────────────────────────
|
||||
case 0x4000077c: { // __ashldi3(a, shift) → a << shift (64-bit)
|
||||
const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
|
||||
const shift = r[12] & 63;
|
||||
const val = BigInt(aLo) | (BigInt(aHi) << 32n);
|
||||
const res = BigInt.asUintN(64, val << BigInt(shift));
|
||||
r[10] = Number(res & 0xFFFFFFFFn) | 0;
|
||||
r[11] = Number((res >> 32n) & 0xFFFFFFFFn) | 0;
|
||||
break;
|
||||
}
|
||||
case 0x40000780: { // __ashrdi3(a, shift) → a >> shift (signed/arithmetic 64-bit)
|
||||
const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
|
||||
const shift = r[12] & 63;
|
||||
const val = BigInt.asIntN(64, BigInt(aLo) | (BigInt(aHi) << 32n));
|
||||
const res = BigInt.asUintN(64, val >> BigInt(shift));
|
||||
r[10] = Number(res & 0xFFFFFFFFn) | 0;
|
||||
r[11] = Number((res >> 32n) & 0xFFFFFFFFn) | 0;
|
||||
break;
|
||||
}
|
||||
case 0x40000830: { // __lshrdi3(a, shift) → a >>> shift (unsigned/logical 64-bit)
|
||||
const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
|
||||
const shift = r[12] & 63;
|
||||
const val = BigInt(aLo) | (BigInt(aHi) << 32n);
|
||||
const res = val >> BigInt(shift); // positive BigInt → logical shift
|
||||
r[10] = Number(res & 0xFFFFFFFFn) | 0;
|
||||
r[11] = Number((res >> 32n) & 0xFFFFFFFFn) | 0;
|
||||
break;
|
||||
}
|
||||
case 0x4000079c: { // __clzsi2(a) → count leading zeros (32-bit)
|
||||
r[10] = Math.clz32(r[10] >>> 0);
|
||||
break;
|
||||
}
|
||||
case 0x40000798: { // __clzdi2(a) → count leading zeros (64-bit)
|
||||
const lo = r[10] >>> 0, hi = r[11] >>> 0;
|
||||
r[10] = hi !== 0 ? Math.clz32(hi) : 32 + Math.clz32(lo);
|
||||
break;
|
||||
}
|
||||
case 0x400007a8: { // __ctzsi2(a) → count trailing zeros (32-bit)
|
||||
const v = r[10] >>> 0;
|
||||
r[10] = v === 0 ? 32 : 31 - Math.clz32(v & -v);
|
||||
break;
|
||||
}
|
||||
case 0x400007a4: { // __ctzdi2(a) → count trailing zeros (64-bit)
|
||||
const lo = r[10] >>> 0, hi = r[11] >>> 0;
|
||||
if (lo !== 0) {
|
||||
r[10] = 31 - Math.clz32(lo & -lo);
|
||||
} else if (hi !== 0) {
|
||||
r[10] = 32 + (31 - Math.clz32(hi & -hi));
|
||||
} else {
|
||||
r[10] = 64;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x4000086c: { // __negdi2(a) → -a (64-bit)
|
||||
const lo = r[10] >>> 0, hi = r[11] >>> 0;
|
||||
const val = BigInt(lo) | (BigInt(hi) << 32n);
|
||||
const neg = BigInt.asUintN(64, -val);
|
||||
r[10] = Number(neg & 0xFFFFFFFFn) | 0;
|
||||
r[11] = Number((neg >> 32n) & 0xFFFFFFFFn) | 0;
|
||||
break;
|
||||
}
|
||||
case 0x400007d0: { // __ffsdi2(a) → find first set bit (64-bit), 0 if none
|
||||
const lo = r[10] >>> 0, hi = r[11] >>> 0;
|
||||
if (lo !== 0) {
|
||||
r[10] = (31 - Math.clz32(lo & -lo)) + 1;
|
||||
} else if (hi !== 0) {
|
||||
r[10] = 32 + (31 - Math.clz32(hi & -hi)) + 1;
|
||||
} else {
|
||||
r[10] = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x400007d4: { // __ffssi2(a) → find first set bit (32-bit), 0 if none
|
||||
const v = r[10] >>> 0;
|
||||
r[10] = v === 0 ? 0 : (31 - Math.clz32(v & -v)) + 1;
|
||||
break;
|
||||
}
|
||||
case 0x40000784: { // __bswapdi2(a) → byte-swap 64-bit
|
||||
const lo = r[10] >>> 0, hi = r[11] >>> 0;
|
||||
const swapLo = ((lo >>> 24) | ((lo >>> 8) & 0xFF00) | ((lo << 8) & 0xFF0000) | (lo << 24)) >>> 0;
|
||||
const swapHi = ((hi >>> 24) | ((hi >>> 8) & 0xFF00) | ((hi << 8) & 0xFF0000) | (hi << 24)) >>> 0;
|
||||
r[10] = swapHi | 0; // swapped hi becomes new lo
|
||||
r[11] = swapLo | 0; // swapped lo becomes new hi
|
||||
break;
|
||||
}
|
||||
case 0x40000788: { // __bswapsi2(a) → byte-swap 32-bit
|
||||
const v = r[10] >>> 0;
|
||||
r[10] = ((v >>> 24) | ((v >>> 8) & 0xFF00) | ((v << 8) & 0xFF0000) | (v << 24)) | 0;
|
||||
break;
|
||||
}
|
||||
case 0x400007a0: { // __cmpdi2(a, b) → 0 if a<b, 1 if a==b, 2 if a>b (signed 64-bit)
|
||||
const a = BigInt.asIntN(64, BigInt(r[10] >>> 0) | (BigInt(r[11]) << 32n));
|
||||
const b = BigInt.asIntN(64, BigInt(r[12] >>> 0) | (BigInt(r[13]) << 32n));
|
||||
r[10] = a < b ? 0 : a === b ? 1 : 2;
|
||||
break;
|
||||
}
|
||||
case 0x400008a8: { // __ucmpdi2(a, b) → 0 if a<b, 1 if a==b, 2 if a>b (unsigned 64-bit)
|
||||
const a = BigInt(r[10] >>> 0) | (BigInt(r[11] >>> 0) << 32n);
|
||||
const b = BigInt(r[12] >>> 0) | (BigInt(r[13] >>> 0) << 32n);
|
||||
r[10] = a < b ? 0 : a === b ? 1 : 2;
|
||||
break;
|
||||
}
|
||||
case 0x40000764: { // __absvdi2(a) → |a| (signed 64-bit, aborts on overflow)
|
||||
const val = BigInt.asIntN(64, BigInt(r[10] >>> 0) | (BigInt(r[11]) << 32n));
|
||||
const abs = val < 0n ? BigInt.asUintN(64, -val) : BigInt.asUintN(64, val);
|
||||
r[10] = Number(abs & 0xFFFFFFFFn) | 0;
|
||||
r[11] = Number((abs >> 32n) & 0xFFFFFFFFn) | 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// ── ESP-IDF ROM helpers ──────────────────────────────────────────
|
||||
case 0x40000018: { // rtc_get_reset_reason() → 1 (POWERON_RESET)
|
||||
r[10] = 1;
|
||||
|
|
@ -813,7 +887,7 @@ export class Esp32C3Simulator {
|
|||
case 0x40000050: { // ets_delay_us(us) → void
|
||||
// Burn the equivalent number of CPU cycles so timers advance
|
||||
const us = r[10] >>> 0;
|
||||
const burnCycles = Math.min(us * (CPU_HZ / 1_000_000), 1_000_000);
|
||||
const burnCycles = Math.min(us * this._ticksPerUs, 1_000_000);
|
||||
this.core.cycles += burnCycles;
|
||||
r[10] = 0;
|
||||
break;
|
||||
|
|
@ -826,6 +900,33 @@ export class Esp32C3Simulator {
|
|||
r[10] = 0;
|
||||
break;
|
||||
}
|
||||
case 0x4000195c: { // rom_i2c_writeReg(...) → 0 (success)
|
||||
r[10] = 0;
|
||||
break;
|
||||
}
|
||||
case 0x40000588: { // ets_update_cpu_frequency(ticks_per_us)
|
||||
// Store value so our ets_delay_us can use the right multiplier.
|
||||
// Firmware calls this with e.g. 40 or 160.
|
||||
this._ticksPerUs = r[10] >>> 0;
|
||||
r[10] = 0;
|
||||
break;
|
||||
}
|
||||
case 0x40000084: { // uart_tx_wait_idle(uart_num) — no-op
|
||||
r[10] = 0;
|
||||
break;
|
||||
}
|
||||
case 0x400005f4: { // intr_matrix_set(cpu_no, model_num, intr_num)
|
||||
// Maps peripheral interrupt source a1 to CPU interrupt line a2.
|
||||
// This directly programs the interrupt matrix hardware.
|
||||
const source = r[11] >>> 0;
|
||||
const line = r[12] & 0x1F;
|
||||
if (source < 62) {
|
||||
this._intSrcMap[source] = line;
|
||||
// Also store in the MMIO echo-back register so firmware reads work
|
||||
this._periRegs.set(INTMATRIX_BASE + source * 4, line);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Unknown ROM function — return a0=0 (ESP_OK for esp_err_t functions)
|
||||
|
|
@ -848,16 +949,11 @@ export class Esp32C3Simulator {
|
|||
* Called for all known TIMG0/TIMG1 base addresses across ESP-IDF versions.
|
||||
*/
|
||||
private _registerTimerGroup(base: number): void {
|
||||
const seen = new Set<number>();
|
||||
const peri = this._periRegs;
|
||||
this.core.addMmio(base, 0x100,
|
||||
(addr) => {
|
||||
const off = addr - base;
|
||||
const wOff = off & ~3;
|
||||
if (!seen.has(wOff)) {
|
||||
seen.add(wOff);
|
||||
console.log(`[TIMG@0x${base.toString(16)}] 1st read wOff=0x${wOff.toString(16)} pc=0x${this.core.pc.toString(16)}`);
|
||||
}
|
||||
if (wOff === 0x68) {
|
||||
// TIMG_RTCCALICFG: bit15=TIMG_RTC_CALI_RDY=1 — calibration instantly done
|
||||
// Also set bit31 (start bit echo) which some versions check
|
||||
|
|
@ -1153,17 +1249,7 @@ export class Esp32C3Simulator {
|
|||
|
||||
start(): void {
|
||||
if (this.running) return;
|
||||
this._dbgFrameCount = 0;
|
||||
this._dbgTickCount = 0;
|
||||
this._dbgLastMtvec = 0;
|
||||
this._dbgMieEnabled = false;
|
||||
this._dbgPrevTickPc = -1;
|
||||
this._dbgSamePcCount = 0;
|
||||
this._dbgStuckDumped = false;
|
||||
this._pcTrace.fill(0);
|
||||
this._pcTraceIdx = 0;
|
||||
this._pcTraceStep = 0;
|
||||
this._romCallCount = 0;
|
||||
this._ticksPerUs = 160;
|
||||
console.log(`[ESP32-C3] Simulation started, entry=0x${this.core.pc.toString(16)}`);
|
||||
this.running = true;
|
||||
this._loop();
|
||||
|
|
@ -1182,17 +1268,7 @@ export class Esp32C3Simulator {
|
|||
this._stIntEna = 0;
|
||||
this._stIntRaw = 0;
|
||||
this._periRegs.clear();
|
||||
this._dbgFrameCount = 0;
|
||||
this._dbgTickCount = 0;
|
||||
this._dbgLastMtvec = 0;
|
||||
this._dbgMieEnabled = false;
|
||||
this._dbgPrevTickPc = -1;
|
||||
this._dbgSamePcCount = 0;
|
||||
this._dbgStuckDumped = false;
|
||||
this._pcTrace.fill(0);
|
||||
this._pcTraceIdx = 0;
|
||||
this._pcTraceStep = 0;
|
||||
this._romCallCount = 0;
|
||||
this._ticksPerUs = 160;
|
||||
this.dram.fill(0);
|
||||
this.iram.fill(0);
|
||||
this.core.reset(IROM_BASE);
|
||||
|
|
@ -1219,163 +1295,15 @@ export class Esp32C3Simulator {
|
|||
private _loop(): void {
|
||||
if (!this.running) return;
|
||||
|
||||
this._dbgFrameCount++;
|
||||
|
||||
// ── Per-frame diagnostics (check once, before heavy execution) ─────────
|
||||
// Detect mtvec being set — FreeRTOS writes this during startup.
|
||||
const mtvec = this.core.mtvecVal;
|
||||
if (mtvec !== this._dbgLastMtvec) {
|
||||
if (mtvec !== 0) {
|
||||
console.log(
|
||||
`[ESP32-C3] mtvec set → 0x${mtvec.toString(16)}` +
|
||||
` (mode=${mtvec & 3}) @ frame ${this._dbgFrameCount}`
|
||||
);
|
||||
}
|
||||
this._dbgLastMtvec = mtvec;
|
||||
}
|
||||
|
||||
// Detect MIE 0→1 transition — FreeRTOS enables this when scheduler starts.
|
||||
const mie = (this.core.mstatusVal & 0x8) !== 0;
|
||||
if (mie && !this._dbgMieEnabled) {
|
||||
console.log(
|
||||
`[ESP32-C3] MIE enabled (interrupts ON) @ frame ${this._dbgFrameCount}` +
|
||||
`, pc=0x${this.core.pc.toString(16)}`
|
||||
);
|
||||
this._dbgMieEnabled = true;
|
||||
}
|
||||
|
||||
// Log PC + key state every ~1 second (60 frames).
|
||||
if (this._dbgFrameCount % 60 === 0) {
|
||||
console.log(
|
||||
`[ESP32-C3] frame=${this._dbgFrameCount}` +
|
||||
` pc=0x${this.core.pc.toString(16)}` +
|
||||
` cycles=${this.core.cycles}` +
|
||||
` ticks=${this._dbgTickCount}` +
|
||||
` mtvec=0x${mtvec.toString(16)}` +
|
||||
` MIE=${mie}` +
|
||||
` GPIO=0x${this.gpioOut.toString(16)}`
|
||||
);
|
||||
}
|
||||
|
||||
// Execute in 1 ms chunks so FreeRTOS tick interrupts fire at ~1 kHz.
|
||||
let rem = CYCLES_PER_FRAME;
|
||||
while (rem > 0) {
|
||||
const n = rem < CYCLES_PER_TICK ? rem : CYCLES_PER_TICK;
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.core.step();
|
||||
// Sample PC every 500 steps into a ring buffer for post-mortem analysis
|
||||
if (++this._pcTraceStep >= 500) {
|
||||
this._pcTraceStep = 0;
|
||||
this._pcTrace[this._pcTraceIdx] = this.core.pc >>> 0;
|
||||
this._pcTraceIdx = (this._pcTraceIdx + 1) & 127;
|
||||
}
|
||||
}
|
||||
rem -= n;
|
||||
|
||||
this._dbgTickCount++;
|
||||
// Log frequently early in boot (every 10 ticks for first 50, then every 100)
|
||||
const shouldLog = this._dbgTickCount <= 50
|
||||
? this._dbgTickCount % 10 === 0
|
||||
: this._dbgTickCount <= 1000 && this._dbgTickCount % 100 === 0;
|
||||
if (shouldLog) {
|
||||
const spc = this.core.pc;
|
||||
let instrInfo = '';
|
||||
const iramOff = spc - IRAM_BASE;
|
||||
const flashOff = spc - IROM_BASE;
|
||||
const romOff = spc - ROM_BASE;
|
||||
let ib0 = 0, ib1 = 0, ib2 = 0, ib3 = 0;
|
||||
if (iramOff >= 0 && iramOff + 4 <= this.iram.length) {
|
||||
[ib0, ib1, ib2, ib3] = [this.iram[iramOff], this.iram[iramOff+1], this.iram[iramOff+2], this.iram[iramOff+3]];
|
||||
} else if (flashOff >= 0 && flashOff + 4 <= this.flash.length) {
|
||||
[ib0, ib1, ib2, ib3] = [this.flash[flashOff], this.flash[flashOff+1], this.flash[flashOff+2], this.flash[flashOff+3]];
|
||||
} else if (romOff >= 0 && romOff < ROM_SIZE) {
|
||||
// PC is in ROM stub region — show 0x8082 (C.RET)
|
||||
ib0 = 0x82; ib1 = 0x80;
|
||||
}
|
||||
const instr16 = ib0 | (ib1 << 8);
|
||||
const instr32 = ((ib0 | (ib1<<8) | (ib2<<16) | (ib3<<24)) >>> 0);
|
||||
const isC = (instr16 & 3) !== 3;
|
||||
const hex = isC ? instr16.toString(16).padStart(4,'0') : instr32.toString(16).padStart(8,'0');
|
||||
if (!isC) {
|
||||
const op = instr32 & 0x7F;
|
||||
const f3 = (instr32 >> 12) & 7;
|
||||
const rs1 = (instr32 >> 15) & 31;
|
||||
if (op === 0x73) {
|
||||
const csr = (instr32 >> 20) & 0xFFF;
|
||||
instrInfo = ` [SYSTEM csr=0x${csr.toString(16)} f3=${f3}]`;
|
||||
} else if (op === 0x03) {
|
||||
const imm = (instr32 >> 20) << 0 >> 0;
|
||||
instrInfo = ` [LOAD x${rs1}+${imm} f3=${f3}]`;
|
||||
} else if (op === 0x63) {
|
||||
instrInfo = ` [BRANCH f3=${f3}]`;
|
||||
} else if (op === 0x23) {
|
||||
instrInfo = ` [STORE f3=${f3}]`;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`[ESP32-C3] tick #${this._dbgTickCount}` +
|
||||
` pc=0x${spc.toString(16)} instr=0x${hex}${instrInfo}` +
|
||||
` MIE=${(this.core.mstatusVal & 0x8) !== 0}`
|
||||
);
|
||||
}
|
||||
|
||||
// ── Stuck-loop detector ────────────────────────────────────────────
|
||||
// If the PC hasn't changed across consecutive ticks (160 000 cycles),
|
||||
// the CPU is stuck in a tight spin. Dump all registers once for
|
||||
// post-mortem analysis so we can identify which peripheral or stub
|
||||
// needs attention.
|
||||
{
|
||||
const curPc = this.core.pc;
|
||||
if (curPc === this._dbgPrevTickPc) {
|
||||
this._dbgSamePcCount++;
|
||||
if (this._dbgSamePcCount >= 3 && !this._dbgStuckDumped) {
|
||||
this._dbgStuckDumped = true;
|
||||
console.warn(
|
||||
`[ESP32-C3] ⚠ CPU stuck at pc=0x${curPc.toString(16)} for ${this._dbgSamePcCount} ticks — register dump:`
|
||||
);
|
||||
const regNames = [
|
||||
'zero','ra','sp','gp','tp','t0','t1','t2',
|
||||
's0','s1','a0','a1','a2','a3','a4','a5',
|
||||
'a6','a7','s2','s3','s4','s5','s6','s7',
|
||||
's8','s9','s10','s11','t3','t4','t5','t6',
|
||||
];
|
||||
for (let i = 0; i < 32; i++) {
|
||||
console.warn(` x${i.toString().padStart(2)}(${regNames[i].padEnd(4)}) = 0x${(this.core.regs[i] >>> 0).toString(16).padStart(8, '0')}`);
|
||||
}
|
||||
console.warn(` mstatus=0x${(this.core.mstatusVal >>> 0).toString(16)} mtvec=0x${(this.core.mtvecVal >>> 0).toString(16)}`);
|
||||
// Dump sampled PC trace (oldest → newest)
|
||||
const traceEntries: string[] = [];
|
||||
for (let j = 0; j < 128; j++) {
|
||||
const idx = (this._pcTraceIdx + j) & 127;
|
||||
const tpc = this._pcTrace[idx];
|
||||
if (tpc !== 0) traceEntries.push(`0x${tpc.toString(16).padStart(8,'0')}`);
|
||||
}
|
||||
if (traceEntries.length > 0) {
|
||||
// Deduplicate consecutive entries for readability
|
||||
const deduped: string[] = [];
|
||||
let prev = '';
|
||||
let count = 0;
|
||||
for (const e of traceEntries) {
|
||||
if (e === prev) { count++; }
|
||||
else {
|
||||
if (count > 1) deduped.push(` (×${count})`);
|
||||
deduped.push(e);
|
||||
prev = e;
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
if (count > 1) deduped.push(` (×${count})`);
|
||||
console.warn(` PC trace (sampled every 500 steps, ${deduped.length} entries):`);
|
||||
console.warn(' ' + deduped.join(', '));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._dbgSamePcCount = 0;
|
||||
this._dbgStuckDumped = false;
|
||||
}
|
||||
this._dbgPrevTickPc = curPc;
|
||||
}
|
||||
|
||||
// Raise SYSTIMER TARGET0 alarm → routed through interrupt matrix.
|
||||
this._stIntRaw |= 1;
|
||||
if (this._stIntEna & 1) {
|
||||
|
|
|
|||
|
|
@ -46,19 +46,20 @@ export const getBoardPinManager = (id: string) => pinManagerMap.get(id);
|
|||
export const getBoardBridge = (id: string) => bridgeMap.get(id);
|
||||
export const getEsp32Bridge = (id: string) => esp32BridgeMap.get(id);
|
||||
|
||||
// Xtensa-based ESP32 boards — still use QEMU bridge (backend)
|
||||
// Xtensa-based ESP32 boards — use QEMU bridge (backend)
|
||||
const ESP32_KINDS = new Set<BoardKind>([
|
||||
'esp32', 'esp32-devkit-c-v4', 'esp32-cam', 'wemos-lolin32-lite',
|
||||
'esp32-s3', 'xiao-esp32-s3', 'arduino-nano-esp32',
|
||||
]);
|
||||
|
||||
// RISC-V ESP32 boards — use the browser-side Esp32C3Simulator (no backend needed)
|
||||
// RISC-V ESP32 boards — also use QEMU bridge (qemu-system-riscv32 -M esp32c3)
|
||||
// The browser-side Esp32C3Simulator cannot handle the 150+ ROM functions ESP-IDF needs.
|
||||
const ESP32_RISCV_KINDS = new Set<BoardKind>([
|
||||
'esp32-c3', 'xiao-esp32-c3', 'aitewinrobot-esp32c3-supermini',
|
||||
]);
|
||||
|
||||
function isEsp32Kind(kind: BoardKind): boolean {
|
||||
return ESP32_KINDS.has(kind);
|
||||
return ESP32_KINDS.has(kind) || ESP32_RISCV_KINDS.has(kind);
|
||||
}
|
||||
|
||||
function isRiscVEsp32Kind(kind: BoardKind): boolean {
|
||||
|
|
|
|||
Loading…
Reference in New Issue