14 KiB
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:
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:
- Create backing buffer:
352 × 4096 = 1,441,792 bytes - Register 4 WASM callbacks via
addFunction():flashRead(cfg, block, off, buffer, size)— reads from JS bufferflashProg(cfg, block, off, buffer, size)— writes to JS bufferflashErase(cfg, block)— no-opflashSync()— no-op
- Format and mount:
_lfs_format()→_lfs_mount() - Write files:
cwrap('lfs_write_file')for each user file - Unmount and copy into flash at offset
0xa0000
USBCDC Serial (not UART)
MicroPython on RP2040 uses USB CDC for the REPL, not UART0:
// 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:
- IndexedDB cache — instant load via
idb-keyval - Remote download — from
micropython.orgwith streaming progress - 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
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:
// 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)
// 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:
- Stops any running simulation
- Clears
compiledProgram - Deletes old file group
- Creates new file group with appropriate defaults:
- RP2040 MicroPython →
main.pywithPin(25)blink - ESP32 MicroPython →
main.pywithPin(2)blink - Arduino →
sketch.inowith default Arduino code
- RP2040 MicroPython →
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
languageModeon theBoardInstance - 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:
- If firmware not yet loaded → auto-loads firmware first, then starts
- If already loaded → starts simulation directly
- 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 |