feat: add comprehensive MicroPython implementation documentation for RP2040, ESP32, ESP32-S3, and ESP32-C3 boards

feature/micropython-rp2040
David Montero Crespo 2026-03-29 23:50:55 -03:00
parent 0bc4c031d1
commit 6e5e9778fc
1 changed files with 348 additions and 0 deletions

View File

@ -0,0 +1,348 @@
# MicroPython Implementation — Technical Summary
> Issue #3 — Full MicroPython support for RP2040, ESP32, ESP32-S3, and ESP32-C3 boards.
> Branch: `feature/micropython-rp2040`
---
## Overview
Velxio supports MicroPython across **15 board variants** using two distinct execution strategies:
| Strategy | Boards | Execution | Serial |
|----------|--------|-----------|--------|
| **Browser-side** (rp2040js) | Raspberry Pi Pico, Pico W | In-browser at 125 MHz | USBCDC |
| **QEMU backend** (WebSocket) | ESP32, ESP32-S3, ESP32-C3 (13 variants) | Remote `qemu-system-xtensa` / `qemu-system-riscv32` | UART via WebSocket |
### Supported Boards
```
RP2040 (browser): raspberry-pi-pico, pi-pico-w
ESP32 Xtensa (QEMU): esp32, esp32-devkit-c-v4, esp32-cam, wemos-lolin32-lite
ESP32-S3 Xtensa (QEMU): esp32-s3, xiao-esp32-s3, arduino-nano-esp32
ESP32-C3 RISC-V (QEMU): esp32-c3, xiao-esp32-c3, aitewinrobot-esp32c3-supermini
```
---
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ User clicks "Run" with MicroPython mode │
│ │
│ EditorToolbar.handleRun() │
│ └── loadMicroPythonProgram(boardId, pyFiles) │
│ ├── RP2040? → sim.loadMicroPython(files) │
│ │ ├── getFirmware() [IndexedDB/remote/bundled]
│ │ ├── loadUF2() [parse UF2 → flash]
│ │ ├── loadUserFiles() [LittleFS → flash]
│ │ └── USBCDC serial [REPL via USB CDC]
│ │ │
│ └── ESP32? → getEsp32Firmware() │
│ ├── base64 encode │
│ ├── bridge.loadFirmware(b64) │
│ └── bridge.setPendingMicroPythonCode()
│ └── on ">>>" detected: │
│ _injectCodeViaRawPaste() │
│ \x01 → \x05 → code → \x04
└─────────────────────────────────────────────────────────────┘
```
---
## Phase 1: RP2040 MicroPython (Browser-Side)
### Files Created/Modified
| File | Action |
|------|--------|
| `frontend/src/simulation/MicroPythonLoader.ts` | **Created** — UF2 parser, LittleFS builder, firmware cache |
| `frontend/src/simulation/RP2040Simulator.ts` | Modified — `loadMicroPython()`, USBCDC serial, `micropythonMode` |
| `frontend/src/types/board.ts` | Modified — `LanguageMode` type, `BOARD_SUPPORTS_MICROPYTHON` set |
| `frontend/src/store/useSimulatorStore.ts` | Modified — `loadMicroPythonProgram`, `setBoardLanguageMode` |
| `frontend/src/store/useEditorStore.ts` | Modified — `DEFAULT_MICROPYTHON_CONTENT`, `createFileGroup` overload |
| `frontend/src/components/editor/EditorToolbar.tsx` | Modified — language selector, MicroPython compile/run flow |
| `frontend/src/components/simulator/SerialMonitor.tsx` | Modified — Ctrl+C/D, REPL label |
| `frontend/public/firmware/micropython-rp2040.uf2` | **Created** — bundled fallback (638 KB) |
### UF2 Firmware Loading
MicroPython for RP2040 uses UF2 format (USB Flashing Format). Each block is 512 bytes:
```
Offset Field
0 Magic 0 (0x0a324655)
4 Magic 1 (0x9e5d5157)
12 Flash address (little-endian)
32 Payload data (256 bytes)
```
Constants:
```typescript
FLASH_START_ADDRESS = 0x10000000 // RP2040 XIP flash base
MICROPYTHON_FS_FLASH_START = 0xa0000 // LittleFS starts at 640KB
MICROPYTHON_FS_BLOCK_SIZE = 4096
MICROPYTHON_FS_BLOCK_COUNT = 352 // 1.4 MB filesystem
```
### LittleFS Filesystem
User files (e.g., `main.py`) are written into a LittleFS image in memory using the `littlefs` WASM package:
1. Create backing buffer: `352 × 4096 = 1,441,792 bytes`
2. Register 4 WASM callbacks via `addFunction()`:
- `flashRead(cfg, block, off, buffer, size)` — reads from JS buffer
- `flashProg(cfg, block, off, buffer, size)` — writes to JS buffer
- `flashErase(cfg, block)` — no-op
- `flashSync()` — no-op
3. Format and mount: `_lfs_format()``_lfs_mount()`
4. Write files: `cwrap('lfs_write_file')` for each user file
5. Unmount and copy into flash at offset `0xa0000`
### USBCDC Serial (not UART)
MicroPython on RP2040 uses USB CDC for the REPL, not UART0:
```typescript
// In RP2040Simulator.loadMicroPython():
this.usbCDC = new USBCDC(rp2040.usbCtrl);
this.usbCDC.onDeviceConnected = () => this.usbCDC.sendSerialByte('\r'.charCodeAt(0));
this.usbCDC.onSerialData = (value) => { /* forward to SerialMonitor */ };
// Serial write routes through USBCDC when micropythonMode is true:
serialWrite(text: string): void {
if (this.micropythonMode && this.usbCDC) {
for (const ch of text) this.usbCDC.sendSerialByte(ch.charCodeAt(0));
} else {
this.rp2040?.uart[0].feedByte(...);
}
}
```
### Firmware Caching Strategy
All firmware uses a 3-tier loading strategy:
1. **IndexedDB cache** — instant load via `idb-keyval`
2. **Remote download** — from `micropython.org` with streaming progress
3. **Bundled fallback** — from `/firmware/` (Vite public directory)
Cache key: `micropython-rp2040-uf2-v1.20.0`
---
## Phase 2: ESP32/ESP32-S3 MicroPython (QEMU Backend)
### Files Created/Modified
| File | Action |
|------|--------|
| `frontend/src/simulation/Esp32MicroPythonLoader.ts` | **Created** — firmware download/cache for ESP32 variants |
| `frontend/src/simulation/Esp32Bridge.ts` | Modified — `setPendingMicroPythonCode()`, raw-paste injection |
| `frontend/src/store/useSimulatorStore.ts` | Modified — ESP32 path in `loadMicroPythonProgram` |
| `frontend/src/store/useEditorStore.ts` | Modified — `DEFAULT_ESP32_MICROPYTHON_CONTENT` |
| `frontend/src/components/simulator/SerialMonitor.tsx` | Modified — generic Ctrl+C/D for all boards |
| `frontend/public/firmware/micropython-esp32.bin` | **Created** — bundled fallback (1.5 MB) |
| `frontend/public/firmware/micropython-esp32s3.bin` | **Created** — bundled fallback (1.4 MB) |
### Firmware Variants
```typescript
FIRMWARE_MAP = {
'esp32': { remote: '.../ESP32_GENERIC-20230426-v1.20.0.bin', cacheKey: 'micropython-esp32-v1.20.0' },
'esp32-s3': { remote: '.../ESP32_GENERIC_S3-20230426-v1.20.0.bin', cacheKey: 'micropython-esp32s3-v1.20.0' },
'esp32-c3': { remote: '.../ESP32_GENERIC_C3-20230426-v1.20.0.bin', cacheKey: 'micropython-esp32c3-v1.20.0' },
}
```
Board kind → firmware variant mapping:
```
esp32, esp32-devkit-c-v4, esp32-cam, wemos-lolin32-lite → 'esp32'
esp32-s3, xiao-esp32-s3, arduino-nano-esp32 → 'esp32-s3'
esp32-c3, xiao-esp32-c3, aitewinrobot-esp32c3-supermini → 'esp32-c3'
```
### Raw-Paste REPL Protocol
Unlike RP2040 (which loads files into LittleFS), ESP32 boards inject user code into the MicroPython REPL after boot using the **raw-paste protocol**:
```
Step 1: Wait for ">>>" in serial output (REPL ready)
Step 2: Send \x01 (Ctrl+A) — enter raw REPL mode
Step 3: Send \x05 (Ctrl+E) — enter raw-paste mode
Step 4: Send code bytes in 256-byte chunks (10ms between chunks)
Step 5: Send \x04 (Ctrl+D) — execute the code
```
Implementation in `Esp32Bridge`:
```typescript
// State fields
private _pendingMicroPythonCode: string | null = null;
private _serialBuffer = '';
micropythonMode = false;
// In serial_output handler — detect REPL prompt
if (this._pendingMicroPythonCode && this._serialBuffer.includes('>>>')) {
this._injectCodeViaRawPaste(this._pendingMicroPythonCode);
this._pendingMicroPythonCode = null;
}
// Timing: 500ms initial delay → 100ms after Ctrl+A → 100ms after Ctrl+E
// → 10ms between 256-byte chunks → 50ms before Ctrl+D
```
### WebSocket Protocol Flow
```
Frontend Backend (QEMU)
│ │
│ start_esp32 { firmware_b64, board }
│──────────────────────────────────→│
│ │ QEMU boots MicroPython
│ │
│ serial_output { data: ">>>" } │
│←──────────────────────────────────│
│ │
│ esp32_serial_input { bytes: [0x01] } ← Ctrl+A (raw REPL)
│──────────────────────────────────→│
│ │
│ esp32_serial_input { bytes: [0x05] } ← Ctrl+E (raw-paste)
│──────────────────────────────────→│
│ │
│ esp32_serial_input { bytes: [...code...] } ← code chunks
│──────────────────────────────────→│
│ │
│ esp32_serial_input { bytes: [0x04] } ← Ctrl+D (execute)
│──────────────────────────────────→│
│ │
│ serial_output { data: "...output..." }
│←──────────────────────────────────│
```
---
## Phase 3: ESP32-C3 MicroPython
### Files Modified
| File | Action |
|------|--------|
| `frontend/src/types/board.ts` | Modified — added C3 boards to `BOARD_SUPPORTS_MICROPYTHON`, fixed comments |
| `frontend/src/simulation/Esp32MicroPythonLoader.ts` | Modified — added `esp32-c3` firmware variant |
| `frontend/public/firmware/micropython-esp32c3.bin` | **Created** — bundled fallback (1.4 MB) |
### Key Finding
ESP32-C3 boards were documented as "browser emulation (Esp32C3Simulator)" in the type definitions, but the actual runtime code routes them through `Esp32Bridge` (QEMU backend) — the `Esp32C3Simulator` is dead code. This was corrected.
The `isEsp32Kind()` function in `useSimulatorStore.ts` already included C3 boards, so the QEMU bridge infrastructure was already wired up. Only the MicroPython-specific additions were needed.
---
## Store Integration
### `loadMicroPythonProgram(boardId, files)`
```typescript
// In useSimulatorStore.ts
async loadMicroPythonProgram(boardId, files) {
if (isEsp32Kind(board.boardKind)) {
// ESP32 path: firmware → QEMU bridge → raw-paste injection
const firmware = await getEsp32Firmware(board.boardKind);
const b64 = uint8ArrayToBase64(firmware);
esp32Bridge.loadFirmware(b64);
esp32Bridge.setPendingMicroPythonCode(mainFile.content);
} else {
// RP2040 path: firmware + LittleFS in browser
await sim.loadMicroPython(files);
}
// Mark as loaded
board.compiledProgram = 'micropython-loaded';
}
```
### `setBoardLanguageMode(boardId, mode)`
When toggling between Arduino and MicroPython:
1. Stops any running simulation
2. Clears `compiledProgram`
3. Deletes old file group
4. Creates new file group with appropriate defaults:
- RP2040 MicroPython → `main.py` with `Pin(25)` blink
- ESP32 MicroPython → `main.py` with `Pin(2)` blink
- Arduino → `sketch.ino` with default Arduino code
---
## UI Integration
### Language Mode Selector
Appears in `EditorToolbar` next to the board name pill when `BOARD_SUPPORTS_MICROPYTHON.has(boardKind)`:
```
[Arduino C++ ▼] ↔ [MicroPython ▼]
```
Changing the selector:
- Switches `languageMode` on the `BoardInstance`
- Replaces editor files with language-appropriate defaults
- Updates compile button text: "Compile (Ctrl+B)" → "Load MicroPython"
### Serial Monitor
- **REPL label**: Shows "MicroPython REPL" in magenta when in MicroPython mode
- **Baud rate**: Hidden in MicroPython mode (REPL has no baud rate)
- **Ctrl+C**: Sends `\x03` (keyboard interrupt) — works for both RP2040 and ESP32
- **Ctrl+D**: Sends `\x04` (soft reset) — works for both RP2040 and ESP32
- **Placeholder**: "Type Python expression... (Ctrl+C to interrupt)"
### Run Button Behavior
For MicroPython boards, the Run button:
1. If firmware not yet loaded → auto-loads firmware first, then starts
2. If already loaded → starts simulation directly
3. Enabled even without `compiledProgram` (unlike Arduino mode)
---
## Firmware Files
| File | Size | Board | Version |
|------|------|-------|---------|
| `micropython-rp2040.uf2` | 638 KB | RP2040 | v1.20.0 (2023-04-26) |
| `micropython-esp32.bin` | 1.5 MB | ESP32 Xtensa | v1.20.0 (2023-04-26) |
| `micropython-esp32s3.bin` | 1.4 MB | ESP32-S3 Xtensa | v1.20.0 (2023-04-26) |
| `micropython-esp32c3.bin` | 1.4 MB | ESP32-C3 RISC-V | v1.20.0 (2023-04-26) |
All firmware uses MicroPython **v1.20.0** (stable release, 2023-04-26).
Firmware source: `https://micropython.org/resources/firmware/`
---
## Dependencies Added
| Package | Version | Purpose |
|---------|---------|---------|
| `littlefs` | 0.1.0 | LittleFS WASM for creating filesystem images (RP2040) |
| `idb-keyval` | 6.x | IndexedDB key-value wrapper for firmware caching |
---
## Git History
```
990ae4b feat: add MicroPython support for RP2040 boards (Pico / Pico W)
7c9fd0b feat: add MicroPython support for ESP32/ESP32-S3 boards via QEMU bridge
0bc4c03 feat: add MicroPython support for ESP32-C3 (RISC-V) boards
```
| Commit | Files Changed | Lines Added | Lines Removed |
|--------|:------------:|:-----------:|:------------:|
| `990ae4b` (RP2040) | 12 | +587 | -38 |
| `7c9fd0b` (ESP32/S3) | 8 | +296 | -33 |
| `0bc4c03` (ESP32-C3) | 3 | +16 | -4 |
| **Total** | **23** | **+899** | **-75** |