Programming IoT Devices

MicroPython Basics

Learn MicroPython — the Python implementation for microcontrollers. Explore the REPL, core modules, file system, memory management, and the key differences from standard Python.

MicroPython Basics

MicroPython is a lean, efficient implementation of Python 3 designed to run on microcontrollers with as little as 256 KB of flash and 16 KB of RAM. It brings Python's readable syntax to ESP32, Raspberry Pi Pico, STM32, and dozens of other boards.


MicroPython vs CPython (Standard Python)

FeatureMicroPythonCPython (Standard Python)
TargetMicrocontrollersDesktop/Server
RAM16 KB – 520 KBGigabytes
Flash256 KB – 4 MBGigabytes (disk)
Startup time< 100 ms30–100 ms
Hardware accessmachine moduleOS-level (GPIO libs)
Standard librarySubset (uos, utime)Full
FloatSingle precision (32-bit)Double precision (64-bit)
f-stringsSupported (v1.19+)Fully supported
asynciouasyncio (subset)Full asyncio

The REPL — Interactive Python Shell

The REPL (Read-Eval-Print Loop) gives you a live Python prompt directly on the device over USB serial.

python
# Connect via Thonny or: screen /dev/ttyUSB0 115200

# Try these in the REPL:
>>> print("Hello from ESP32!")
Hello from ESP32!

>>> import machine
>>> machine.freq()
240000000                # 240 MHz

>>> import gc
>>> gc.mem_free()
208976                   # ~204 KB free RAM

>>> import os
>>> os.uname()
(sysname='esp32', nodename='esp32', release='1.22.0',
 version='v1.22.0 on 2024-01-05', machine='ESP32 module')

>>> 2 ** 32
4294967296               # Python integers, not C int — no overflow!

# List files on the device
>>> import os
>>> os.listdir()
['boot.py', 'main.py', 'config.py']

Core Modules

machine — Hardware Control

python
from machine import Pin, ADC, PWM, I2C, SPI, UART, Timer, WDT, RTC, deepsleep

# GPIO
led = Pin(2, Pin.OUT)
btn = Pin(0, Pin.IN, Pin.PULL_UP)

# ADC
adc = ADC(Pin(34))
adc.atten(ADC.ATTN_11DB)

# PWM
pwm = PWM(Pin(15), freq=50)
pwm.duty(512)  # 50% duty cycle

# Timer (calls a function periodically)
def tick(timer):
    led.toggle()

timer = Timer(0)
timer.init(period=500, mode=Timer.PERIODIC, callback=tick)
# LED now blinks at 1 Hz without blocking main code

# Watchdog Timer (resets the device if not fed regularly)
wdt = WDT(timeout=5000)  # 5 second timeout
while True:
    # Do work...
    wdt.feed()  # Reset watchdog. If this isn't called, device resets.

utime — Time Functions

python
import utime

# Current time (seconds since 2000-01-01 in MicroPython)
print(utime.time())         # e.g. 749289600

# High-resolution timing
start = utime.ticks_ms()    # Milliseconds
# ... do some work ...
elapsed = utime.ticks_diff(utime.ticks_ms(), start)
print(f"Took: {elapsed} ms")

# Microsecond precision
start_us = utime.ticks_us()
utime.sleep_us(100)         # Sleep 100 microseconds
elapsed_us = utime.ticks_diff(utime.ticks_us(), start_us)
print(f"Elapsed: {elapsed_us} µs")

# Sleep functions
utime.sleep(1)              # 1 second
utime.sleep_ms(500)         # 500 milliseconds
utime.sleep_us(1000)        # 1000 microseconds

network — WiFi

python
import network
import utime

def connect_wifi(ssid, password, timeout=15):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)

    if wlan.isconnected():
        print(f"Already connected: {wlan.ifconfig()[0]}")
        return True

    wlan.connect(ssid, password)
    deadline = utime.time() + timeout

    while not wlan.isconnected():
        if utime.time() > deadline:
            print(f"Connection timeout after {timeout}s")
            return False
        print(".", end="")
        utime.sleep(0.5)

    ip, mask, gateway, dns = wlan.ifconfig()
    rssi = wlan.status('rssi')
    print(f"\nIP: {ip}  RSSI: {rssi} dBm")
    return True

connect_wifi("MySSID", "MyPassword")

File System

MicroPython has a built-in file system (FAT or LittleFS) stored in flash.

python
import os

# File system operations
print(os.listdir('/'))       # ['boot.py', 'main.py', 'lib']
print(os.listdir('/lib'))    # ['dht.py', 'mqtt.py']

os.mkdir('/data')            # Create directory
os.rename('/data', '/logs')  # Rename
os.remove('/logs/old.txt')   # Delete file
os.rmdir('/logs')            # Remove directory (must be empty)

# Get filesystem stats (total, free bytes)
stat = os.statvfs('/')
total_bytes = stat[0] * stat[2]
free_bytes  = stat[0] * stat[3]
print(f"Flash: {free_bytes/1024:.1f} KB free of {total_bytes/1024:.1f} KB")

# Reading and writing files
with open('/data/sensor.csv', 'a') as f:
    f.write("2024-01-15,24.5,61.2\n")

with open('/data/sensor.csv', 'r') as f:
    for line in f:
        print(line.strip())

Memory Management

RAM is the most precious resource on a microcontroller.

python
import gc

# Check memory
print(f"Free:  {gc.mem_free():,} bytes")
print(f"Alloc: {gc.mem_alloc():,} bytes")

# Force garbage collection
gc.collect()
print(f"After GC: {gc.mem_free():,} bytes free")

# Memory-efficient patterns
# BAD: builds a list in memory then iterates
data = [i * 2 for i in range(1000)]
for item in data:
    pass

# GOOD: generator — processes one item at a time
def double_gen(n):
    for i in range(n):
        yield i * 2

for item in double_gen(1000):  # Only one value in memory at a time
    pass

# BAD: string concatenation (creates new strings repeatedly)
msg = ""
for i in range(100):
    msg += str(i) + ","

# GOOD: join (builds once)
msg = ",".join(str(i) for i in range(100))

# Check allocation after operations
gc.collect()
print(f"Final free RAM: {gc.mem_free():,} bytes")

boot.py and main.py

MicroPython runs two files on startup in this order:

python
# boot.py — runs first, hardware initialization
# Keep it minimal. Errors here are hard to debug.
import machine
import gc

# Increase heap for complex apps
gc.enable()

# Optionally reduce frequency to save power during init
# machine.freq(80_000_000)  # 80 MHz during boot

print("Boot complete")


# main.py — your application code
# This is where your app logic lives
import dht
from machine import Pin
import utime
from config import WIFI_SSID, WIFI_PASSWORD, MQTT_BROKER

# ... app code ...

def main():
    sensor = dht.DHT22(Pin(4))
    while True:
        sensor.measure()
        print(f"T={sensor.temperature()} H={sensor.humidity()}")
        utime.sleep(5)

if __name__ == '__main__':
    main()

Key MicroPython Differences from CPython

python
# 1. No `time` module — use `utime`
import utime                  # Not: import time
utime.sleep(1)

# 2. Reduced standard library — use 'u' prefixed versions
import ujson                  # Faster, smaller than json
import uos                    # Subset of os
import ure                    # Subset of re (regex)

# 3. No `threading` — use async or hardware timers
import uasyncio

async def blink():
    led = Pin(2, Pin.OUT)
    while True:
        led.toggle()
        await uasyncio.sleep_ms(500)

async def main():
    uasyncio.create_task(blink())
    # ... other tasks ...
    await uasyncio.sleep(60)

uasyncio.run(main())

# 4. Single precision floats (not double)
import math
print(math.pi)       # 3.141593 (not 3.141592653589793)

# 5. Limited f-string support in older versions
# Use format() if targeting older firmware
temp = 24.5
old_way = "Temp: {:.1f} C".format(temp)
new_way = f"Temp: {temp:.1f} C"  # Works in MicroPython 1.19+

# 6. `const()` optimization — avoids allocating an object
from micropython import const
MAX_RETRIES = const(5)   # Compiled to integer constant, saves RAM

Installing Libraries (upip / mip)

python
# MicroPython 1.19+ — use mip (micropython package manager)
# Run in REPL while connected to WiFi:

import mip
mip.install("umqtt.simple")  # MQTT client
mip.install("dht")           # DHT sensor driver
mip.install("bme280")        # BME280 driver

# Manual installation: copy .py files to /lib/ on the device
# via Thonny file manager or ampy tool (pip install adafruit-ampy)

Summary

  • MicroPython is Python 3 running on microcontrollers with kilobytes of RAM
  • The REPL gives you an interactive Python prompt over USB serial for rapid testing
  • The machine module provides hardware control: GPIO, ADC, PWM, I2C, SPI, UART, Timer
  • utime provides timing functions; always use ticks_diff() for elapsed time (handles overflow)
  • boot.py runs first (hardware init), main.py runs your application
  • MicroPython has a smaller standard library, single-precision floats, and no threading (use uasyncio)
  • RAM management matters — use generators, call gc.collect(), and use const() for fixed values

Next, we'll look at Arduino programming for cases where you need C/C++ performance and the Arduino library ecosystem.