# A complete reference for developers coming from any language
# Covers core Python + MicroPython hardware API for the Arduino Uno R4
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.
Python 3.4+ syntax · Runs directly on the RA4M1 chip · No OS · 32KB RAM · 256KB Flash
Arduino Lab for MicroPython (official) · Thonny IDE · mpremote CLI · Upload .py files over USB
main.py runs on boot. boot.py runs before it. The REPL lets you type code interactively.
machine — GPIO, ADC, PWM, UART, I2C, SPI. time — sleep, ticks_ms. sys — system info.
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 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.
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.
void setup() { Serial.begin(9600); } void loop() { digitalWrite(13, HIGH); delay(500); }
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)
# ── 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
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.
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).
| Type | Example | MicroPython Notes |
|---|---|---|
| int | x = 42 | Arbitrary precision (limited by RAM on MCU) |
| float | v = 3.14 | 32-bit float on MicroPython (not 64-bit like CPython) |
| bool | flag = True | Subclass of int. True==1, False==0 |
| str | s = "hello" | Immutable Unicode sequence |
| bytes | b = b"\x00\xFF" | Immutable byte buffer — common in hardware I/O |
| bytearray | ba = bytearray(4) | Mutable byte buffer — preferred for MCU comms |
| list | pins = [9, 10, 11] | Mutable ordered collection |
| tuple | pos = (x, y) | Immutable — slightly faster, less RAM |
| dict | cfg = {"baud": 9600} | Hash map — key/value store |
| None | val = None | Python's null. Check with is None |
# ── 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
| Category | Operator | Example | Note |
|---|---|---|---|
| Arithmetic | + - * / // % ** | 10 / 3 → 3.333 | / always returns float. // is floor div. ** is power. |
| Augmented | += -= *= /= //= **= %= | x += 1 | No ++ or -- in Python |
| Comparison | == != < > <= >= | x == 10 | Returns bool |
| Logical | and or not | a and not b | Words, not symbols. Short-circuits. |
| Identity | is is not | x is None | Compares object identity, not value |
| Membership | in not in | 3 in [1,2,3] | Works on lists, strings, dicts, sets |
| Bitwise | & | ^ ~ << >> | reg &= ~(1 << 3) | Same as C++ — essential for hardware |
| Ternary | x if cond else y | v = "on" if flag else "off" | Inline conditional expression |
| Walrus | := | if (n := len(a)) > 10: | Python 3.8+ — assign and test in one |
# ── 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"
# ── 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++)
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.
# ── 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
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
# ══ 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}
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.
# 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']
# ── 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
# ── 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")
| Function / Class | Purpose | Example |
|---|---|---|
| Pin(n, mode) | Digital pin. mode: Pin.IN, Pin.OUT, Pin.OPEN_DRAIN | p = Pin(13, Pin.OUT) |
| pin.on() / off() | Set pin HIGH / LOW | led.on() |
| pin.value() | Read digital pin → 0 or 1 | v = btn.value() |
| pin.value(1) | Write digital pin | led.value(1) |
| Pin(n, Pin.IN, Pin.PULL_UP) | Input with internal pull-up | btn = 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–65535 | raw = a.read_u16() |
| PWM(pin, freq) | PWM output. R4 supports 16-bit duty | p = PWM(Pin(9), freq=1000) |
| pwm.duty_u16(v) | Set PWM duty 0–65535 | p.duty_u16(32767) |
| pwm.freq(n) | Set PWM frequency in Hz | p.freq(440) |
| time.sleep(s) | Block for N seconds (float OK) | time.sleep(0.5) |
| time.sleep_ms(ms) | Block for N milliseconds | time.sleep_ms(200) |
| time.sleep_us(us) | Block for N microseconds | time.sleep_us(50) |
| time.ticks_ms() | Milliseconds since boot | t = time.ticks_ms() |
| time.ticks_diff(a,b) | Correct ms difference (handles overflow) | dt = time.ticks_diff(now, last) |
| machine.freq() | Get/set CPU frequency | machine.freq(48_000_000) |
| machine.reset() | Hard reset the board | machine.reset() |
| Pin.irq(handler, trigger) | Hardware interrupt callback | btn.irq(on_press, Pin.IRQ_FALLING) |
| I2C(id, scl, sda, freq) | I²C bus master | i2c = I2C(0, scl=Pin(22), sda=Pin(21)) |
| UART(id, baud) | Serial UART | u = UART(0, 9600) |
| print() | Outputs to USB serial (Serial Monitor) | print(f"v={voltage:.2f}") |
| input() | Read line from USB serial REPL | cmd = input("> ") |
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').
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).
""" 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}")
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.
Arduino Uno R4 · 3× LEDs + 220Ω resistors (pins 9, 10, 11) · 1× pushbutton (pin 2) · USB cable
Classes · Properties · Dunder methods · Lists · Dicts · Comprehensions · Generators · ISR · f-strings · Exceptions · Modules · Closures · Non-blocking timers
Upload via Arduino Lab for MicroPython → Open Serial Terminal → Type: 0 (off), 1 (blink), 2 (chase), 3 (pulse), ? (help), s (status)
""" ╔══════════════════════════════════════════════════════════╗ ║ 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
| Code Element | Concept | Why It Matters |
|---|---|---|
| LED_PINS = (9, 10, 11) | Tuple constant | Immutable — safer than a list for fixed config data |
| leds = [SmartLED(p) for p in LED_PINS] | List comprehension | Creates all LED objects in one readable line |
| @property / @duty.setter | Property descriptor | Validated attribute — raises ValueError on bad input |
| __str__, __bool__, __repr__ | Dunder methods | print(led) and if led: work naturally |
| make_toggler(10) | Closure | Inner function captures outer variable — elegant callback factory |
| def pulse_wave(...): yield | Generator | Lazy sequence — no list allocated, saves RAM |
| MODE_FNS = {0: update_off, ...} | Dict of functions | Pythonic dispatch table — replaces long if/elif chain |
| btn.irq(handler=on_button_press) | Hardware IRQ | Button response is instant, not polling-based |
| state["btn_event"] = False | Flag pattern | IRQ sets flag; main loop handles it safely |
| time.ticks_diff(now, last) | Non-blocking timer | Loop stays responsive — no blocking sleep |
| for led, duty in zip(leds, duties) | zip() | Iterate two lists in parallel — clean and Pythonic |
| except KeyboardInterrupt | Exception handling | Ctrl+C exits cleanly and turns off LEDs |
| if __name__ == "__main__": | Module guard | Allows file to be imported as a module without running main() |
| gc.collect() | Garbage collection | Manually frees heap RAM — essential on 32KB MCU |
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.
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).
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().
Use the REPL (serial terminal) to test code interactively. print() outputs to the terminal. dir(obj) shows all attributes. help(machine) shows module docs.
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.
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.