When makers search for "vm pico" troubleshooting, they are almost always hitting a wall with the MicroPython Virtual Machine (VM) running on the RP2040 chip. Unlike compiled C code that runs directly on the silicon, MicroPython executes bytecode interpreted by a VM. This VM requires a dedicated heap in the RP2040’s 264KB SRAM. After the firmware and C-stack claim their share, you are left with roughly 190KB of usable Python heap. When your code crashes, it is usually a VM heap exhaustion issue, not a hardware failure.
This guide targets the Raspberry Pi Pico W running MicroPython v1.22 or newer. We will build a testbench to intentionally trigger VM crashes, catch them, and apply concrete fixes to stabilize your embedded projects.
The MicroPython VM on Pico: Architecture and Memory Limits
The RP2040 is a capable dual-core ARM Cortex-M0+ running at 133MHz, but the MicroPython VM abstracts the hardware. Every variable, list, and object you create in Python is dynamically allocated on the VM heap. Because the Pico lacks an MMU (Memory Management Unit) and runs bare-metal, there is no virtual memory swapping to flash. When the 190KB heap is full, the VM panics.
Time Required: 45 minutes
Core Concept: VM heap management, garbage collection, and bytecode optimization.
Understanding the difference between flash storage (2MB on the Pico W, used for saving your .py files) and SRAM (264KB, used for the VM heap at runtime) is the first step to debugging memory errors. You can have a 1MB script saved on the Pico, but if executing it requires 200KB of active objects, the VM will crash.
Parts List and Pin Mapping for the Debugging Testbench
To demonstrate VM memory limits and error handling, we will build a simple data-logging circuit. We will use the Pico's internal temperature sensor to generate a continuous data stream, pushing the VM heap to its limits.
Spec Sheet & Parts List
| Component | Exact Variant / Specification | Purpose |
|---|---|---|
| Microcontroller | Raspberry Pi Pico W (RP2040, 2MB QSPI, CYW43439) | Target board running the MicroPython VM |
| Status LED | Standard 5mm Red LED with 330Ω current-limiting resistor | Visual indicator for VM crash/recovery state |
| Sensor | Internal RP2040 Temperature Sensor (ADC Channel 4) | Generates continuous data to fill the VM heap |
| Wiring | 22 AWG solid core jumper wires | Breadboard connections |
Pin Mapping Table
| Pico W Pin | GPIO / Function | Connected To |
|---|---|---|
| GPIO 15 | Digital Output | External LED Anode (via 330Ω resistor) |
| GND (Pin 18) | Ground | External LED Cathode |
| ADC_VREF / Temp | ADC Channel 4 (Internal) | No external wiring required |
Note: We use an external LED on GPIO 15 because the Pico W’s onboard LED is wired to the WiFi chip's WL_GPIO0, which requires initializing the network stack just to blink it—a waste of precious VM heap memory for a simple status indicator.
Complete Test Code with VM Error Handling
The following MicroPython script is designed to intentionally exhaust the VM heap by appending floating-point temperature readings to a native Python list. It includes robust error handling to catch the VM crash, trigger garbage collection, and safely recover without requiring a hard reset.
import machine
import gc
import time
# --- Pin Definitions ---
# Using external LED on GPIO 15 to avoid Pico W WiFi chip overhead
STATUS_LED = machine.Pin(15, machine.Pin.OUT)
TEMP_SENSOR = machine.ADC(machine.ADC.CORE_TEMP)
# Conversion factor for RP2040 internal temp sensor (3.3V reference, 16-bit ADC)
CONVERSION_FACTOR = 3.3 / 65535
def read_temp_c():
"""Reads the internal temperature sensor and returns Celsius."""
reading = TEMP_SENSOR.read_u16() * CONVERSION_FACTOR
return 27 - (reading - 0.706) / 0.001721
def main():
data_log = [] # Native Python list (high VM memory overhead per item)
loop_count = 0
print("Starting VM Pico Memory Stress Test...")
print(f"Initial VM Heap Free: {gc.mem_free()} bytes")
while True:
try:
# Flash LED to show VM is actively executing
STATUS_LED.toggle()
temp = read_temp_c()
# Appending floats to a list creates new objects, fragmenting the heap
data_log.append(temp)
loop_count += 1
if loop_count % 1000 == 0:
print(f"Logged {loop_count} entries. Heap free: {gc.mem_free()} bytes")
# Force garbage collection to see true available contiguous memory
gc.collect()
except MemoryError as e:
# Catch the exact VM crash
print(f"\n[CRITICAL] VM Crash Caught: {e}")
print(f"Crashed at {loop_count} entries.")
# Recovery sequence
STATUS_LED.value(1) # Solid ON indicates fault state
print("Clearing data log and forcing GC...")
data_log = [] # Dereference the massive list
gc.collect() # Reclaim the heap
print(f"Recovered. Heap free: {gc.mem_free()} bytes\n")
# Prevent immediate re-triggering by adding a delay
time.sleep(2)
STATUS_LED.value(0)
loop_count = 0
except Exception as e:
print(f"Unexpected VM Error: {e}")
machine.reset()
time.sleep(0.01) # 10ms sample rate
if __name__ == "__main__":
main()
Debugging the VM: Exact Error Strings and Ranked Causes
When the MicroPython VM runs out of memory, it doesn't just silently fail; it throws specific exceptions. Here is how to interpret the exact error strings you will see in your Thonny or PuTTY serial console.
The First Three Things to Check When It Fails
- Heap Fragmentation: You might have 20KB "free", but if it's broken into 100-byte chunks, a single 5KB allocation will fail. Always run
gc.collect()before checkinggc.mem_free(). - Unbounded Data Structures: Look for lists or dictionaries inside your
while True:loop that grow indefinitely without being cleared or written to flash. - String Concatenation in Loops: Using
+=on strings in a loop creates a brand new string object in the VM heap on every iteration, orphaning the old one and causing massive fragmentation.
Error 1: MemoryError: memory allocation failed, allocating X bytes
Exact Error String: MemoryError: memory allocation failed, allocating 1024 bytes
Ranked Causes:
- Native Lists/Dicts: A standard Python list in MicroPython carries significant overhead. Each float appended to a list consumes not just 4 bytes for the float, but additional bytes for the list pointer and object headers.
- Pre-allocating Massive Buffers: Attempting to initialize a
bytearray(150000)all at once when the heap is already partially fragmented. - Importing Heavy Modules: Importing modules like
networkorumqttdynamically inside a function rather than at the top of the script, causing heap spikes during execution.
Error 2: RuntimeError: maximum recursion depth exceeded
Exact Error String: RuntimeError: maximum recursion depth exceeded
Ranked Causes:
- Accidental Recursive Calls: A function calling itself without a proper base-case exit condition.
- C-Stack Overflow: The VM shares the RP2040's C-stack. Deeply nested
try/exceptblocks or massive nested dictionary lookups can blow the C-stack before the Python heap is full.
lists for sensor data. Switch to the array module or pre-allocate a bytearray. A pre-allocated bytearray reserves a single contiguous block of C-level memory, completely bypassing the VM's per-object allocation overhead.
Extending and Simplifying Your VM Pico Build
Once you understand how the VM allocates memory, you can optimize your builds to do more with the Pico's limited SRAM.
How to Simplify: Use Pre-compiled Bytecode (.mpy)
When you upload a standard .py file to the Pico, the VM must parse the text and compile it into bytecode in RAM at runtime. This consumes a significant portion of your 190KB heap. By cross-compiling your scripts into .mpy files on your PC using the mpy-cross tool, the Pico loads the pre-compiled bytecode directly from flash into RAM, bypassing the parsing overhead and saving up to 30% of your VM heap.
How to Extend: Offloading to the Second Core
The RP2040 has two cores. Core 0 typically runs the MicroPython VM. You can extend your build's capabilities by writing a C/C++ PIO (Programmable I/O) state machine to handle high-speed sensor polling. The PIO runs independently of the VM, buffering data in hardware FIFOs and only triggering a VM interrupt when a full batch of data is ready to be processed. This keeps the VM heap quiet and prevents allocation spikes.
For deeper architectural constraints, refer to the official MicroPython Constrained Environments Guide and the Raspberry Pi Pico Python SDK Datasheet.
Frequently Asked Questions
How do I check the available VM heap memory on my Pico?
Import the gc (garbage collector) module. Run gc.collect() to defragment the heap, then call gc.mem_free() to see the exact bytes available, and gc.mem_alloc() to see what is currently in use. Always call gc.collect() first; otherwise, the free memory number is misleading due to fragmentation.
Why does the vm pico garbage collector fail to free memory?
The garbage collector only frees memory that has no active references. If you append data to a global list, or if a background timer interrupt holds a reference to an object, the GC will not touch it. Ensure you are explicitly dereferencing large objects (e.g., my_data = None) when you are done with them.
Can I increase the VM heap size on the RP2040?
No, not beyond the physical limits of the RP2040's 264KB SRAM. The MicroPython firmware reserves a portion for the C-stack and hardware peripherals. You can slightly tweak the heap allocation in a custom C-level firmware build, but you will only gain a few kilobytes at the risk of destabilizing the C-stack. The real solution is optimizing your Python code to use less heap.
What is the difference between the Pico VM and native C execution?
Native C/C++ (via the Pico SDK) compiles directly to ARM machine code and gives you direct, unmanaged access to the full 264KB of SRAM. The MicroPython VM is an interpreter running on top of a C runtime; it manages memory for you but imposes a strict heap ceiling and execution overhead. Use C for microsecond-precision timing and heavy DSP; use the VM for high-level logic, WiFi networking, and rapid prototyping.






