Advanced Examples#
These examples demonstrate more complex patterns: interrupt-driven I/O, finite state machines, power management, and C interop.
Traffic-Light State Machine#
A UK-style traffic light driven by Timer0 overflows. Demonstrates match/case
for FSM dispatch, @property setters as a zero-overhead hardware abstraction,
and compile-time timer constants.
from pymcu.types import uint8, uint16
from pymcu.hal.gpio import Pin
from pymcu.hal.uart import UART
from pymcu.hal.timer import Timer
from pymcu.chips.atmega328p import TIFR0
class State:
RED = 0
RED_YELLOW = 1
GREEN = 2
YELLOW = 3
DUR_RED = 732 # 3 s (244 overflows/s × 3)
DUR_RY = 244 # 1 s
DUR_GREEN = 732 # 3 s
DUR_YELLOW = 244 # 1 s
class TrafficLight:
def __init__(self, red_pin: str, yellow_pin: str, green_pin: str):
self._red = Pin(red_pin, Pin.OUT)
self._yellow = Pin(yellow_pin, Pin.OUT)
self._green = Pin(green_pin, Pin.OUT)
@property
def state(self) -> uint8:
return 0
@state.setter
def state(self, s: uint8):
match s:
case State.RED:
self._red.high(); self._yellow.low(); self._green.low()
case State.RED_YELLOW:
self._red.high(); self._yellow.high(); self._green.low()
case State.GREEN:
self._red.low(); self._yellow.low(); self._green.high()
case _:
self._red.low(); self._yellow.high(); self._green.low()
def main():
light = TrafficLight("PB0", "PB1", "PB2")
uart = UART(9600)
timer = Timer(0, 256)
state: uint8 = State.RED
ticks: uint16 = 0
dur: uint16 = DUR_RED
light.state = State.RED
uart.println("RED")
while True:
if timer.overflow():
TIFR0[0] = 1 # clear TOV0 (write-1-to-clear)
ticks = ticks + 1
if ticks >= dur:
ticks = 0
match state:
case State.RED:
state = State.RED_YELLOW
dur = DUR_RY
light.state = State.RED_YELLOW
uart.println("RED+YEL")
case State.RED_YELLOW:
state = State.GREEN
dur = DUR_GREEN
light.state = State.GREEN
uart.println("GREEN")
case State.GREEN:
state = State.YELLOW
dur = DUR_YELLOW
light.state = State.YELLOW
uart.println("YELLOW")
case _:
state = State.RED
dur = DUR_RED
light.state = State.RED
uart.println("RED")
Timer0 at prescaler 256, 16 MHz: One overflow = 256 × 256 / 16,000,000 = 4.096 ms → 244 overflows ≈ 1 second.
Wiring:
D8 (PB0) ←→ Red LED + 220 Ω to GND
D9 (PB1) ←→ Yellow LED + 220 Ω to GND
D10 (PB2) ←→ Green LED + 220 Ω to GND
Sensor Dashboard#
ADC sampling driven by Timer0 overflows, min/max tracking, exponential moving average, and a button that toggles between verbose and compact output. Demonstrates GPIOR atomic flags for ISR → main-loop signalling.
from pymcu.types import uint8, uint16
from pymcu.chips.atmega328p import GPIOR0, TIFR0
from pymcu.hal.gpio import Pin
from pymcu.hal.uart import UART
from pymcu.hal.timer import Timer
from pymcu.hal.adc import AnalogPin
def timer0_ovf_isr():
GPIOR0[0] = 1 # signal: timer tick ready
def int0_isr():
GPIOR0[1] = 1 # signal: button pressed
def main():
led = Pin("PB5", Pin.OUT)
btn = Pin("PD2", Pin.IN, pull=Pin.PULL_UP)
uart = UART(9600)
adc = AnalogPin("PC0")
timer = Timer(0, 256)
timer.irq(timer0_ovf_isr)
btn.irq(Pin.IRQ_FALLING, int0_isr)
GPIOR0[0] = 0
GPIOR0[1] = 0
uart.println("SENSOR DASHBOARD")
raw: uint8 = 0
avg: uint8 = 0
lo: uint8 = 255
hi: uint8 = 0
verbose: uint8 = 1
tick: uint8 = 0
while True:
if GPIOR0[0] == 1:
GPIOR0[0] = 0
TIFR0[0] = 1 # clear TOV0
tick += 1
if tick == 64: # sample every ~262 ms
tick = 0
raw16: uint16 = adc.read()
raw = raw16 >> 2 # scale 10-bit to 8-bit
if raw < lo: lo = raw
if raw > hi: hi = raw
avg = (avg + raw) >> 1
led.toggle()
if verbose == 1:
uart.write_str("R:")
uart.write_hex(raw)
uart.write_str(" A:")
uart.write_hex(avg)
uart.write_str(" L:")
uart.write_hex(lo)
uart.write_str(" H:")
uart.write_hex(hi)
uart.write('\n')
else:
uart.write_hex(avg)
uart.write('\n')
if GPIOR0[1] == 1:
GPIOR0[1] = 0
if verbose == 1:
verbose = 0
uart.println("MODE:COMPACT")
else:
verbose = 1
uart.println("MODE:VERBOSE")
UART output (verbose mode): R:A3 A:92 L:12 H:FF (raw, avg, min, max as hex).
GPIOR registers (0x1E–0x20, in the I/O space) are single-cycle read/write on AVR and do not need atomic access for single-bit operations — ideal for ISR → main-loop flags without disabling interrupts.
Wiring:
A0 (PC0) ←→ potentiometer or sensor (0–5V analog)
D2 (PD2) ←→ button to GND (internal pull-up active)
D13 (PB5) ←→ built-in LED
Sleep / Wake on Interrupt#
Put the MCU to sleep (IDLE mode) and wake it on a button press (INT0 falling edge). Repeats 5 times then halts.
from pymcu.types import uint8
from pymcu.chips.atmega328p import GPIOR0
from pymcu.hal.uart import UART
from pymcu.hal.gpio import Pin
from pymcu.hal.power import sleep_idle
def int0_isr():
GPIOR0[0] = 1 # set wakeup flag
def main():
uart = UART(9600)
btn = Pin("PD2", Pin.IN, pull=Pin.PULL_UP)
uart.println("SLEEP DEMO")
GPIOR0[0] = 0
btn.irq(Pin.IRQ_FALLING, int0_isr) # configures INT0, enables SEI
count: uint8 = 0
while count < 5:
uart.println("SLEEP")
sleep_idle() # CPU halts here until INT0 fires
if GPIOR0[0] == 1:
GPIOR0[0] = 0
count += 1
uart.println("WAKE")
uart.println("DONE")
while True:
pass
Current consumption in IDLE mode: ~1 mA at 5V / 16 MHz (peripherals still
active). Use sleep_power_down() for deeper sleep (~1 µA), but note that only
asynchronous interrupts (INT0/INT1, TWI address match, watchdog) can wake the MCU
from power-down.
Wiring:
D2 (PD2) ←→ button to GND
C FFI — Calling C Functions from Python#
Declare external C functions with @extern and link them at firmware build time.
Useful for performance-critical routines (CRC, DSP, crypto) written in C.
from pymcu.types import uint8, inline
from pymcu.hal.uart import UART
from pymcu.ffi import extern
# Stub bodies are ignored by the compiler — only the signature matters.
@extern("c_mul8")
def c_mul8(a: uint8, b: uint8) -> uint8:
pass
@extern("c_add_saturate")
def c_add_saturate(a: uint8, b: uint8) -> uint8:
pass
@inline
def print_hex(uart: UART, prefix: uint8, val: uint8):
uart.write(prefix)
uart.write(':')
uart.write_hex(val)
uart.write('\n')
def main():
uart = UART(9600)
print("EXTERN")
m: uint8 = c_mul8(3, 10) # 30 = 0x1E
print_hex(uart, 'M', m)
s: uint8 = c_add_saturate(200, 100) # 255 (saturated) = 0xFF
print_hex(uart, 'S', s)
a: uint8 = c_add_saturate(4, 6) # 10 = 0x0A
print_hex(uart, 'A', a)
print("OK")
while True:
pass
The matching C source (c_src/math_helper.c):
#include <stdint.h>
uint8_t c_mul8(uint8_t a, uint8_t b) {
return a * b;
}
uint8_t c_add_saturate(uint8_t a, uint8_t b) {
uint16_t r = (uint16_t)a + b;
return r > 255 ? 255 : (uint8_t)r;
}
Register c_src/math_helper.c in pyproject.toml:
[tool.pymcu.ffi]
sources = ["c_src/math_helper.c"]
AVR calling convention: arguments are passed right-to-left in register pairs
starting at R25:R24 (arg0), R23:R22 (arg1), R21:R20 (arg2). uint8 values use only
the low register of each pair (R24, R22, R20). Return value is in R24 (R25:R24 for 16-bit).