Execution Models in Embedded Systems
This reference explains the spectrum of execution models, from pure bare-metal C to full operating systems, and where MicroPython fits.
The Execution Model Spectrum
← Maximum control Maximum abstraction →
← Maximum responsibility Minimum responsibility →
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ BARE-METAL │ │ MICROPYTHON │ │ RTOS │ │ EMBEDDED │ │ DESKTOP │
│ C │ │ on bare │ │ (FreeRTOS, │ │ LINUX │ │ OS │
│ │ │ metal │ │ Zephyr) │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
▲ ▲ ▲ ▲ ▲
│ │ │ │ │
No OS No OS Kernel Full kernel Full OS
No runtime Has runtime Scheduler Processes Processes
You do all Runtime helps Tasks help Isolation Full services
Pure Bare-Metal C
What it means: Your compiled code IS the firmware. Nothing else runs.
┌─────────────────────────────────┐
│ Your C Code (compiled) │ ← You write everything
├─────────────────────────────────┤
│ Startup code (reset vector) │ ← Minimal, often SDK-provided
├─────────────────────────────────┤
│ Hardware │
└─────────────────────────────────┘
What You Get
| Aspect | Details |
|---|---|
| Direct hardware access | Write to registers directly, no abstraction layers |
| Deterministic timing | Code executes predictably, no surprise pauses |
| Minimal memory footprint | Only your code, no runtime overhead |
| Full ISR control | Do anything in interrupt handlers (carefully) |
What You Don't Get
| Aspect | Details |
|---|---|
| No OS-provided memory protection | A bug can corrupt any memory |
| No preemptive scheduler | Unless you implement one or add RTOS |
| No OS-provided drivers | Use SDK/HAL or write your own |
| No safety net | Null pointer = hard fault, no traceback |
Drivers in Bare-Metal
"No device drivers" doesn't mean you have no drivers. It means:
- No OS-provided drivers (no
/dev/ttyUSB0, no kernel modules) - You use SDK drivers (Pico SDK provides
gpio_put(),i2c_write_blocking()) - Or write your own (direct register access)
// Using SDK driver (abstraction)
gpio_put(25, 1);
// Or direct register access (no abstraction)
*(volatile uint32_t*)(SIO_BASE + 0x014) = (1 << 25);
MicroPython on Bare-Metal
What it means: The MicroPython firmware runs on bare-metal, your Python runs inside it.
┌─────────────────────────────────┐
│ Your Python Script │ ← You write this
├─────────────────────────────────┤
│ MicroPython Runtime (~256KB) │
│ ┌───────────────────────────┐ │
│ │ Python VM / Interpreter │ │ ← Executes bytecode
│ │ Garbage Collector │ │ ← Automatic memory
│ │ Exception Handling │ │ ← Tracebacks
│ │ Built-in Modules │ │ ← machine, time, etc.
│ │ REPL │ │ ← Interactive debug
│ │ Filesystem (optional) │ │ ← littlefs on flash
│ └───────────────────────────┘ │
├─────────────────────────────────┤
│ Hardware (RP2350) │
└─────────────────────────────────┘
Is MicroPython "Bare-Metal"?
Yes, in the sense that: - No Linux/Windows/macOS underneath - No kernel, no processes, no virtual memory - The firmware runs directly on the MCU
No, in the sense that: - Your code runs inside a substantial runtime - You don't have direct register access (by default) - The interpreter adds overhead and non-determinism
What MicroPython Gives You
| Aspect | C Bare-Metal | MicroPython |
|---|---|---|
| Memory management | Manual (malloc/free) |
Automatic (GC) |
| Error handling | Hard fault, debugging required | Exception + traceback |
| Development cycle | Edit → Compile → Flash → Run | Edit → Run |
| Drivers | Write or use SDK | Pre-written (machine module) |
| Interactive debug | JTAG/SWD, printf | REPL, inspect live |
What You Trade Away
| Aspect | C Bare-Metal | MicroPython |
|---|---|---|
| Timing precision | Deterministic (µs) | GC pauses (1-10ms) |
| ISR flexibility | Full control | Limited (no allocation) |
| Execution speed | ~10ns per op | ~1µs per op (100x slower) |
| Memory efficiency | Only your code | +256KB runtime |
| Register access | Direct, fast | Possible but awkward |
When GC Pauses Matter
See this in Lab 05
Lab 05 demonstrates how blocking operations (like ultrasonic reads) destroy control loop timing - similar to GC pauses. → Ultrasonic & Timing tutorial
# GC pause can happen at ANY allocation
while True:
data = sensor.read() # Might trigger GC here
result = process(data) # Or here (creates objects)
motor.set(result) # Or here
For 50-100 Hz control loops: GC pause of 5ms is ~50% of a 10ms period. Usually fine, but measure!
For audio/video or fast protocols: GC pause is unacceptable. Use C or hardware (PIO).
ISR Limitations in MicroPython
# WRONG - allocates memory in ISR
def bad_isr(pin):
message = f"Pin {pin} triggered" # String allocation!
data.append(time.ticks_ms()) # List growth!
# CORRECT - minimal ISR, process in main loop
flag = False
def good_isr(pin):
global flag
flag = True # Just set flag, no allocation
while True:
if flag:
flag = False
handle_event() # Do real work here
RTOS (Real-Time Operating System)
What it means: A small kernel provides scheduling, but no full OS services.
┌─────────────────────────────────┐
│ Task 1 Task 2 Task 3 │ ← Your code in tasks
├─────────────────────────────────┤
│ RTOS Kernel (~5-20KB) │
│ - Scheduler (preemptive) │
│ - Task management │
│ - Synchronization primitives │
├─────────────────────────────────┤
│ Hardware │
└─────────────────────────────────┘
What RTOS Adds
| Feature | Bare-Metal | RTOS |
|---|---|---|
| Task scheduling | You manage timing | Kernel schedules |
| Priority | Manual in loop | Preemption by priority |
| Blocking | Stops everything | Only stops that task |
| Synchronization | DIY (flags, etc.) | Mutex, semaphore, queue |
When to Use RTOS
Use RTOS when: - 3+ tasks with different timing needs - Some tasks are time-critical (must never be delayed) - Tasks need to block without affecting others - You need proven synchronization primitives
Stay with bare-metal/super-loop when: - Simple system (1-2 main activities) - Timing can be managed with careful loop design - Memory is very constrained (<16KB) - Learning fundamentals (this course!)
Embedded Linux
What it means: Full Linux kernel, but on embedded hardware.
┌─────────────────────────────────┐
│ Your Application (process) │
├─────────────────────────────────┤
│ Linux Kernel │
│ - Process isolation │
│ - Virtual memory │
│ - Device drivers │
│ - Filesystem │
│ - Networking stack │
├─────────────────────────────────┤
│ Hardware (needs MMU, >64MB RAM)│
└─────────────────────────────────┘
When to Use Embedded Linux
- Complex applications (GUI, networking, databases)
- Need standard libraries and tools
- Hardware has MMU and sufficient RAM (>64MB)
- Timing requirements are soft (ms, not µs)
When NOT to Use
- Hard real-time requirements (µs precision)
- Very resource-constrained (<1MB RAM)
- Boot time matters (Linux takes seconds)
- Power consumption is critical
Summary Comparison
| Aspect | Bare-Metal C | MicroPython | RTOS | Embedded Linux |
|---|---|---|---|---|
| OS underneath | None | None | Kernel only | Full kernel |
| Your code is | The firmware | Hosted by runtime | Tasks | Process |
| Timing precision | µs | ms (GC pauses) | µs | ms |
| Memory overhead | Minimal | ~256KB | ~5-20KB | >8MB |
| Development speed | Slow | Fast | Medium | Medium |
| Debugging | JTAG, printf | REPL, exceptions | JTAG, traces | gdb, logs |
| Best for | Production, safety | Prototyping, learning | Multi-task RT | Complex apps |
This Course: MicroPython on Bare-Metal
We use MicroPython because:
- Fast iteration - 10x faster than C compile cycle
- Forgiving - Exceptions instead of hard faults
- Focus on concepts - Not fighting syntax
- Still "bare-metal enough" - No OS, direct hardware access
The concepts (GPIO, PWM, control loops, timing) transfer directly to C when you need production performance.