Introduction to MicroPython
What is MicroPython?
MicroPython is a lightweight and efficient implementation of Python designed specifically for microcontrollers and embedded systems. It brings the ease and readability of Python to hardware programming, making it an excellent choice for beginners and experienced developers alike. Unlike standard Python, MicroPython includes libraries and modules tailored for controlling hardware components such as GPIO pins, I2C, and SPI interfaces.
Why use MicroPython on the Raspberry Pi Pico2?
The Raspberry Pi Pico2 is a powerful yet affordable microcontroller based on the RP2350 chip. MicroPython is an excellent choice for programming the Pico because:
- Ease of Use: MicroPython simplifies embedded programming by eliminating complex build systems and compilation steps.
- Interactivity: The REPL (Read-Eval-Print Loop) allows for immediate testing and debugging of code.
- Rich Standard Library: Many built-in functions make development faster.
- Cross-Platform Development: Code written in MicroPython is easier to port between different microcontrollers.
- Community Support: A strong community and extensive documentation make it easier to find solutions to common problems.
Raspberry Pi Pico vs. Pico W vs. Pico 2
The Raspberry Pi Pico family consists of multiple versions, primarily the Pico, Pico W, and Pico 2:
| Feature | Raspberry Pi Pico | Raspberry Pi Pico W | Raspberry Pi Pico 2 |
|---|---|---|---|
| Chip | RP2040 (Dual-core Cortex-M0+) | RP2040 (Dual-core Cortex-M0+) | RP2350 (Dual-core Cortex-M33) |
| WiFi Support | No | Yes (built-in Infineon CYW43439 WiFi chip) | No |
| Bluetooth | No | No (though the chip supports Bluetooth, it's currently not enabled) | No |
| GPIO Pins | 26 | 26 | 30 |
| Power Consumption | Lower | Slightly higher due to WiFi module | Optimized for efficiency |
| Performance | Standard | Standard with WiFi overhead | Higher (Cortex-M33 improvements) |
The Pico W is ideal for IoT applications requiring wireless connectivity, while the Pico is better suited for projects that do not need WiFi and require lower power consumption. The Pico 2, based on the RP2350, provides improved performance and additional GPIOs, making it a strong candidate for more demanding embedded applications.
Differences between C and MicroPython in Embedded Programming
MicroPython and C are two popular choices for embedded development, but they differ in various aspects:
| Feature | MicroPython | C |
|---|---|---|
| Ease of Learning | High (simplified syntax, no need for pointers) | Moderate to Hard (requires memory management, pointers) |
| Execution Speed | Slower (interpreted) | Faster (compiled) |
| Development Speed | Fast (no compilation, interactive testing) | Slower (compilation required) |
| Code Size | Larger due to interpreter overhead | Smaller, optimized for hardware |
| Hardware Access | Simplified through built-in modules | Full control over hardware |
| Portability | High (works across different microcontrollers) | Requires porting effort |
Here’s a simple LED blinking example in both C and MicroPython:
C Code (Using SDK):
#include "pico/stdlib.h"
int main() {
gpio_init(25);
gpio_set_dir(25, GPIO_OUT);
while (1) {
gpio_put(25, 1);
sleep_ms(500);
gpio_put(25, 0);
sleep_ms(500);
}
}
MicroPython Code:
from machine import Pin
from time import sleep
led = Pin("LED", Pin.OUT) # Onboard LED on Pico/Pico 2
while True:
led.toggle()
sleep(0.5)
Key Takeaways:
- Simplicity: MicroPython code is shorter and easier to read.
- Interactivity: You can test MicroPython code in real-time from REPL.
- Performance: C is faster and more memory-efficient, making it ideal for time-critical applications.
How Slow is Python, Really?
Let's measure it! Here's what a simple pin toggle takes on a Pico 2:
| Operation | Time per call | Speed |
|---|---|---|
Python led.on()/led.off() |
~535 µs | ~1,900 toggles/sec |
| Python empty loop iteration | ~2.6 µs | ~385,000 iter/sec |
| Direct register write (Python) | ~6 µs | ~167,000 writes/sec |
| C compiled code | ~0.008 µs (8 ns) | ~125,000,000 ops/sec |
| PIO hardware | 0.1 µs (100 ns) | 10,000,000 ops/sec |
Python is ~67,000x slower than C for pin operations!
Why Does This Matter?
Some hardware protocols require nanosecond precision:
WS2812B (NeoPixel) LED protocol:
├── '0' bit: 350ns HIGH, 900ns LOW
├── '1' bit: 800ns HIGH, 450ns LOW
└── Tolerance: ±150ns
Python speed: 535,000 ns per toggle
Required speed: 350 ns per toggle
─────────────────────
Python is 1,500x TOO SLOW!
So Why Use MicroPython at All?
Because most embedded tasks don't need nanosecond timing:
| Task | Timing Needed | Python OK? |
|---|---|---|
| Read button state | ~10 ms | ✓ Yes |
| Read temperature sensor | ~100 ms | ✓ Yes |
| Control motor speed | ~1 ms | ✓ Yes |
| Update display | ~50 ms | ✓ Yes |
| Decision logic | ~1 ms | ✓ Yes |
| WS2812B LED protocol | ~350 ns | ✗ No |
| Ultrasonic echo timing | ~1 µs | ✗ Marginal |
For timing-critical tasks, the Pico has hardware peripherals that run independently:
┌─────────────────────────────────────────────────────────┐
│ Pico Architecture │
│ │
│ ┌─────────────┐ Your code runs here │
│ │ MicroPython │ (slow but easy) │
│ │ Interpreter │ │
│ └──────┬──────┘ │
│ │ "Hey PIO, send this LED data" │
│ ▼ │
│ ┌─────────────┐ Hardware runs here │
│ │ PIO / PWM │ (fast and precise) │
│ │ Hardware │ Runs INDEPENDENTLY! │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ GPIO Pins ────► LEDs, Motors, Sensors │
└─────────────────────────────────────────────────────────┘
The Best of Both Worlds
MicroPython lets you:
- Write simple code for logic, decisions, and user interaction
- Use hardware peripherals (PIO, PWM, DMA) for timing-critical operations
- Focus on learning concepts, not fighting compiler errors
The Embedded Developer's Rule
"Use software for thinking, use hardware for timing."
- Python handles: logic, state machines, sensor processing, communication
- Hardware handles: precise PWM, LED protocols, motor control signals, accurate measurements
This is why professional embedded systems often use a high-level language for application logic and hardware peripherals or C for timing-critical drivers - you'll see this pattern everywhere from Arduino to industrial PLCs.
MicroPython in Production: Not Just for Prototyping
A common misconception: "MicroPython is for learning, C is for real products."
Reality: Many production embedded systems use Python (or similar high-level languages) deliberately. The key is architectural separation:
┌──────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ MicroPython / High-Level Code │ │
│ │ • State machines • Communication protocols │ │
│ │ • Business logic • Configuration & settings │ │
│ │ • User interaction • OTA updates │ │
│ └────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ API calls │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ C / SDK / Hardware Drivers │ │
│ │ • PIO programs • DMA transfers │ │
│ │ • Interrupt handlers • Timing-critical code │ │
│ │ • Low-level protocols • Hardware abstraction │ │
│ └────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Hardware │ │
│ │ GPIO / PIO / PWM / DMA / UART / I2C │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Why Professionals Choose MicroPython
| Reason | Explanation |
|---|---|
| Development Speed | No compile-flash cycles. Hardware teams often finish late—software must adapt fast. |
| Field Updates | Change behavior by uploading a .py file. No reflashing, no bricking risk. |
| Team Separation | Firmware engineers write C drivers; application engineers write Python logic. |
| Safety | Python crash → restart interpreter. C bug → potential hardware lockup or brick. |
| Readability | Non-embedded specialists can understand and modify behavior code. |
When MicroPython is NOT Appropriate
| Use Case | Why Not |
|---|---|
| Hard real-time control loops | Jitter from garbage collection |
| Sub-microsecond timing | Interpreter overhead |
| Bit-banged protocols | Too slow for precise timing |
| Maximum power efficiency | Interpreter uses more resources |
| Safety-critical systems | Certification requirements |
These belong in C / PIO / DMA.
The Teaching Value
This course uses MicroPython because it lets you focus on:
- Concepts (state machines, control loops, sensor fusion)
- Architecture (how to structure embedded software)
- Problem-solving (debugging, testing, iteration)
Without drowning in:
- Linker scripts and memory maps
- Toolchain setup and CMake
- Pointer arithmetic and memory management
- Hours of compile-flash-debug cycles
Then, when you understand why timing matters (because you've seen Python fail at WS2812B!), the motivation for learning C/SDK becomes clear.
The Course Philosophy
"Understand the problem before optimizing the solution."
We deliberately use a "slow" framework so you can: 1. See timing failures happen (NeoPixels glitching) 2. Understand why they happen (Python interpreter overhead) 3. Appreciate how hardware peripherals solve them (PIO) 4. Know when to reach for C/SDK in your career
Going Further: The Pico C/C++ SDK
Advanced Topic
This section is for students who want to understand what "bare-metal" programming looks like. You don't need this for the course, but it helps understand what MicroPython is abstracting away.
When you need deterministic timing or maximum performance, the Pico C/C++ SDK provides direct hardware access.
What "Using the SDK" Means
| MicroPython | Pico SDK |
|---|---|
Write .py files |
Write .c files |
| Run directly via REPL | Build with CMake, flash .uf2 |
| Interpreter handles hardware | You control everything |
| ~535 µs per GPIO toggle | ~8 ns per GPIO toggle |
Minimal SDK Example
Project structure:
main.c - LED blink in C:
#include "pico/stdlib.h"
int main() {
const uint LED_PIN = 25;
gpio_init(LED_PIN);
gpio_set_dir(LED_PIN, GPIO_OUT);
while (true) {
gpio_put(LED_PIN, 1);
sleep_ms(500);
gpio_put(LED_PIN, 0);
sleep_ms(500);
}
}
Build and flash:
mkdir build && cd build
cmake ..
make
# Hold BOOTSEL, plug in Pico, copy .uf2 file
cp my_project.uf2 /media/$USER/RPI-RP2/
Concept Mapping: MicroPython → SDK
| Concept | MicroPython | Pico SDK |
|---|---|---|
| GPIO control | Pin() |
gpio_init(), gpio_put() |
| Delays | time.sleep() |
sleep_ms() |
| PWM | PWM() |
pwm_init(), pwm_set_wrap() |
| PIO | @rp2.asm_pio |
.pio files (compiled) |
| Serial output | print() |
printf() (via stdio_init_all()) |
| Build process | None (interpreted) | CMake → Make → .uf2 |
When to Consider SDK
Start here: MicroPython
│
▼
Does it work? ─── Yes ──► Ship it! ✓
│
No (timing issues)
│
▼
Can PIO/PWM/DMA fix it? ─── Yes ──► Use hardware peripheral ✓
│
No (need full control)
│
▼
Rewrite critical part in C/SDK
Keep Python for application logic
Resources
The Hybrid Pattern: C Library + Python Application
In production systems, you often don't choose between MicroPython OR C — you use both:
┌─────────────────────────────────────────────────────────────┐
│ Your Application (MicroPython) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ from led_driver import set_pixel, show │ │
│ │ │ │
│ │ # High-level logic in Python │ │
│ │ if sensor.detected(): │ │
│ │ set_pixel(0, RED) │ │
│ │ else: │ │
│ │ set_pixel(0, GREEN) │ │
│ │ show() │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ Python calls C │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Native C Module (compiled into firmware) │ │
│ │ │ │
│ │ // Timing-critical code in C │ │
│ │ void set_pixel(int idx, uint32_t color) { │ │
│ │ led_buffer[idx] = color; │ │
│ │ } │ │
│ │ void show() { │ │
│ │ pio_sm_put_blocking(pio0, 0, led_buffer); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ C controls hardware │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Hardware (PIO state machine) │ │
│ │ Generates precise 800kHz WS2812 timing │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
This is exactly how MicroPython's built-in modules work!
When you write from neopixel import NeoPixel, you're using a C module that:
- Exposes a simple Python API (leds[0] = (255, 0, 0))
- Internally uses PIO hardware for precise timing
- Was compiled into the MicroPython firmware
You're Already Using This Pattern
Every machine.Pin(), machine.PWM(), and neopixel.NeoPixel() call is Python talking to C code that controls hardware. The architecture you've been learning about is what you've been using all along!
Example: What a C Driver Looks Like
Here's a simplified WS2812 driver in C (what runs "behind" MicroPython's NeoPixel):
// ws2812_driver.c - Compiled into MicroPython firmware
#include "hardware/pio.h"
#include "ws2812.pio.h" // PIO program (generated from .pio file)
static uint32_t led_buffer[8];
static PIO pio = pio0;
static uint sm = 0;
// Called once at startup
void ws2812_init(uint pin, uint num_leds) {
uint offset = pio_add_program(pio, &ws2812_program);
ws2812_program_init(pio, sm, offset, pin, 800000);
}
// Called from Python: set_pixel(index, color)
void ws2812_set_pixel(uint index, uint32_t grb) {
led_buffer[index] = grb; // Just store in buffer (fast!)
}
// Called from Python: show()
void ws2812_show(void) {
for (int i = 0; i < 8; i++) {
pio_sm_put_blocking(pio, sm, led_buffer[i] << 8);
}
}
Key insight: The C code is simple! Most of the work is done by the PIO hardware. C just: 1. Initializes the PIO state machine (once) 2. Stores colors in a buffer (fast) 3. Sends buffer to PIO (hardware does the timing)
The PIO Program (Assembly)
The actual timing-critical code runs on PIO hardware, written in PIO assembly:
; ws2812.pio - Runs on PIO hardware, NOT on CPU!
.program ws2812
.side_set 1
.wrap_target
bitloop:
out x, 1 side 0 [T3 - 1] ; Shift 1 bit, drive low
jmp !x do_zero side 1 [T1 - 1] ; If 0: short high pulse
do_one:
jmp bitloop side 1 [T2 - 1] ; If 1: long high pulse
do_zero:
nop side 0 [T2 - 1] ; Pad low time
.wrap
This runs at 10MHz, independent of Python or even C!
The CPU (running Python or C) just feeds data to PIO. PIO generates the precise nanosecond pulses.
For Curious Students: Building Custom C Modules
If you want to add your own C code to MicroPython:
- User C Modules — Add C code that compiles with MicroPython firmware
- Rebuild firmware — Compile MicroPython with your module included
- Flash and use —
import your_modulein Python
Resources: - MicroPython: Extending with C - Pico SDK + MicroPython Integration
This is advanced — but now you understand WHY it exists.
Summary: The Complete Picture
┌────────────────────────────────────────────────────────────────┐
│ EMBEDDED SYSTEM ARCHITECTURE │
├────────────────────────────────────────────────────────────────┤
│ │
│ WHAT │ LANGUAGE │ SPEED │ USED FOR │
│ ────────────────────────────────────────────────────────── │
│ Application │ Python │ ~1 ms │ Logic, │
│ Logic │ │ │ decisions, │
│ │ │ │ state │
│ ────────────────────────────────────────────────────────── │
│ Hardware │ C / SDK │ ~1 µs │ Drivers, │
│ Drivers │ │ │ protocols, │
│ │ │ │ buffering │
│ ────────────────────────────────────────────────────────── │
│ Hardware │ PIO / PWM │ ~100 ns │ Precise │
│ Peripherals │ / DMA │ │ timing, │
│ │ │ │ waveforms │
│ │
└────────────────────────────────────────────────────────────────┘
"Software for thinking, hardware for timing"
In this course, you work at the top layer (Python) and use pre-built hardware peripherals (PIO via NeoPixel library, PWM via machine.PWM).
In your career, you may also work at the middle layer (C drivers) or even the bottom layer (PIO programs) — and now you understand how they all fit together.
In summary, MicroPython is a great language that provides good tools for prototyping and production. The key is knowing when to let hardware handle timing, and when to reach for C/SDK for full control.
Info
You can find a more detailed article about the basics of the MicroPyhton and compered to C here.