MicroPython — Python Programming for Microcontrollers Guide
In this tutorial, you'll learn about MicroPython. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
MicroPython is a lean and efficient implementation of Python 3 designed to run on microcontrollers and constrained systems, bringing Python's readability and rapid development to IoT devices with as little as 256KB of flash and 16KB of RAM.
Why MicroPython Matters
Writing firmware in C/C++ requires managing memory manually, understanding linker scripts, and waiting through compile-flash-test cycles. MicroPython allows you to interact with hardware through a live REPL (Read-Eval-Print Loop) — you type code and see results immediately. Change a pin assignment? Edit the file on the device's filesystem and reset. No cross-compiler, no IDE, no flashing toolchain. This speed of iteration makes MicroPython ideal for prototyping, education, and low-complexity deployments. The Raspberry Pi Pico ships with MicroPython support as a first-class option. ESP32, STM32, nRF52840, and hundreds of other boards run MicroPython. DodaZIP's internal prototyping lab uses MicroPython for quick validation of sensor integrations before committing to C firmware.
Plain-Language Explanation
Imagine writing Arduino code. You write a complete program, compile it, upload it to the board, and wait for it to reboot. If something is wrong, you edit, recompile, and re-upload. This is like writing a letter, mailing it, and waiting for a reply to see if you made a typo.
MicroPython is like having a conversation. You connect to the board over USB, and it presents a Python prompt (>>>). Type import machine and press enter — instant result. Blink a pin: machine.Pin(2, machine.Pin.OUT).value(1) — the LED turns on immediately. When your code works, save it as main.py on the board. On reset, it runs automatically. Writing IoT firmware feels like writing a Python script.
graph TD
subgraph "MicroPython Development Flow"
PC[Computer] -->|USB Serial| REPL[MicroPython REPL
>> prompt]
PC -->|File Upload| FS[Flash Filesystem
main.py, boot.py]
REPL -->|Interactive| Test[Test GPIO / I2C / SPI]
FS -->|Auto-run on boot| App[Application Code]
App -->|WiFi| Net[Network
MQTT / HTTP]
App -->|GPIO| Sensors[Sensors]
App -->|GPIO| Actuators[Actuators]
end
style REPL fill:#27ae60,color:#fff
style FS fill:#3498db,color:#fff
style App fill:#e67e22,color:#fff
Getting Started
Flash MicroPython to the board using esptool (ESP32) or the official UF2 bootloader (Raspberry Pi Pico):
# ESP32: Erase and flash MicroPython
pip install esptool
esptool.py --port /dev/ttyUSB0 erase_flash
esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 esp32-20230426-v1.20.0.bin
# Raspberry Pi Pico: Download UF2 file, copy to Pico when in bootloader mode
Connect via serial terminal:
screen /dev/ttyUSB0 115200
# Or: picocom /dev/ttyUSB0 -b115200
GPIO Control
MicroPython's machine module provides hardware control:
import machine
import time
# Setup LED pin
led = machine.Pin(2, machine.Pin.OUT)
# Blink pattern
while True:
led.value(1)
time.sleep(0.5)
led.value(0)
time.sleep(0.5)
# Read button input
button = machine.Pin(0, machine.Pin.IN, machine.Pin.PULL_UP)
print(f"Button state: {button.value()}")
# PWM (dim LED)
pwm = machine.PWM(machine.Pin(2), freq=1000)
for duty in range(0, 1024, 16):
pwm.duty(duty)
time.sleep(0.05)
Expected REPL output:
Button state: 1
(LED fades smoothly from off to full brightness)
I2C Sensor Reading
Connect a BME280 temperature/humidity/pressure sensor over I2C:
import machine
import time
i2c = machine.I2C(0, scl=machine.Pin(22), sda=machine.Pin(21), freq=400000)
print(f"I2C devices: {i2c.scan()}")
# BME280 typically at address 0x76
BME280_ADDR = 0x76
def read_bme280():
# Read calibration data
calib = i2c.readfrom_mem(BME280_ADDR, 0x88, 26)
dig_T1 = calib[0] | (calib[1] << 8)
# Read temperature
data = i2c.readfrom_mem(BME280_ADDR, 0xFA, 3)
adc_T = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
# Compensate temperature
var1 = ((adc_T >> 3) - (dig_T1 << 1)) * 5
temperature = var1 / 100.0
return temperature
while True:
temp = read_bme280()
print(f"Temperature: {temp:.1f}°C")
time.sleep(5)
Expected output:
I2C devices: [118]
Temperature: 24.3°C
Temperature: 24.5°C
Temperature: 24.4°C
WiFi and MQTT
Connecting the ESP32 to WiFi and publishing over MQTT:
import network
import time
from umqtt.simple import MQTTClient
# WiFi connection
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("YourNetwork", "YourPassword")
while not wlan.isconnected():
time.sleep(0.5)
print("Connecting...")
print(f"Connected: {wlan.ifconfig()}")
# MQTT publish
client = MQTTClient("esp32-sensor", "broker.dodatech.com")
client.connect()
print("MQTT connected")
import machine
adc = machine.ADC(machine.Pin(34))
adc.atten(machine.ADC.ATTN_11DB)
while True:
light_level = adc.read()
client.publish(b"sensors/light", str(light_level).encode())
print(f"Published: {light_level}")
time.sleep(10)
Expected output:
Connecting...
Connected: ('192.168.1.42', '255.255.255.0', '192.168.1.1', '8.8.8.8')
MQTT connected
Published: 2048
Published: 1987
Published: 2102
Filesystem and Boot Scripts
MicroPython mounts an internal flash filesystem. Create main.py to run on boot:
# boot.py — runs first on boot
import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("YourNetwork", "YourPassword")
# main.py — runs after boot.py
import time
import machine
led = machine.Pin(2, machine.Pin.OUT)
while True:
led.value(not led.value())
time.sleep(1)
Use ampy or rshell to upload files:
# Install ampy
pip install adafruit-ampy
# Upload files
ampy --port /dev/ttyUSB0 put boot.py
ampy --port /dev/ttyUSB0 put main.py
# List files on device
ampy --port /dev/ttyUSB0 ls
# Run a script without saving
ampy --port /dev/ttyUSB0 run test.py
Classes and Modules
MicroPython supports Python classes — organize your code into modules:
# sensor.py — saved on the device
import machine
class BME280:
def __init__(self, i2c, addr=0x76):
self.i2c = i2c
self.addr = addr
self._load_calibration()
def _load_calibration(self):
calib = self.i2c.readfrom_mem(self.addr, 0x88, 26)
self.dig_T1 = calib[0] | (calib[1] << 8)
def read_temperature(self) -> float:
data = self.i2c.readfrom_mem(self.addr, 0xFA, 3)
adc = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
var = ((adc >> 3) - (self.dig_T1 << 1)) * 5
return var / 100.0
# Usage in main.py
import machine
from sensor import BME280
i2c = machine.I2C(0, scl=machine.Pin(22), sda=machine.Pin(21))
bme = BME280(i2c)
print(f"Temperature: {bme.read_temperature():.1f}°C")
Common Mistakes
Memory exhaustion: MicroPython has limited RAM. Avoid allocating large lists or strings in loops. Use generators and pre-allocate buffers for network operations.
WiFi reconnection not handled: WiFi disconnections are common. Always check
wlan.isconnected()before publishing and reconnect if needed.Floating-point on non-FP hardware: Some microcontrollers lack hardware FPU. Use integer math and fixed-point scaling where possible.
Infinite loops without yield: A tight loop blocks the scheduler (if using
_thread) and prevents WiFi maintenance. Addtime.sleep_ms(0)to yield control.Not deep sleeping: MicroPython doesn't natively support ESP32 deep sleep in all builds. Verify your firmware build includes deep sleep support before deploying battery devices.
Practice Questions
What is the difference between MicroPython and regular Python? MicroPython is Python 3.4-compatible with limited standard library. It lacks
os,subprocess, andnumpybut includesmachineandnetworkmodules for hardware access.How does the REPL help with debugging? The REPL allows interactive testing of hardware commands without compile-flash cycles. You can probe GPIO pins, read I2C registers, and test logic incrementally.
What is the purpose of boot.py vs main.py?
boot.pyruns first for one-time setup (WiFi, pin config).main.pyruns next as the main application loop. This separation allows fixing a brokenmain.pyfrom the REPL.Why might MicroPython be unsuitable for production IoT? Memory constraints, lack of hard real-time guarantees, and higher power consumption compared to optimized C firmware limit MicroPython to prototyping and moderate-complexity applications.
How do you recover a MicroPython board with a corrupted filesystem? Flash the MicroPython firmware again with esptool. This erases and rewrites the flash including the filesystem, restoring the REPL.
Mini Project
Build a MicroPython web server on ESP32:
import network
import socket
import machine
import time
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("YourNetwork", "YourPassword")
while not wlan.isconnected():
time.sleep(0.5)
adc = machine.ADC(machine.Pin(34))
adc.atten(machine.ADC.ATTN_11DB)
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
server = socket.socket()
server.bind(addr)
server.listen(1)
print(f"Server at http://{wlan.ifconfig()[0]}/")
while True:
conn, _ = server.accept()
request = conn.recv(1024)
light = adc.read()
response = f"""HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n
<!DOCTYPE html>
<html><body>
<h1>ESP32 Light Sensor</h1>
<p>Value: <strong>{light}</strong></p>
<p>Time: {time.localtime()}</p>
</body></html>"""
conn.send(response.encode())
conn.close()
Open a browser to the ESP32's IP address. The page refreshes with each request showing the current light sensor reading.
Cross-References
- ESP32
- FreeRTOS
- MQTT
- Raspberry Pi
- IoT Overview
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro