617 lines
22 KiB
Markdown
617 lines
22 KiB
Markdown
# RP2040 Emulation (Raspberry Pi Pico / Pico W)
|
||
|
||
> Status: **Functional** · In-browser emulation · No backend dependencies
|
||
> Engine: **rp2040js** — ARM Cortex-M0+ emulator in TypeScript
|
||
> Platform: **RP2040 @ 125 MHz** — dual-core ARM Cortex-M0+ (single core emulated)
|
||
> Available on: all platforms (Windows, macOS, Linux, Docker)
|
||
> Applies to: **Raspberry Pi Pico**, **Raspberry Pi Pico W**
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
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. [Binary Format and Loading](#6-binary-format-and-loading)
|
||
7. [GPIO](#7-gpio)
|
||
8. [UART — Serial Monitor](#8-uart--serial-monitor)
|
||
9. [ADC — Analog Inputs](#9-adc--analog-inputs)
|
||
10. [I2C Virtual Devices](#10-i2c-virtual-devices)
|
||
11. [SPI](#11-spi)
|
||
12. [PWM](#12-pwm)
|
||
13. [Simulation Execution Loop](#13-simulation-execution-loop)
|
||
14. [Pin Mapping](#14-pin-mapping)
|
||
15. [Oscilloscope / Logic Analyzer](#15-oscilloscope--logic-analyzer)
|
||
16. [Known Limitations](#16-known-limitations)
|
||
17. [Tests](#17-tests)
|
||
18. [Differences vs Other Emulators](#18-differences-vs-other-emulators)
|
||
19. [Key Files](#19-key-files)
|
||
|
||
---
|
||
|
||
## 1. Overview
|
||
|
||
The **Raspberry Pi Pico** and **Pico W** boards use the **RP2040** microcontroller — a dual-core **ARM Cortex-M0+** chip designed by Raspberry Pi. Unlike the ESP32 (which requires QEMU running in the backend), RP2040 emulation runs **entirely in the browser** using the [rp2040js](https://github.com/wokwi/rp2040js) library (a local clone in `wokwi-libs/rp2040js/`).
|
||
|
||
### Emulation Engine Comparison
|
||
|
||
| Board | CPU | Engine |
|
||
| ----- | --- | ------ |
|
||
| Arduino Uno / Nano / Mega | AVR ATmega | avr8js (browser) |
|
||
| Raspberry Pi Pico / Pico W | RP2040 ARM Cortex-M0+ | **rp2040js (browser, no backend)** |
|
||
| ESP32-C3, XIAO-C3 | RISC-V RV32IMC | RiscVCore.ts (browser) |
|
||
| ESP32, ESP32-S3 | Xtensa LX6/LX7 | QEMU lcgamboa (backend WebSocket) |
|
||
|
||
### Advantages of the In-Browser Emulator
|
||
|
||
- **No network dependencies** — works offline, no WebSocket connection to any backend process
|
||
- **Instant startup** — emulation begins immediately after compilation (no process launch latency)
|
||
- **Testable with Vitest** — the same TypeScript code runs in both production and CI tests
|
||
- **Cross-platform** — identical behavior on Windows, macOS, Linux, and Docker
|
||
|
||
---
|
||
|
||
## 2. Supported Boards
|
||
|
||
<table>
|
||
<tr>
|
||
<td align="center"><img src="img/boards/pi-pico.png" width="180" alt="Raspberry Pi Pico"/><br/><b>Raspberry Pi Pico</b></td>
|
||
<td align="center"><img src="img/boards/pi-pico-w.png" width="180" alt="Raspberry Pi Pico W"/><br/><b>Raspberry Pi Pico W</b></td>
|
||
</tr>
|
||
</table>
|
||
|
||
| Board | arduino-cli FQBN | Built-in LED | Notes |
|
||
| ----- | ---------------- | ------------ | ----- |
|
||
| Raspberry Pi Pico | `rp2040:rp2040:rpipico` | GPIO 25 | Standard Pico |
|
||
| Raspberry Pi Pico W | `rp2040:rp2040:rpipicow` | GPIO 25 (via CYW43) | WiFi chip not emulated |
|
||
|
||
> **Pico W note:** The wireless chip (Infineon CYW43439) is not emulated. GPIO 25 drives the on-board LED in the same way as the standard Pico for simulation purposes.
|
||
|
||
---
|
||
|
||
## 3. Emulator Architecture
|
||
|
||
```text
|
||
Arduino Sketch (.ino)
|
||
│
|
||
▼ arduino-cli (backend, FQBN rp2040:rp2040:rpipico)
|
||
sketch.ino.bin ← raw ARM binary (no HEX format)
|
||
│
|
||
▼ base64 → frontend
|
||
compileBoardProgram(boardId, base64)
|
||
│
|
||
▼ RP2040Simulator.loadBinary(base64)
|
||
Uint8Array → copied into rp2040.flash at offset 0
|
||
│
|
||
▼ rp2040.loadBootrom(bootromB1)
|
||
Bootrom B1 revision loaded
|
||
│
|
||
▼ PC reset to 0x10000000 (FLASH_START_ADDRESS)
|
||
CortexM0Core.executeInstruction() ← requestAnimationFrame @ 60 FPS
|
||
│ 2,083,333 cycles/frame (125 MHz ÷ 60)
|
||
├── GPIO listener → PinManager → visual component update
|
||
├── UART0/UART1 byte → onSerialData → Serial Monitor
|
||
├── ADC channel read → channelValues[ch] → analogRead()
|
||
└── I2C bus events → virtual I2C device callbacks
|
||
```
|
||
|
||
### Main Classes
|
||
|
||
| Class | File | Responsibility |
|
||
| ----- | ---- | -------------- |
|
||
| `RP2040Simulator` | `simulation/RP2040Simulator.ts` | Main wrapper: bootrom, GPIO, UART, ADC, I2C, SPI, RAF loop |
|
||
| `RP2040` | `rp2040js` (library) | Full hardware model: CPU, peripherals, memory map |
|
||
| `CortexM0Core` | `rp2040js` (library) | ARM Cortex-M0+ instruction decoder and executor |
|
||
| `PinManager` | `simulation/PinManager.ts` | Routes GPIO events to visual components |
|
||
|
||
---
|
||
|
||
## 4. Emulated Memory and Peripherals
|
||
|
||
### Memory Map
|
||
|
||
| Region | Base Address | Size | Description |
|
||
| ------ | ------------ | ---- | ----------- |
|
||
| Flash (XIP) | `0x10000000` | 16 MB | Program storage (firmware binary) |
|
||
| SRAM | `0x20000000` | 264 KB | Data RAM (stack, variables) |
|
||
| USB DPRAM | `0x50100000` | 4 KB | USB dual-port RAM |
|
||
| Bootrom | `0x00000000` | 4 KB | B1 revision bootrom |
|
||
|
||
### Peripherals Emulated by rp2040js
|
||
|
||
| Peripheral | Details |
|
||
| ---------- | ------- |
|
||
| GPIO | All 30 pins (GPIO0–GPIO29), input/output |
|
||
| UART0 | TX/RX, baud rate auto-detection |
|
||
| UART1 | TX/RX, forwarded to same `onSerialData` callback |
|
||
| I2C0 | Master mode, virtual device callbacks |
|
||
| I2C1 | Master mode, virtual device callbacks |
|
||
| SPI0 | Default loopback; custom handler supported |
|
||
| SPI1 | Default loopback; custom handler supported |
|
||
| ADC | 5 channels (GPIO26–29 + internal temp sensor, ch4) |
|
||
| PWM | Available on any GPIO |
|
||
| Timers | Used internally for `delay()`, `millis()` |
|
||
| Watchdog | Present (reads return 0) |
|
||
| PLL / Clock | Simulated at fixed 125 MHz |
|
||
|
||
### Peripherals NOT Emulated
|
||
|
||
- **WiFi/BLE** (Pico W: CYW43439 wireless chip)
|
||
- **USB device stack** (USB device enumeration)
|
||
- **DMA transfers** (DMA control registers return 0)
|
||
- **PIO state machines** (PIO registers return 0)
|
||
|
||
> PIO in particular is a significant limitation — `PicoLED`, `WS2812` (NeoPixel), `DHT`, and other timing-critical libraries that rely on PIO will not function.
|
||
|
||
---
|
||
|
||
## 5. Full Flow: Compile and Run
|
||
|
||
### 5.1 Compile the Sketch
|
||
|
||
The backend compiles for the Pico using the earlephilhower arduino-pico core:
|
||
|
||
```bash
|
||
# First-time setup — install the RP2040 board manager:
|
||
arduino-cli config add board_manager.additional_urls \
|
||
https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
|
||
|
||
arduino-cli core install rp2040:rp2040
|
||
|
||
# Compile for Raspberry Pi Pico:
|
||
arduino-cli compile \
|
||
--fqbn rp2040:rp2040:rpipico \
|
||
--output-dir build/ \
|
||
my_sketch/
|
||
|
||
# Output: build/my_sketch.ino.bin ← raw binary
|
||
```
|
||
|
||
The Velxio backend automatically finds `sketch.ino.bin` (or `sketch.ino.uf2` as fallback), encodes it as base64, and sends it to the frontend in `CompileResponse.binary_content`.
|
||
|
||
### 5.2 Serial Redirection (Important)
|
||
|
||
The backend **automatically prepends** `#define Serial Serial1` to `sketch.ino` for RP2040 boards:
|
||
|
||
```python
|
||
# backend/app/services/arduino_cli.py
|
||
if "rp2040" in board_fqbn and write_name == "sketch.ino":
|
||
content = "#define Serial Serial1\n" + content
|
||
```
|
||
|
||
**Why?** The RP2040 bootrom uses UART0 for its own communication. To avoid conflicts, the Arduino `Serial` object is remapped to UART1. This means `Serial.print()` in your sketch goes through UART1, which the emulator also captures and shows in the Serial Monitor.
|
||
|
||
### 5.3 Minimal Sketch for Raspberry Pi Pico
|
||
|
||
```cpp
|
||
// Blink the built-in LED (GPIO 25)
|
||
void setup() {
|
||
pinMode(LED_BUILTIN, OUTPUT);
|
||
Serial.begin(115200);
|
||
Serial.println("Pico started!");
|
||
}
|
||
|
||
void loop() {
|
||
digitalWrite(LED_BUILTIN, HIGH);
|
||
Serial.println("LED ON");
|
||
delay(500);
|
||
digitalWrite(LED_BUILTIN, LOW);
|
||
Serial.println("LED OFF");
|
||
delay(500);
|
||
}
|
||
```
|
||
|
||
### 5.4 Analog Read Example
|
||
|
||
```cpp
|
||
// Read a potentiometer on GPIO 26 (ADC channel 0)
|
||
void setup() {
|
||
Serial.begin(115200);
|
||
}
|
||
|
||
void loop() {
|
||
int raw = analogRead(A0); // GPIO26 → ADC ch0, returns 0–4095
|
||
float voltage = raw * 3.3f / 4095.0f;
|
||
Serial.print("ADC: ");
|
||
Serial.print(raw);
|
||
Serial.print(" → ");
|
||
Serial.print(voltage);
|
||
Serial.println(" V");
|
||
delay(200);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Binary Format and Loading
|
||
|
||
The RP2040 uses a **raw binary format** (`.bin`), not the Intel HEX format used by AVR:
|
||
|
||
```typescript
|
||
// RP2040Simulator.loadBinary(base64: string)
|
||
loadBinary(base64: string): void {
|
||
const binary = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
||
// Copy directly into flash at offset 0
|
||
this.rp2040.flash.set(binary, 0);
|
||
// Load bootrom and reset PC to 0x10000000
|
||
this.rp2040.loadBootrom(bootromB1);
|
||
}
|
||
```
|
||
|
||
**Flash memory layout** (as seen by the CPU):
|
||
|
||
```text
|
||
0x10000000 ← FLASH_START_ADDRESS — firmware entry point (PC reset here)
|
||
0x10000100 ... program .text section
|
||
0x10xxxxxx ... .rodata, .data, constants
|
||
0x20000000 ← SRAM — stack, heap, .bss
|
||
```
|
||
|
||
The bootrom B1 (`rp2040-bootrom.ts`) contains the RP2040's factory-programmed ROM code, required for correct flash XIP (eXecute In Place) initialization.
|
||
|
||
---
|
||
|
||
## 7. GPIO
|
||
|
||
All 30 GPIO pins (GPIO0–GPIO29) are emulated. Each pin has an event listener attached at startup:
|
||
|
||
```typescript
|
||
// RP2040Simulator.ts — gpio listener setup
|
||
for (let gpioIdx = 0; gpioIdx < 30; gpioIdx++) {
|
||
const gpio = this.rp2040.gpio[gpioIdx];
|
||
gpio.addListener((state: GPIOPinState) => {
|
||
const isHigh = state === GPIOPinState.High;
|
||
this.pinManager.triggerPinChange(gpioIdx, isHigh);
|
||
if (this.onPinChangeWithTime) {
|
||
const timeMs = this.rp2040.clock.timeUs / 1000;
|
||
this.onPinChangeWithTime(gpioIdx, isHigh, timeMs);
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
### GPIO Input (Push Buttons / Switches)
|
||
|
||
External components can drive GPIO pins from the UI:
|
||
|
||
```typescript
|
||
// Set a pin HIGH or LOW from the simulator canvas (e.g. button press)
|
||
simulator.setPinState(gpioPin: number, state: boolean): void
|
||
// → rp2040.gpio[gpioPin].setInputValue(state)
|
||
```
|
||
|
||
This is equivalent to the signal propagated by a physical wire from a button to the GPIO pin.
|
||
|
||
### LED_BUILTIN
|
||
|
||
- `LED_BUILTIN` = GPIO 25 on both Pico and Pico W
|
||
- A pin change listener on GPIO 25 drives the on-board LED component
|
||
- `digitalWrite(LED_BUILTIN, HIGH)` → GPIO 25 goes high → LED visual component turns on
|
||
|
||
---
|
||
|
||
## 8. UART — Serial Monitor
|
||
|
||
The RP2040 has two UART controllers. Both are emulated and forwarded to the Serial Monitor:
|
||
|
||
| Channel | TX Pin | RX Pin | Arduino Object | Notes |
|
||
| ------- | ------ | ------ | -------------- | ----- |
|
||
| UART0 | GPIO0 | GPIO1 | `Serial1` | Used by bootrom; Arduino `Serial` redirected away |
|
||
| UART1 | GPIO4 | GPIO5 | `Serial` (redirected) | Receives Arduino `Serial.print()` |
|
||
|
||
### Serial Output (TX → Monitor)
|
||
|
||
```typescript
|
||
// Both UART0 and UART1 forward bytes to the same callback:
|
||
this.rp2040.uart[0].onByte = (value: number) => {
|
||
this.onSerialData?.(String.fromCharCode(value));
|
||
};
|
||
this.rp2040.uart[1].onByte = (value: number) => {
|
||
this.onSerialData?.(String.fromCharCode(value));
|
||
};
|
||
```
|
||
|
||
### Serial Input (Monitor → Sketch)
|
||
|
||
```typescript
|
||
// Feed text from the Serial Monitor input box into the sketch:
|
||
simulator.serialWrite("COMMAND\n");
|
||
// → bytes queued in rp2040.uart[0].feedByte(charCode)
|
||
// → sketch reads them via Serial.read() / Serial.readString()
|
||
```
|
||
|
||
---
|
||
|
||
## 9. ADC — Analog Inputs
|
||
|
||
The RP2040 has a 12-bit ADC (0–4095) with 5 channels:
|
||
|
||
| ADC Channel | GPIO Pin | Arduino Alias | Default Value | Notes |
|
||
| ----------- | -------- | ------------- | ------------- | ----- |
|
||
| 0 | GPIO26 | A0 | 2048 (~1.65 V) | General purpose |
|
||
| 1 | GPIO27 | A1 | 2048 (~1.65 V) | General purpose |
|
||
| 2 | GPIO28 | A2 | 2048 (~1.65 V) | General purpose |
|
||
| 3 | GPIO29 | A3 | 2048 (~1.65 V) | General purpose |
|
||
| 4 | — (internal) | — | 876 (~27 °C) | Internal temperature sensor |
|
||
|
||
### Injecting ADC Values from UI Components
|
||
|
||
When a potentiometer is wired to an ADC pin on the canvas, the simulator canvas reads the potentiometer's value and injects it:
|
||
|
||
```typescript
|
||
// SimulatorCanvas — potentiometer input event
|
||
simulator.setADCValue(channel: number, value: number): void
|
||
// value range: 0–4095 (clamped automatically)
|
||
// → rp2040.adc.channelValues[channel] = value
|
||
```
|
||
|
||
`analogRead(A0)` in the sketch reads `adc.channelValues[0]` directly.
|
||
|
||
---
|
||
|
||
## 10. I2C Virtual Devices
|
||
|
||
The RP2040 emulator supports virtual I2C devices — software objects that respond to I2C bus transactions from the firmware:
|
||
|
||
```typescript
|
||
export interface RP2040I2CDevice {
|
||
address: number; // 7-bit I2C address
|
||
writeByte(value: number): boolean; // return true = ACK
|
||
readByte(): number;
|
||
stop?(): void;
|
||
}
|
||
```
|
||
|
||
### Default Virtual Devices (Auto-Registered)
|
||
|
||
Three virtual I2C devices are registered automatically when a Pico program is loaded:
|
||
|
||
| Device | Address | Bus | Description |
|
||
| ------ | ------- | --- | ----------- |
|
||
| `VirtualDS1307` | 0x68 | I2C0 | Real-time clock (DS1307 compatible) |
|
||
| `VirtualTempSensor` | 0x48 | I2C0 | Temperature sensor (LM75 / TMP102 compatible) |
|
||
| `I2CMemoryDevice` | 0x50 | I2C0 | 24C04 EEPROM (512 bytes) |
|
||
|
||
### Default I2C0 Pins
|
||
|
||
| Signal | GPIO |
|
||
| ------ | ---- |
|
||
| SDA | GPIO4 |
|
||
| SCL | GPIO5 |
|
||
|
||
### I2C Bus Event Flow
|
||
|
||
```text
|
||
firmware: Wire.beginTransmission(0x68)
|
||
│
|
||
▼ I2C master asserts START + address
|
||
rp2040.i2c[0].onConnect(address=0x68)
|
||
│
|
||
▼ device found in registry
|
||
ds1307.writeByte(register_address)
|
||
│
|
||
Wire.requestFrom(0x68, 7)
|
||
│
|
||
▼ 7 read calls
|
||
ds1307.readByte() × 7
|
||
│
|
||
▼ I2C STOP
|
||
ds1307.stop()
|
||
```
|
||
|
||
---
|
||
|
||
## 11. SPI
|
||
|
||
Both SPI buses implement a **loopback** by default — bytes transmitted by the master are immediately echoed back as received bytes:
|
||
|
||
```typescript
|
||
// Default: loopback
|
||
this.rp2040.spi[0].onTransmit = (value: number) => {
|
||
this.rp2040.spi[0].completeTransmit(value);
|
||
};
|
||
```
|
||
|
||
### Default SPI0 Pins
|
||
|
||
| Signal | GPIO |
|
||
| ------ | ---- |
|
||
| MISO | GPIO16 |
|
||
| CS | GPIO17 |
|
||
| SCK | GPIO18 |
|
||
| MOSI | GPIO19 |
|
||
|
||
### Custom SPI Handler
|
||
|
||
A custom handler can replace the loopback for simulating specific SPI peripherals (displays, sensors):
|
||
|
||
```typescript
|
||
simulator.setSPIHandler(bus: 0 | 1, handler: (value: number) => number): void
|
||
// handler receives TX byte, returns RX byte
|
||
```
|
||
|
||
---
|
||
|
||
## 12. PWM
|
||
|
||
PWM is available on any GPIO pin through the RP2040's hardware PWM slices. The rp2040js library emulates the PWM peripheral registers, so `analogWrite()` and `ledcWrite()` work at the firmware level.
|
||
|
||
Visual PWM feedback (LED dimming) in the simulator canvas uses the `onPwmChange` callback from `PinManager`, which receives the duty cycle as a 0.0–1.0 float and sets `el.style.opacity` on the LED element.
|
||
|
||
---
|
||
|
||
## 13. Simulation Execution Loop
|
||
|
||
The simulation runs at **60 FPS** using `requestAnimationFrame`. Each frame executes enough CPU cycles to match a 125 MHz clock:
|
||
|
||
```
|
||
F_CPU = 125,000,000 Hz
|
||
CYCLE_NANOS = 1e9 / F_CPU = 8 ns/cycle
|
||
CYCLES_PER_FRAME = F_CPU / 60 = 2,083,333 cycles/frame
|
||
```
|
||
|
||
### WFI Optimization
|
||
|
||
When the ARM core executes a **WFI** (Wait For Interrupt) instruction — which Arduino uses during `delay()` and `sleep()` — the loop skips ahead to the next pending timer alarm instead of executing millions of NOP-equivalent cycles:
|
||
|
||
```typescript
|
||
while (cyclesDone < cyclesTarget) {
|
||
if (core.waiting) {
|
||
// CPU is sleeping — jump to next alarm
|
||
const jump = clock.nanosToNextAlarm;
|
||
if (jump <= 0) break;
|
||
clock.tick(jump);
|
||
cyclesDone += Math.ceil(jump / CYCLE_NANOS);
|
||
} else {
|
||
// Execute one ARM instruction
|
||
const cycles = core.executeInstruction();
|
||
clock.tick(cycles * CYCLE_NANOS);
|
||
cyclesDone += cycles;
|
||
}
|
||
}
|
||
```
|
||
|
||
This allows `delay(1000)` to complete in microseconds of real time instead of simulating all 125 million cycles.
|
||
|
||
### Variable Speed
|
||
|
||
The simulation speed can be adjusted:
|
||
|
||
```typescript
|
||
simulator.setSpeed(speed: number): void
|
||
// speed: 0.1 (10% = very slow, for debugging) to 10.0 (10× faster)
|
||
```
|
||
|
||
---
|
||
|
||
## 14. Pin Mapping
|
||
|
||
The Pico has 40 physical pins. Below is the mapping from board pin names to GPIO numbers:
|
||
|
||
| Board Pin | GPIO | Function | Board Pin | GPIO | Function |
|
||
| --------- | ---- | -------- | --------- | ---- | -------- |
|
||
| GP0 | 0 | UART0 TX | GP15 | 15 | SPI1 TX |
|
||
| GP1 | 1 | UART0 RX | GP16 | 16 | SPI0 MISO |
|
||
| GP2 | 2 | I2C1 SDA | GP17 | 17 | SPI0 CS |
|
||
| GP3 | 3 | I2C1 SCL | GP18 | 18 | SPI0 SCK |
|
||
| GP4 | 4 | I2C0 SDA | GP19 | 19 | SPI0 MOSI |
|
||
| GP5 | 5 | I2C0 SCL | GP20 | 20 | — |
|
||
| GP6 | 6 | SPI0 SCK | GP21 | 21 | — |
|
||
| GP7 | 7 | SPI0 TX | GP22 | 22 | — |
|
||
| GP8 | 8 | UART1 TX | GP26 | 26 | ADC ch0 (A0) |
|
||
| GP9 | 9 | UART1 RX | GP27 | 27 | ADC ch1 (A1) |
|
||
| GP10 | 10 | SPI1 SCK | GP28 | 28 | ADC ch2 (A2) |
|
||
| GP11 | 11 | SPI1 TX | GP29 | 29 | ADC ch3 (A3) |
|
||
| GP12 | 12 | SPI1 RX | **GP25** | **25** | **LED_BUILTIN** |
|
||
| GP13 | 13 | SPI1 CS | — | — | VBUS, VSYS, 3V3, GND |
|
||
| GP14 | 14 | SPI1 SCK | — | — | RUN, ADC_VREF |
|
||
|
||
> All GPIO pins support PWM. GPIO 0–22 and 26–29 are available for general-purpose digital I/O. GPIO 23–25 are internal (power control, LED, SMPS mode).
|
||
|
||
---
|
||
|
||
## 15. Oscilloscope / Logic Analyzer
|
||
|
||
The RP2040 emulator provides timestamps for every GPIO transition using the internal simulation clock:
|
||
|
||
```typescript
|
||
// Callback fires on every GPIO state change
|
||
simulator.onPinChangeWithTime = (pin: number, state: boolean, timeMs: number) => {
|
||
// timeMs = rp2040.clock.timeUs / 1000
|
||
// Accurate to ~8 ns (one ARM clock cycle)
|
||
};
|
||
```
|
||
|
||
The oscilloscope store (`useOscilloscopeStore`) subscribes to this callback and records samples:
|
||
|
||
```text
|
||
GPIO event fired (pin=25, state=HIGH, timeMs=123.456)
|
||
│
|
||
▼
|
||
useOscilloscopeStore.pushSample(channelId, 123.456, true)
|
||
│
|
||
▼
|
||
Oscilloscope waveform display updates
|
||
```
|
||
|
||
This enables the built-in logic analyzer to display accurate waveforms for signals like PWM, UART bit patterns, and I2C clock/data.
|
||
|
||
---
|
||
|
||
## 16. Known Limitations
|
||
|
||
| Limitation | Detail |
|
||
| ---------- | ------ |
|
||
| Single-core only | RP2040 is dual-core; emulator runs core 0 only. Code that uses `multicore_launch_core1()` will not execute on core 1 |
|
||
| No PIO | PIO state machines return 0. Libraries that depend on PIO (WS2812 NeoPixels, DHT sensors, I2S audio, quadrature encoders) will not work |
|
||
| No WiFi (Pico W) | The CYW43439 wireless chip is not emulated; `WiFi.begin()` will not connect |
|
||
| No USB device | USB HID, CDC, and MIDI device modes are not emulated |
|
||
| No DMA | DMA transfers return without moving data; memcpy-based alternatives work fine |
|
||
| No hardware FPU | ARM Cortex-M0+ has no floating-point unit; float operations are emulated in software (slower, but correct) |
|
||
| Timing accuracy | Emulation runs at variable speed; `micros()` and `millis()` track simulated time, not wall-clock time |
|
||
| Flash writes | `LittleFS`, `EEPROM.commit()`, and other flash-write operations may not persist across simulated resets |
|
||
| Serial redirect | `Serial` is mapped to `Serial1` (UART1) at compile time; code that uses `Serial1` directly alongside `Serial` may behave unexpectedly |
|
||
|
||
---
|
||
|
||
## 17. Tests
|
||
|
||
RP2040 emulation tests are in `frontend/src/__tests__/`:
|
||
|
||
```bash
|
||
cd frontend
|
||
npm test -- RP2040
|
||
```
|
||
|
||
### Test Suites
|
||
|
||
| Suite | Tests | What It Verifies |
|
||
| ----- | ----- | ---------------- |
|
||
| Lifecycle | 4 | `create`, `loadBinary`, `start`, `stop`, `reset` idempotency |
|
||
| GPIO | 8 | All 30 pins, state injection, listener callbacks, multiple listeners |
|
||
| ADC | 3 | Channel access (0–4), value clamping to 0–4095, `analogRead` integration |
|
||
| Serial | 4 | `onSerialData` callback, `serialWrite` input feed, both UART channels |
|
||
| I2C | 5 | Virtual device registration, address matching, read/write/stop callbacks |
|
||
| SPI | 2 | Custom handler registration, loopback default |
|
||
| Bootrom | 1 | Loads without errors, PC resets to `0x10000000` |
|
||
|
||
---
|
||
|
||
## 18. Differences vs Other Emulators
|
||
|
||
| Aspect | Raspberry Pi Pico (RP2040) | Arduino AVR | ESP32-C3 (RISC-V) | ESP32 (Xtensa) |
|
||
| ------ | -------------------------- | ----------- | ------------------ | -------------- |
|
||
| Engine | `rp2040js` (browser) | `avr8js` (browser) | `RiscVCore.ts` (browser) | QEMU backend (WebSocket) |
|
||
| Backend dependency | **No** | **No** | **No** | Yes |
|
||
| Architecture | ARM Cortex-M0+ | AVR 8-bit | RISC-V RV32IMC | Xtensa LX6 |
|
||
| Clock speed | 125 MHz | 16 MHz | 160 MHz | 240 MHz |
|
||
| Binary format | `.bin` (raw) | `.hex` (Intel HEX) | `.bin` (merged flash) | `.bin` (merged flash) |
|
||
| Arduino framework | Full | Full | Partial (no FreeRTOS) | Full |
|
||
| Serial | UART0 + UART1 | USART0 | UART0 | UART0 |
|
||
| ADC | 4 external + 1 temp | 6 channels (10-bit) | Not emulated | Emulated |
|
||
| I2C | 2 buses + virtual devices | Not emulated | Not emulated | Emulated |
|
||
| SPI | 2 buses (loopback) | Not emulated | Not emulated | Emulated |
|
||
| PIO | **Not emulated** | N/A | N/A | N/A |
|
||
| PWM | All GPIO pins | Timer-based | Not emulated | LEDC (mapped) |
|
||
| Oscilloscope support | Yes (8 ns resolution) | Yes | Yes | No |
|
||
| CI tests | Yes (Vitest) | Yes | Yes | No |
|
||
|
||
---
|
||
|
||
## 19. Key Files
|
||
|
||
| File | Description |
|
||
| ---- | ----------- |
|
||
| `frontend/src/simulation/RP2040Simulator.ts` | Main emulator wrapper (GPIO, UART, ADC, I2C, SPI, RAF loop) |
|
||
| `frontend/src/simulation/rp2040-bootrom.ts` | RP2040 B1 bootrom binary (required for flash XIP) |
|
||
| `frontend/src/simulation/PinManager.ts` | Routes GPIO events to visual components on canvas |
|
||
| `frontend/src/store/useSimulatorStore.ts` | Zustand store — `compileBoardProgram()`, `startBoard()`, serial wiring |
|
||
| `frontend/src/components/simulator/SimulatorCanvas.tsx` | Canvas rendering, pin subscriptions, ADC/button event forwarding |
|
||
| `frontend/src/components/components-wokwi/PiPicoWElement.ts` | Pico W SVG board rendering and pin coordinate map |
|
||
| `frontend/src/__tests__/RP2040Simulator.test.ts` | Unit and integration tests |
|
||
| `backend/app/services/arduino_cli.py` | Compilation — detects RP2040 FQBN, encodes `.bin` as base64, prepends `Serial1` redirect |
|
||
| `wokwi-libs/rp2040js/` | Local clone of the rp2040js library (ARM Cortex-M0+ emulator) |
|