PyMCU vs Arduino#

Arduino is the dominant platform for beginner and hobbyist embedded development. This page shows how PyMCU compares — what works the same, what works differently, and where PyMCU goes beyond what Arduino offers.


The pitch#

Arduino made microcontrollers accessible with a simplified C++ API. PyMCU does the same for Python: write familiar Python syntax, get the same tight native code you’d get from C.

```cpp void setup() { pinMode(13, OUTPUT); }

void loop() { digitalWrite(13, HIGH); delay(500); digitalWrite(13, LOW); delay(500); } ```

PyMCU (Python)

```python from pymcu.hal.gpio import Pin from pymcu.time import delay_ms

def main(): led = Pin(“PB5”, Pin.OUT) # D13 on Uno while True: led.toggle() delay_ms(500) ```


Both compile to the same 124-byte AVR assembly.

---

## Function equivalents

| Arduino function | PyMCU equivalent | Notes |
|------------------|-----------------|-------|
| `pinMode(n, OUTPUT)` | `Pin("PB5", Pin.OUT)` | Pin name is compile-time typed |
| `digitalWrite(n, HIGH)` | `led.high()` | Inlines to a single `SBI` |
| `digitalRead(n)` | `led.value` | Single `SBIS`/`SBIC` |
| `analogRead(n)` | `AnalogPin(n).read()` | 10-bit, same as Arduino |
| `analogWrite(n, v)` | `PWM("PD6", v).start()` | Hardware PWM channels only |
| `delay(ms)` | `delay_ms(ms)` | Exact same semantics |
| `millis()` | `millis()` | From `pymcu.time` |
| `micros()` | `micros()` | From `pymcu.time` |
| `tone(pin, freq)` | `tone(freq)` | Hardware toggle on OC2A (D11) |
| `noTone(pin)` | `noTone()` | |
| `map(x, ...)` | `map_range(x, ...)` | From `pymcu.math` |
| `constrain(x, lo, hi)` | `constrain(x, lo, hi)` | From `pymcu.math` |
| `random(n)` | `random(n)` | From `pymcu.random` |
| `randomSeed(s)` | `randomSeed(s)` | From `pymcu.random` |
| `attachInterrupt(pin, fn, FALLING)` | `pin.irq(fn, Pin.IRQ_FALLING)` | |
| `Serial.begin(baud)` | `UART(baud)` | |
| `Serial.print(x)` | `uart.write_str(x)` | |
| `Serial.println(x)` | `uart.println(x)` | |
| `EEPROM.read(addr)` | `eeprom.read(addr)` | |
| `EEPROM.write(addr, v)` | `eeprom.write(addr, v)` | |
| `Wire.begin()` | `I2C(0)` | master mode |
| `Wire.beginTransmission(addr)` + `Wire.write(b)` | `i2c.write_to(addr, buf, n)` | |
| `SPI.begin()` + `SPI.transfer(b)` | `SPI(SPI.CONTROLLER).transfer(b)` | |
| `Servo.write(degrees)` | `Servo("PB1").write(degrees)` | Timer1, D9 or D10 |

### MicroPython / CircuitPython compat layer

If you already know MicroPython, import the compat shim and use the API you know:

```python
from machine import Pin, UART, SPI, I2C, ADC, Timer, PWM
import utime

The compat layer is a thin compile-time alias — it produces identical machine code to the native PyMCU HAL.


What PyMCU adds#

1. Static typing catches errors at compile time#

duty: uint8 = read_sensor()  # uint8 — compiler enforces the type
pwm.set_duty(duty)           # no runtime type check needed

Arduino has no type checking. A silent integer truncation in C++ can cause subtle hardware bugs that are nearly impossible to find with a serial monitor.

2. Source-level debugger#

PyMCU ships a full VS Code DAP debugger — set breakpoints directly on Python source lines, step through code, inspect all 32 registers and SRAM in real time.

Arduino’s only debugging tool is Serial.print().

PyMCU VS Code debugger showing breakpoint on line 42

3. CPU profiler with flamegraphs#

pymcu profile --ms 5000 -o profile.speedscope.json

Drag profile.speedscope.json onto speedscope.app to see exactly where your firmware spends CPU time, down to the individual function call. Works with multi-task RTOS firmware — each task gets its own profile tab.

No equivalent exists for Arduino.

4. RTOS in Python#

from rtos import add_task, start_scheduler, Priority

add_task(sensor_task, Priority.HIGH)    # 50% CPU
add_task(display_task, Priority.NORMAL) # 25% CPU
add_task(blink_task,   Priority.IDLE)   # 12.5% CPU
start_scheduler()  # preemptive, 1 ms tick

A production-grade preemptive RTOS in pure Python, compiled to tight AVR assembly. See the rtos-multitask example.

5. Zero-cost abstractions#

Every HAL method is @inline — no function call overhead, no virtual dispatch. The compiler folds chip-dispatch match blocks at compile time and emits exactly the instructions for the selected hardware.

A led.toggle() call on ATmega328P compiles to a single SBIS PORTB,5 / RJMP +2 / SBI PORTB,5 / RJMP +2 / CBI PORTB,5 sequence — the same as hand-written assembly.

6. C interop via @extern#

from pymcu.ffi import extern

@extern("crc8_maxim", header="crc.h")
def crc8(data: ptr[uint8], len: uint8) -> uint8: ...

Call any C library function directly from Python. PyMCU handles the calling convention; no wrapper glue code needed.


What Arduino still has#

Arduino advantage

Notes

Library ecosystem

4000+ libraries; PyMCU has a growing stdlib + FFI for C libs

More boards

ESP32, STM32, RP2040, SAMD, nRF52 (PyMCU: AVR + PIC + RISC-V, ARM coming)

Arduino IDE simplicity

PyMCU uses VS Code; more powerful but higher initial setup

Community / tutorials

Vast corpus of Arduino examples


Code size comparison#

Compiling blink (toggle D13, delay 500 ms) for Arduino Uno (16 MHz):

Toolchain

Flash

SRAM

Arduino (avr-gcc -Os)

~928 bytes

9 bytes

PyMCU

124 bytes

0 bytes

PyMCU eliminates the Arduino runtime overhead (init code, millis ISR, main() wrapper, Serial globals) that is linked in even for simple programs.


Summary#

PyMCU is the right choice when you want:

  • Python syntax with zero runtime overhead

  • Static typing to catch mistakes at compile time

  • Professional tooling: VS Code debugger, Speedscope profiler

  • RTOS without a heavy C library

  • Minimal flash footprint on constrained chips (ATtiny, ATmega48)

Arduino is the right choice when you need:

  • The vast existing library ecosystem

  • Boards beyond AVR (ESP32, STM32, RP2040)

  • A simpler zero-config IDE