feat: add comprehensive MicroPython implementation documentation for RP2040, ESP32, ESP32-S3, and ESP32-C3 boards
parent
0bc4c031d1
commit
6e5e9778fc
|
|
@ -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** |
|
||||||
Loading…
Reference in New Issue