MicroPython · Arduino Uno R4 Edition

Python forArduino

# A complete reference for developers coming from any language
# Covers core Python + MicroPython hardware API for the Arduino Uno R4

# Table of Contents
01

Python on Arduino — How It Works

The Arduino Uno R4 (Minima and WiFi) runs MicroPython — a lean reimplementation of Python 3 designed for microcontrollers. It is not the same as CPython (the standard desktop Python) but shares ~95% of the syntax. The key difference is the available libraries.

MicroPython Runtime

Python 3.4+ syntax · Runs directly on the RA4M1 chip · No OS · 32KB RAM · 256KB Flash

IDE / Tooling

Arduino Lab for MicroPython (official) · Thonny IDE · mpremote CLI · Upload .py files over USB

Entry Point

main.py runs on boot. boot.py runs before it. The REPL lets you type code interactively.

Key Module

machine — GPIO, ADC, PWM, UART, I2C, SPI. time — sleep, ticks_ms. sys — system info.

⚠ MicroPython ≠ Full Python

These standard library modules are absent or partial on MicroPython: os.path, pathlib, threading, asyncio (use uasyncio), decimal, tkinter. The machine module replaces them all for hardware access. Everything covered in sections 1–11 works in MicroPython.

💡 Python vs C++ on Arduino

Python is slower than C++ but far more readable and faster to develop in. For timing-critical tasks (sub-microsecond precision, hardware ISRs) prefer C++. For logic, communication, and sensor math — Python shines. The Uno R4 at 48 MHz handles MicroPython comfortably for most maker projects.


02

Program Structure

Python uses indentation to define blocks — no curly braces, no semicolons. This is the single biggest adjustment for developers coming from C++, Java, or JavaScript.

C++ (Arduino)
void setup() {
  Serial.begin(9600);
}

void loop() {
  digitalWrite(13, HIGH);
  delay(500);
}
Python (MicroPython)
from machine import Pin
import time

led = Pin(13, Pin.OUT)
while True:
    led.on()
    time.sleep(0.5)
    led.off()
    time.sleep(0.5)
Pythonstructure.py
# ── 1. Imports come first ─────────────────────────────────
from machine import Pin, ADC, PWM   # hardware access
import time                          # sleep, ticks_ms
import sys                           # system info

# ── 2. Constants (UPPER_CASE by convention) ───────────────
LED_PIN  = 13
BAUD     = 9600
INTERVAL = 0.5   # seconds

# ── 3. Global variables ───────────────────────────────────
counter = 0

# ── 4. Hardware objects ───────────────────────────────────
led = Pin(LED_PIN, Pin.OUT)

# ── 5. Functions ──────────────────────────────────────────
def blink(times=1, ms=200):
    """Blink the LED 'times' times with 'ms' delay."""  # docstring
    for _ in range(times):
        led.on()
        time.sleep_ms(ms)
        led.off()
        time.sleep_ms(ms)

# ── 6. Main entry point ───────────────────────────────────
# MicroPython runs main.py top-to-bottom, then enters REPL.
# Wrap your program in a main() function and call it:
def main():
    print("Arduino ready!")
    blink(times=3)

    while True:           # the infinite loop replaces C++ loop()
        global counter
        counter += 1
        print(f"Loop {counter}")
        time.sleep(INTERVAL)

main()   # runs automatically when file loads
📌 No setup() / loop()

MicroPython has no special function names. You write a regular Python script. The while True: loop is the equivalent of C++'s loop(). Everything before it is setup. Indentation (4 spaces) defines all blocks — never mix spaces and tabs.


03

Data Types & Variables

Python is dynamically typed — you never declare a type. A variable is just a name bound to an object. The type is determined at runtime and can change. Python is also strongly typed — it won't silently coerce types ("1" + 1 raises TypeError).

TypeExampleMicroPython Notes
intx = 42Arbitrary precision (limited by RAM on MCU)
floatv = 3.1432-bit float on MicroPython (not 64-bit like CPython)
boolflag = TrueSubclass of int. True==1, False==0
strs = "hello"Immutable Unicode sequence
bytesb = b"\x00\xFF"Immutable byte buffer — common in hardware I/O
bytearrayba = bytearray(4)Mutable byte buffer — preferred for MCU comms
listpins = [9, 10, 11]Mutable ordered collection
tuplepos = (x, y)Immutable — slightly faster, less RAM
dictcfg = {"baud": 9600}Hash map — key/value store
Noneval = NonePython's null. Check with is None
Pythontypes_variables.py
# ── Variable assignment ──────────────────────────────────
name     = "Arduino"
count    = 0
voltage  = 3.3
is_on    = False
nothing  = None

# Multiple assignment
x, y, z = 1, 2, 3     # tuple unpacking
a = b = c = 0          # chain assignment

# ── Type introspection ──────────────────────────────────
print(type(42))            # <class 'int'>
print(isinstance(42, int))  # True  — preferred over type() check

# ── Explicit type conversion ─────────────────────────────
raw   = 512
volts = float(raw) * 3.3 / 65535   # R4 ADC is 14-bit (0-65535)
text  = str(raw)
val   = int("42")

# ── f-strings (Python 3.6+, supported in MicroPython) ──
print(f"Voltage: {volts:.3f} V")    # format spec in f-string
print(f"Count: {count}, Pin: {13}")

# ── Truthiness — falsy values ────────────────────────────
# None, False, 0, 0.0, "", [], {}, () → all falsy
# Everything else → truthy
if count:             # True only if count != 0
    print("has value")
if val is not None:  # explicit None check — preferred style
    print("not null")

# ── bytes / bytearray — essential for hardware ───────────
buf = bytearray(4)     # 4-byte mutable buffer filled with zeros
buf[0] = 0xFF           # set first byte
buf[1] = 0b10110000    # binary literal
print(buf.hex())        # 'ffb00000'

# ── Constants — by convention UPPER_CASE, not enforced ──
MAX_BRIGHTNESS = 255
I2C_ADDR       = 0x3C   # hex literal — common in embedded code

04

Operators

CategoryOperatorExampleNote
Arithmetic+ - * / // % **10 / 3 → 3.333/ always returns float. // is floor div. ** is power.
Augmented+= -= *= /= //= **= %=x += 1No ++ or -- in Python
Comparison== != < > <= >=x == 10Returns bool
Logicaland or nota and not bWords, not symbols. Short-circuits.
Identityis is notx is NoneCompares object identity, not value
Membershipin not in3 in [1,2,3]Works on lists, strings, dicts, sets
Bitwise& | ^ ~ << >>reg &= ~(1 << 3)Same as C++ — essential for hardware
Ternaryx if cond else yv = "on" if flag else "off"Inline conditional expression
Walrus:=if (n := len(a)) > 10:Python 3.8+ — assign and test in one
Pythonoperators.py
# ── No ++ operator — use += 1 ────────────────────────────
i = 0
i += 1      # NOT i++  ← that doesn't exist in Python

# ── Integer vs float division ────────────────────────────
print(10 / 3)    # 3.3333  — always float
print(10 // 3)   # 3       — floor division (like C int/int)
print(10 % 3)    # 1       — modulo
print(2 ** 8)    # 256     — exponentiation

# ── Logical operators are WORDS not symbols ──────────────
x, y = 5, 10
if x > 0 and y < 20:   # not &&
    print("both true")
if x == 0 or y == 0:    # not ||
    print("one is zero")
if not is_on:             # not !
    print("LED is off")

# ── Bitwise (identical to C++) ───────────────────────────
reg = 0b10110000
reg |=  (1 << 2)   # SET bit 2
reg &= ~(1 << 7)   # CLEAR bit 7
reg ^=  (1 << 4)   # TOGGLE bit 4

# ── Chained comparisons (Pythonic!) ─────────────────────
val = 512
if 0 <= val <= 1023:      # more readable than val>=0 and val<=1023
    print("valid ADC reading")

# ── Ternary expression ───────────────────────────────────
status = "ON" if is_on else "OFF"

05

Control Flow

Pythoncontrol_flow.py
# ── IF / ELIF / ELSE ──────────────────────────────────────
temp = 25
if temp < 0:
    print("Freezing")
elif temp < 20:           # elif — NOT else if
    print("Cold")
else:
    print("Warm")

# ── MATCH / CASE (Python 3.10+, MicroPython 1.21+) ───────
mode = 2
match mode:
    case 0: print("OFF")
    case 1: print("BLINK")
    case 2: print("CHASE")
    case _: print("UNKNOWN")  # wildcard default

# ── FOR LOOP — iterates directly over any iterable ────────
pins = [9, 10, 11]
for pin in pins:          # no index needed!
    print(pin)

# Range (like C for-loop)
for i in range(10):        # 0–9
    print(i)
for i in range(2, 10, 2): # 2,4,6,8 (start, stop, step)
    print(i)

# enumerate() — get index AND value (not range(len()))
for i, pin in enumerate(pins):
    print(f"LED{i} → pin {pin}")

# for…else — else runs ONLY if loop completed without break
for n in pins:
    if n == 99:
        break
else:
    print("pin 99 not found")   # prints — loop completed

# ── WHILE LOOP ────────────────────────────────────────────
count = 0
while count < 5:
    count += 1

# ── NON-BLOCKING TIMING with ticks_ms ────────────────────
# MicroPython equivalent of millis() pattern
last_tick = time.ticks_ms()
INTERVAL_MS = 1000

while True:
    now = time.ticks_ms()
    if time.ticks_diff(now, last_tick) >= INTERVAL_MS:
        last_tick = now
        print("tick!")      # periodic work — not blocking
    # other code runs here every loop iteration

# ── BREAK / CONTINUE / PASS ──────────────────────────────
for i in range(10):
    if i == 5:
        break           # exit loop entirely
    if i % 2 == 0:
        continue        # skip even numbers
    print(i)

def not_yet_implemented():
    pass               # pass = no-op placeholder (like {} in C++)

06

Functions

Functions in Python are first-class objects — they can be stored in variables, passed as arguments, and returned from other functions. This unlocks powerful patterns like callbacks and decorators.

Pythonfunctions.py
# ── Basic function ───────────────────────────────────────
def add(a, b):
    return a + b

# ── Default parameters ───────────────────────────────────
def blink(pin, times=1, ms=200):
    for _ in range(times):
        pin.on();  time.sleep_ms(ms)
        pin.off(); time.sleep_ms(ms)

blink(led)              # uses defaults: times=1, ms=200
blink(led, times=3)     # keyword argument — order doesn't matter
blink(led, 5, 100)      # positional arguments

# ── Multiple return values (as tuple) ───────────────────
def read_sensors():
    temp    = 25.4
    humidity = 60.2
    return temp, humidity       # returns a tuple

t, h = read_sensors()           # unpack immediately

# ── *args — variadic positional arguments (tuple) ───────
def total(*nums):
    return sum(nums)

total(1, 2, 3, 4)   # 10

# ── **kwargs — variadic keyword args (dict) ──────────────
def configure(**opts):
    for key, val in opts.items():
        print(f"  {key} = {val}")

configure(baud=9600, debug=True)

# ── Type hints (documentation, not enforced at runtime) ─
def voltage(raw: int, ref: float = 3.3) -> float:
    return raw * ref / 65535.0   # R4 14-bit ADC

# ── Lambda — anonymous single-expression function ────────
scale = lambda x: x * 3.3 / 65535
result = scale(32767)   # 1.65 V

# ── Functions as arguments (callback pattern) ────────────
def apply(value, func):
    return func(value)

apply(512, scale)   # passes function as argument

# ── Closure — inner function captures outer variable ─────
def make_scaler(ref_voltage):
    def scaler(raw):
        return raw * ref_voltage / 65535  # ref_voltage captured
    return scaler

to_5v = make_scaler(5.0)   # closure with 5V reference
to_3v = make_scaler(3.3)   # closure with 3.3V reference

# ── Generator — lazy sequence, saves RAM on MCU ─────────
def pwm_steps(start=0, stop=256, step=16):
    while start < stop:
        yield start        # pauses here, resumes on next()
        start += step

for level in pwm_steps():
    print(level)           # 0, 16, 32, ... 240

07

Classes & Object-Oriented Programming

Pythonclasses.py
from machine import Pin, PWM

# ── Class definition ─────────────────────────────────────
class SmartLED:
    """A PWM-capable LED with convenience methods."""

    # Class variable — shared by ALL instances
    count = 0

    def __init__(self, pin: int, freq: int = 1000):
        """Constructor — called when SmartLED(13) is created."""
        self._pin = Pin(pin, Pin.OUT)         # _prefix = private by convention
        self._pwm = PWM(self._pin, freq=freq)
        self._brightness = 0
        self._is_on = False
        SmartLED.count += 1                 # increment class variable

    # ── Regular methods ──────────────────────────────────
    def on(self, brightness: int = 65535):    # R4 PWM is 16-bit
        self._brightness = max(0, min(65535, brightness))
        self._pwm.duty_u16(self._brightness)
        self._is_on = True

    def off(self):
        self._pwm.duty_u16(0)
        self._is_on = False

    def toggle(self):
        self.off() if self._is_on else self.on()

    def blink(self, times=1, ms=200):
        for _ in range(times):
            self.on(); time.sleep_ms(ms)
            self.off(); time.sleep_ms(ms)

    # ── Properties — computed / validated attributes ──────
    @property
    def brightness(self):
        return self._brightness

    @brightness.setter
    def brightness(self, value):
        if not (0 <= value <= 65535):
            raise ValueError(f"Brightness must be 0-65535, got {value}")
        self.on(value)

    # ── Dunder (magic) methods ───────────────────────────
    def __repr__(self):        # repr(obj) — developer string
        return f"SmartLED(pin={self._pin}, on={self._is_on})"

    def __str__(self):         # str(obj) / print(obj)
        state = "ON" if self._is_on else "OFF"
        return f"LED[{state} @{self._brightness}]"

    def __bool__(self):        # truth value: if led:
        return self._is_on

    def __del__(self):         # called before garbage collection
        self.off()
        SmartLED.count -= 1

    # ── Class and static methods ─────────────────────────
    @classmethod
    def total(cls):
        return cls.count         # cls = the class itself

    @staticmethod
    def percent_to_duty(pct):  # no self or cls needed
        return int(pct / 100 * 65535)


# ── Inheritance ──────────────────────────────────────────
class RGBled(SmartLED):
    """Extends SmartLED with color channel tracking."""

    def __init__(self, r_pin, g_pin, b_pin):
        super().__init__(r_pin)           # call parent __init__
        self._g = SmartLED(g_pin)
        self._b = SmartLED(b_pin)

    def color(self, r, g, b):          # r,g,b as 0-255
        scale = lambda v: v * 257      # 0-255 → 0-65535
        self.on(scale(r))
        self._g.on(scale(g))
        self._b.on(scale(b))


# ── Usage ────────────────────────────────────────────────
led = SmartLED(13)
led.blink(times=3)
led.brightness = 32000    # uses setter
print(led)                  # uses __str__
print(SmartLED.total())     # class method
duty = SmartLED.percent_to_duty(50)  # static method

08

Collections

Pythoncollections.py
# ══ LIST — mutable ordered sequence ═════════════════════
pins = [9, 10, 11]
pins.append(12)          # [9, 10, 11, 12]
pins.insert(0, 8)         # [8, 9, 10, 11, 12]
pins.pop()                # removes & returns 12
pins.remove(8)            # removes first occurrence of 8
pins.sort()               # in-place sort
pins.reverse()            # in-place reverse
len(pins)                  # length
pins[0]                   # first element
pins[-1]                  # last element
pins[1:3]                  # slice [10, 11]
pins[::-1]                 # reversed copy

# ══ TUPLE — immutable ordered sequence ══════════════════
# Use for fixed data: pin configs, coordinates, RGB values
pos = (3, 4)
x, y = pos                # unpack
first, *rest = (1, 2, 3, 4)   # first=1, rest=[2,3,4]
RGB_RED   = (255, 0, 0)  # constant config — tuple is ideal

# ══ DICT — key/value map ════════════════════════════════
config = {
    "baud":   9600,
    "pins":   [9, 10, 11],
    "debug":  True
}
config["baud"]           # 9600 — KeyError if missing
config.get("baud")       # 9600 — None if missing (safe)
config.get("x", 0)       # 0    — default if missing
config["mode"] = "blink" # add / update key
del config["debug"]      # delete key
"baud" in config          # True — key membership

for key, val in config.items():   # iterate pairs
    print(f"  {key}: {val}")

# ══ SET — unordered, unique elements ════════════════════
active = {9, 10}
active.add(11)            # {9, 10, 11}
active.discard(99)        # no error if missing
9 in active                # True — fast membership test
all_pins  = {9, 10, 11, 12}
used = active & all_pins   # intersection: {9, 10, 11}
free = all_pins - active   # difference:   {12}

09

Comprehensions

Comprehensions are a concise, Pythonic way to build collections. They replace verbose for loops and are generally faster. Use them freely — they read like English once you know the syntax.

Pythoncomprehensions.py
# Syntax: [expression  for item in iterable  if condition]

# ── LIST COMPREHENSION ───────────────────────────────────
squares  = [x**2 for x in range(10)]
evens    = [x for x in range(20) if x % 2 == 0]
voltages = [raw * 3.3 / 65535 for raw in adc_readings]

# Flatten a 2D list (nested comprehension)
matrix = [[1,2],[3,4],[5,6]]
flat   = [x for row in matrix for x in row]  # [1,2,3,4,5,6]

# ── DICT COMPREHENSION ───────────────────────────────────
pins       = [9, 10, 11]
pin_states = {p: False for p in pins}   # {9: False, 10: False, 11: False}
inverted   = {v: k for k, v in config.items()}  # flip keys/values

# ── SET COMPREHENSION ────────────────────────────────────
unique_lengths = {len(w) for w in ["hi", "hey", "hello", "hi"]}  # {2, 3, 5}

# ── GENERATOR EXPRESSION — lazy, no RAM allocation ──────
# Use () instead of []. Ideal when you only iterate once.
total = sum(x**2 for x in range(1000))   # ← no list built in RAM!

# ── Conditional expression (ternary) inside comprehension
labels = ["even" if x % 2 == 0 else "odd" for x in range(6)]
# ['even','odd','even','odd','even','odd']

10

Exception Handling

Pythonexceptions.py
# ── try / except / else / finally ───────────────────────
try:
    val = int("not a number")     # raises ValueError
except ValueError as e:
    print(f"Bad value: {e}")
except (TypeError, OSError) as e:  # catch multiple types
    print(f"Error: {e}")
except Exception as e:            # catch-all (use sparingly)
    print(f"Unexpected: {e}")
    raise                          # re-raise the original exception
else:
    print("No exception occurred")  # runs ONLY on success
finally:
    print("Always runs — cleanup")  # runs no matter what

# ── Custom exception class ───────────────────────────────
class SensorError(Exception):
    def __init__(self, msg, code=None):
        super().__init__(msg)
        self.code = code

raise SensorError("Temp sensor offline", code=503)

# ── Common exceptions in embedded/MicroPython ────────────
# ValueError   — wrong value for right type: int("abc")
# TypeError    — wrong type: "1" + 1
# OSError      — hardware I/O failure (I2C, UART, file)
# IndexError   — list index out of range
# KeyError     — dict key not found
# AttributeError — object has no such attribute
# MemoryError  — ran out of RAM (critical on MCU!)

# ── Safe sensor reading pattern ──────────────────────────
def safe_read(sensor, retries=3):
    for attempt in range(retries):
        try:
            return sensor.read()
        except OSError:
            time.sleep_ms(100)
    return None   # all retries failed

11

Modules & Imports

Pythonimports.py
# ── Import styles ────────────────────────────────────────
import time                        # use as time.sleep()
import time as t                  # use as t.sleep()
from machine import Pin, ADC, PWM # import specific names
from machine import *             # import all — avoid (pollutes namespace)

# ── MicroPython built-in modules ─────────────────────────
import machine    # Pin, ADC, PWM, UART, I2C, SPI, Timer
import time       # sleep, sleep_ms, sleep_us, ticks_ms, ticks_diff
import sys        # sys.exit, sys.platform, sys.version
import gc         # garbage collector — gc.collect(), gc.mem_free()
import math       # sin, cos, sqrt, pi — hardware float
import struct     # pack/unpack binary data — common in sensor protocols
import ujson      # JSON encode/decode (micro version)
import uasyncio   # async/await coroutines (micro asyncio)

# ── Writing your own module ──────────────────────────────
# Save as 'sensors.py' on the device (via Arduino Lab IDE)
# sensors.py:
#   def read_temp(adc_pin): ...
#   def read_light(adc_pin): ...

from sensors import read_temp     # import from your module

# ── __name__ guard (good practice) ───────────────────────
# Code inside this block only runs when the file is run directly,
# not when it's imported as a module.
if __name__ == "__main__":
    main()

# ── Check available RAM ───────────────────────────────────
import gc
gc.collect()
print(gc.mem_free(), "bytes free")

12

MicroPython I/O API Quick Reference

Function / ClassPurposeExample
Pin(n, mode)Digital pin. mode: Pin.IN, Pin.OUT, Pin.OPEN_DRAINp = Pin(13, Pin.OUT)
pin.on() / off()Set pin HIGH / LOWled.on()
pin.value()Read digital pin → 0 or 1v = btn.value()
pin.value(1)Write digital pinled.value(1)
Pin(n, Pin.IN, Pin.PULL_UP)Input with internal pull-upbtn = Pin(2, Pin.IN, Pin.PULL_UP)
ADC(pin)Analog-to-digital converter. R4 = 14-bit (0-65535)a = ADC(Pin('A0'))
adc.read_u16()Read ADC → 0–65535raw = a.read_u16()
PWM(pin, freq)PWM output. R4 supports 16-bit dutyp = PWM(Pin(9), freq=1000)
pwm.duty_u16(v)Set PWM duty 0–65535p.duty_u16(32767)
pwm.freq(n)Set PWM frequency in Hzp.freq(440)
time.sleep(s)Block for N seconds (float OK)time.sleep(0.5)
time.sleep_ms(ms)Block for N millisecondstime.sleep_ms(200)
time.sleep_us(us)Block for N microsecondstime.sleep_us(50)
time.ticks_ms()Milliseconds since boott = time.ticks_ms()
time.ticks_diff(a,b)Correct ms difference (handles overflow)dt = time.ticks_diff(now, last)
machine.freq()Get/set CPU frequencymachine.freq(48_000_000)
machine.reset()Hard reset the boardmachine.reset()
Pin.irq(handler, trigger)Hardware interrupt callbackbtn.irq(on_press, Pin.IRQ_FALLING)
I2C(id, scl, sda, freq)I²C bus masteri2c = I2C(0, scl=Pin(22), sda=Pin(21))
UART(id, baud)Serial UARTu = UART(0, 9600)
print()Outputs to USB serial (Serial Monitor)print(f"v={voltage:.2f}")
input()Read line from USB serial REPLcmd = input("> ")
📌 Arduino R4 ADC Note

The Uno R4's RA4M1 chip has a 14-bit ADC (0–65535), not 10-bit like the older R3. In MicroPython, adc.read_u16() always returns a 16-bit value (0–65535) regardless of hardware precision. To get voltage: volts = raw * 3.3 / 65535. Analog pins are addressed as Pin('A0') through Pin('A5').


13

Hello World

The Python "Hello World" on a microcontroller means blinking the built-in LED and printing to the serial terminal (accessible via the MicroPython REPL in Arduino Lab IDE).

Pythonhelloworld.py
"""
Hello World — Arduino Uno R4 MicroPython
Blinks the built-in LED on pin 13 and prints to serial.

Hardware: No extra components.
IDE: Open Serial Terminal in Arduino Lab for MicroPython.
"""

from machine import Pin
import time

LED_PIN = 13              # built-in LED on Uno R4
led     = Pin(LED_PIN, Pin.OUT)
count   = 0

print("Hello, Arduino World!")

while True:
    led.on()
    time.sleep(0.5)
    led.off()
    time.sleep(0.5)

    count += 1
    print(f"Blink #{count}")

14

Comprehensive Beginner Project: Smart LED Console

A single MicroPython script that demonstrates virtually every core Python concept in one working program. It creates a serial-command-driven LED control station with multiple modes, a class, timers, interrupt handling, collections, comprehensions, generators, and exception handling.

Hardware Needed

Arduino Uno R4 · 3× LEDs + 220Ω resistors (pins 9, 10, 11) · 1× pushbutton (pin 2) · USB cable

Concepts Covered

Classes · Properties · Dunder methods · Lists · Dicts · Comprehensions · Generators · ISR · f-strings · Exceptions · Modules · Closures · Non-blocking timers

How to Run

Upload via Arduino Lab for MicroPython → Open Serial Terminal → Type: 0 (off), 1 (blink), 2 (chase), 3 (pulse), ? (help), s (status)

Python — Full ProjectSmartLEDConsole.py
"""
╔══════════════════════════════════════════════════════════╗
║       SMART LED CONSOLE — MicroPython Arduino Project     ║
║  Demonstrates core Python concepts in one cohesive sketch ║
╚══════════════════════════════════════════════════════════╝

CIRCUIT:
  Pins 9, 10, 11 → LED → 220Ω → GND
  Pin 2          → pushbutton → GND  (uses PULL_UP)

CONCEPTS DEMONSTRATED:
  Classes & inheritance       Properties & dunder methods
  Lists, dicts, tuples        Comprehensions & generators
  *args / **kwargs            Closures & lambdas
  Exception handling          Hardware interrupts (IRQ)
  Non-blocking timing         f-strings & type hints
  Modules & __name__ guard    gc / memory management
"""

# ══ IMPORTS ══════════════════════════════════════════════
from machine import Pin, PWM
import time
import sys
import gc
import math

# ══ CONSTANTS ════════════════════════════════════════════
LED_PINS   = (9, 10, 11)    # tuple — immutable config
BTN_PIN    = 2
TICK_MS    = 50              # main timer resolution
MAX_DUTY   = 65535          # R4 PWM is 16-bit
MODES      = {0: "OFF", 1: "BLINK", 2: "CHASE", 3: "PULSE"}

HELP_TEXT  = """
=== Smart LED Console ===
  0  →  All LEDs off
  1  →  Blink mode
  2  →  Chase / Knight Rider
  3  →  Pulse / breathing
  ?  →  Show this help
  s  →  Print status
  m  →  Show free memory
========================="""

# ══ CLASS — SmartLED ═════════════════════════════════════
class SmartLED:
    """Single PWM LED with state tracking and convenience API."""

    # Class variable — shared by all instances
    _total = 0

    def __init__(self, pin: int):
        """Create LED on given pin. Sets up PWM at 1kHz."""
        self._hw_pin    = Pin(pin, Pin.OUT)
        self._pwm       = PWM(self._hw_pin, freq=1000)
        self._duty      = 0
        self._is_on     = False
        self._pin_num   = pin
        SmartLED._total += 1
        self.off()

    # ── Core methods ─────────────────────────────────────
    def on(self, duty: int = MAX_DUTY):
        self._duty  = max(0, min(MAX_DUTY, duty))
        self._pwm.duty_u16(self._duty)
        self._is_on = self._duty > 0

    def off(self):
        self._pwm.duty_u16(0)
        self._duty  = 0
        self._is_on = False

    def toggle(self):
        self.off() if self._is_on else self.on()

    def blink(self, times: int = 1, ms: int = 200):
        for _ in range(times):
            self.on(); time.sleep_ms(ms)
            self.off(); time.sleep_ms(ms)

    # ── Property — validated attribute ───────────────────
    @property
    def duty(self) -> int:
        return self._duty

    @duty.setter
    def duty(self, value: int):
        if not (0 <= value <= MAX_DUTY):
            raise ValueError(f"duty must be 0-{MAX_DUTY}")
        self.on(value)

    # ── Dunder (magic) methods ────────────────────────────
    def __repr__(self) -> str:
        return f"SmartLED(pin={self._pin_num})"

    def __str__(self) -> str:
        pct = int(self._duty / MAX_DUTY * 100)
        return f"[p{self._pin_num} {'ON' if self._is_on else 'off'} {pct}%]"

    def __bool__(self) -> bool:
        return self._is_on     # if led: → True when on

    # ── Class / static methods ────────────────────────────
    @classmethod
    def count(cls) -> int:
        return cls._total

    @staticmethod
    def pct_to_duty(pct: float) -> int:
        return int(max(0, min(100, pct)) / 100 * MAX_DUTY)


# ══ GLOBAL STATE ═════════════════════════════════════════
# List comprehension creates SmartLED objects for all pins
leds = [SmartLED(p) for p in LED_PINS]

# Button with internal pull-up resistor
btn = Pin(BTN_PIN, Pin.IN, Pin.PULL_UP)

# Mutable state dict — Python equivalent of global vars
state = {
    "mode":       0,
    "tick":       0,
    "last_tick":  time.ticks_ms(),
    "btn_event":  False,
    "last_btn":   0,
}


# ══ HARDWARE INTERRUPT ═══════════════════════════════════
# This callback is triggered by hardware immediately when
# the button is pressed (Pin goes LOW = FALLING edge).
# Keep it SHORT — no print(), no sleep() inside an IRQ!
def on_button_press(pin):
    state["btn_event"] = True   # set flag; handle in main loop

btn.irq(trigger=Pin.IRQ_FALLING, handler=on_button_press)


# ══ HELPER FUNCTIONS ═════════════════════════════════════

def all_off():
    """Turn off every LED — demonstrates for loop on list."""
    for led in leds:
        led.off()

def print_status(label: str = ""):
    """Print current mode and LED states using str(led) dunder."""
    mode_name = MODES.get(state["mode"], "?")
    led_str   = " ".join(str(l) for l in leds)  # generator expr
    tag       = f"[{label}] " if label else ""
    print(f"{tag}Mode={mode_name} Tick={state['tick']} {led_str}")

def cycle_mode():
    """Advance to next mode, wrapping at max — demos modulo."""
    all_off()
    state["tick"] = 0
    state["mode"] = (state["mode"] + 1) % len(MODES)
    print(f"Button → Mode: {MODES[state['mode']]}")

# ── Closure — factory that bakes in the tick reference ──
def make_toggler(period_ticks: int):
    """Return a function that toggles every N ticks."""
    def toggler() -> bool:
        return (state["tick"] // period_ticks) % 2 == 0
    return toggler

blink_toggler = make_toggler(10)   # blinks every 10 ticks = 500ms

# ── Generator — yields PWM brightness in a triangle wave ─
def pulse_wave(phase: int, count: int, offset: int = 0):
    """
    Yield brightness values for each LED based on current phase.
    Uses a triangle wave for smooth breathing effect.
    Each LED is offset by 120° (360/3 LEDs).
    """
    for i in range(count):
        shifted = (phase + i * 85) % 256   # 85 ≈ 256/3 for equal spacing
        # Triangle wave: ramp up 0→MAX then down MAX→0
        bright = shifted * 2 if shifted < 128 else (255 - shifted) * 2
        yield int(bright / 255 * MAX_DUTY)  # scale to 16-bit duty


# ══ MODE UPDATE FUNCTIONS ════════════════════════════════

def update_off():
    all_off()

def update_blink():
    """Toggle all LEDs together using closure toggler."""
    on = blink_toggler()
    for led in leds:
        led.on() if on else led.off()

def update_chase():
    """Light one LED at a time in sequence."""
    active = (state["tick"] // 8) % len(leds)   # cycles 0→1→2→0
    for i, led in enumerate(leds):
        led.on() if i == active else led.off()

def update_pulse():
    """
    Breathing / fading effect using generator.
    Demonstrates: generators, enumerate, comprehensions.
    """
    phase = (state["tick"] * 4) % 256   # phase advances each tick

    # Collect duties from generator, set each LED
    duties = list(pulse_wave(phase, len(leds)))  # list from generator
    for led, duty in zip(leds, duties):
        led.duty = duty    # uses property setter (validates range)

# Dispatch table — dict of functions (Pythonic switch pattern)
# Maps mode int → update function
MODE_FNS = {
    0: update_off,
    1: update_blink,
    2: update_chase,
    3: update_pulse,
}

# Comprehension to verify all modes have handlers
assert all(m in MODE_FNS for m in MODES), "Missing mode handler!"


# ══ SERIAL COMMAND HANDLER ════════════════════════════════
# sys.stdin.read(1) reads one char from the serial terminal
def handle_serial():
    """Read and dispatch serial commands. Demonstrates exception handling."""
    try:
        import select
        ready, _, _ = select.select([sys.stdin], [], [], 0)
        if not ready:
            return
        cmd = sys.stdin.read(1).strip()
    except Exception:
        return

    all_off()
    state["tick"] = 0

    # Dict of commands → lambda actions (Pythonic command dispatch)
    commands = {
        '0': lambda: _set_mode(0),
        '1': lambda: _set_mode(1),
        '2': lambda: _set_mode(2),
        '3': lambda: _set_mode(3),
        '?': lambda: print(HELP_TEXT),
        's': lambda: print_status("Manual"),
        'm': lambda: print(f"Free RAM: {gc.mem_free()} bytes"),
    }

    action = commands.get(cmd)
    if action:
        action()        # call the lambda stored in the dict
    elif cmd:
        print(f"Unknown: '{cmd}' — type ? for help")

def _set_mode(n: int):
    state["mode"] = n
    print(f"Mode: {MODES[n]}")


# ══ STARTUP SEQUENCE ═════════════════════════════════════
def startup():
    """
    Run once at boot. Demonstrates:
      - list comprehension
      - f-strings
      - try/except
      - class methods
    """
    gc.collect()   # free memory before starting

    print(HELP_TEXT)
    print(f"MicroPython {sys.version}")
    print(f"LEDs: {SmartLED.count()} created on pins {list(LED_PINS)}")
    print(f"Free RAM: {gc.mem_free()} bytes")

    # Startup sweep — list comprehension + enumerate
    for i, led in enumerate(leds):
        led.on()
        time.sleep_ms(150)
        led.off()

    # Print init duty values as comprehension
    duties = [led.duty for led in leds]
    print(f"Initial duties: {duties}")


# ══ MAIN LOOP ════════════════════════════════════════════
def main():
    """
    Main event loop. Non-blocking timing via ticks_ms.
    Handles: hardware IRQ flag, periodic tick, serial input.
    """
    startup()

    while True:

        # ── Handle button IRQ flag (debounce) ──────────────
        if state["btn_event"]:
            state["btn_event"] = False     # clear flag FIRST
            now = time.ticks_ms()
            # Debounce: ignore if pressed again within 200ms
            if time.ticks_diff(now, state["last_btn"]) > 200:
                state["last_btn"] = now
                cycle_mode()

        # ── Non-blocking periodic tick (every TICK_MS) ─────
        now = time.ticks_ms()
        if time.ticks_diff(now, state["last_tick"]) >= TICK_MS:
            state["last_tick"] = now
            state["tick"]     += 1

            # Call mode update function via dispatch dict
            MODE_FNS[state["mode"]]()

            # Print status every 100 ticks (5 seconds)
            if state["tick"] % 100 == 0:
                print_status()
                gc.collect()   # periodic garbage collection

        # ── Check for serial command (non-blocking) ─────────
        handle_serial()


# ══ ENTRY POINT ══════════════════════════════════════════
# __name__ == "__main__" when file runs directly (not imported)
if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nStopped by user (Ctrl+C)")
        all_off()           # clean up hardware on exit
    except Exception as e:
        print(f"Fatal error: {e}")
        all_off()
        raise

Concept Map

Code ElementConceptWhy It Matters
LED_PINS = (9, 10, 11)Tuple constantImmutable — safer than a list for fixed config data
leds = [SmartLED(p) for p in LED_PINS]List comprehensionCreates all LED objects in one readable line
@property / @duty.setterProperty descriptorValidated attribute — raises ValueError on bad input
__str__, __bool__, __repr__Dunder methodsprint(led) and if led: work naturally
make_toggler(10)ClosureInner function captures outer variable — elegant callback factory
def pulse_wave(...): yieldGeneratorLazy sequence — no list allocated, saves RAM
MODE_FNS = {0: update_off, ...}Dict of functionsPythonic dispatch table — replaces long if/elif chain
btn.irq(handler=on_button_press)Hardware IRQButton response is instant, not polling-based
state["btn_event"] = FalseFlag patternIRQ sets flag; main loop handles it safely
time.ticks_diff(now, last)Non-blocking timerLoop stays responsive — no blocking sleep
for led, duty in zip(leds, duties)zip()Iterate two lists in parallel — clean and Pythonic
except KeyboardInterruptException handlingCtrl+C exits cleanly and turns off LEDs
if __name__ == "__main__":Module guardAllows file to be imported as a module without running main()
gc.collect()Garbage collectionManually frees heap RAM — essential on 32KB MCU

15

Best Practices for MicroPython on Arduino

✅ Do

Use time.ticks_ms() + ticks_diff() for timing. Call gc.collect() periodically. Use tuples for constant data. Use generators for sequences. Keep IRQ handlers tiny.

❌ Don't

Avoid time.sleep() in the main loop (blocks everything). Avoid large string concatenation in loops. Don't print inside IRQ handlers. Avoid deep recursion (limited stack).

💾 Save RAM

Use const() from micropython module for integer constants. Use bytearray instead of lists for byte data. Pre-allocate buffers outside loops. Check gc.mem_free().

🔧 Debug

Use the REPL (serial terminal) to test code interactively. print() outputs to the terminal. dir(obj) shows all attributes. help(machine) shows module docs.

Pythonbest_practices.py
from micropython import const
import gc

# ✅ const() — stored as C integer, not Python object → saves RAM
MAX_BRIGHTNESS = const(65535)
LED_PIN        = const(13)

# ✅ Pre-allocate buffers OUTSIDE loops
buf = bytearray(32)   # allocate once
while True:
    read_into(buf)    # reuse the same buffer — no new allocation

# ✅ Use ticks_diff for non-blocking timing
last = time.ticks_ms()
while True:
    now = time.ticks_ms()
    if time.ticks_diff(now, last) >= 1000:
        last = now
        do_periodic_work()    # never blocked by sleep

# ✅ Tuple for config data (no mutation = safer + less RAM)
VALID_PINS = (9, 10, 11)     # NOT a list

# ✅ Use f-strings (not % or .format — they create more objects)
print(f"v={voltage:.3f}V raw={raw}")

# ✅ Periodic GC — prevents heap fragmentation
if state["tick"] % 200 == 0:
    gc.collect()
    print(f"RAM free: {gc.mem_free()}")

# ✅ Graceful Ctrl+C exit — always wrap main loop
try:
    main()
except KeyboardInterrupt:
    cleanup()   # turn off pins, save state, etc.
💡 Next Steps

After mastering this guide: explore uasyncio for cooperative multitasking, the machine.I2C and machine.SPI APIs for sensor communication, ujson for config files stored on the device filesystem, and the urequests library if you're using the Uno R4 WiFi board for HTTP calls.