CircuitPython Compatibility Examples#

These examples use the pymcu-circuitpython compatibility layer. The same source files run unmodified on any real CircuitPython board — PyMCU compiles them to bare-metal AVR firmware with zero runtime overhead.

Install the compat layer with:

pip install pymcu-circuitpython

UART Echo#

Echo every received byte over busio.UART. Adapted from the Adafruit CircuitPython Essentials UART example.

import board
import busio
from digitalio import DigitalInOut, Direction
from pymcu.types import uint8

def main():
    led = DigitalInOut(board.LED)
    led.direction = Direction.OUTPUT

    uart = busio.UART(board.TX, board.RX, baudrate=9600)

    buf: bytearray = bytearray(1)

    while True:
        uart.readinto(buf)        # blocks until buf is full
        led.value = True
        uart.write(buf)
        led.value = False

Differences from CircuitPython:

  • uart.read(n) and uart.readline() would have to return a fresh bytes object, which needs a heap. They compile to a no-op and warn; use readinto(buf) with a bytearray you own. write(buf) takes the same buffer back.

  • direction, value and pull are properties, exactly as in CircuitPython.


Button-controlled LED#

Hold a button to turn on the LED. Uses digitalio.Pull.UP for the internal pull-up resistor.

import board
import time
from digitalio import DigitalInOut, Direction, Pull
from pymcu.types import uint8

def main():
    led = DigitalInOut(board.LED)
    led.direction = Direction.OUTPUT

    btn = DigitalInOut(board.D2)
    btn.direction = Direction.INPUT
    btn.pull = Pull.UP

    while True:
        state: uint8 = btn.value
        # Button is active-low (pressed = 0)
        if state == 0:
            led.value = True
        else:
            led.value = False
        time.sleep(0.01)

Wiring:

Arduino Uno     Button
-----------     ------
D2 (PD2)   ←→  one leg
GND        ←→  other leg   (internal pull-up active — no external resistor needed)
D13        ←→  built-in LED

DHT11 Sensor#

Read temperature and humidity and print them to the serial monitor.

import board
import time
from digitalio import DigitalInOut, Direction
from adafruit_dht import DHT11

def main():
    led    = DigitalInOut(board.LED)
    sensor = DHT11(board.D2)

    led.direction = Direction.OUTPUT

    print("DHT11 ready")

    while True:
        try:
            print(f"H: {sensor.humidity}  T: {sensor.temperature}")
            led.value = True
            time.sleep(0.1)
            led.value = False
        except ValueError:
            print("read error")
            led.value = False

        time.sleep(2.0)

The driver ships in the pymcu-lib-dht library rather than the compat layer, so import adafruit_dht is the same line a real CircuitPython board would run. A failed read raises ValueError, exactly as adafruit_dht does.

Wiring:

Arduino Uno    DHT11
-----------    -----
D2  (PD2)  ←→ DATA  (4.7 kΩ pull-up to 5V recommended)
5V         ←→ VCC
GND        ←→ GND

Morse Blinker#

Blinks SOS in Morse code. Demonstrates @inline for timing-critical helpers — DigitalInOut is a zero-cost abstraction and cannot be passed to non-inlined functions.

import board
import time
from digitalio import DigitalInOut, Direction
from pymcu.types import inline

@inline
def dot(led):
    led.value = True
    time.sleep(0.2)
    led.value = False
    time.sleep(0.2)

@inline
def dash(led):
    led.value = True
    time.sleep(0.6)
    led.value = False
    time.sleep(0.2)

def main():
    led = DigitalInOut(board.LED)
    led.direction = Direction.OUTPUT

    while True:
        # S: ...
        dot(led)
        dot(led)
        dot(led)
        time.sleep(0.4)    # letter gap

        # O: ---
        dash(led)
        dash(led)
        dash(led)
        time.sleep(0.4)    # letter gap

        # S: ...
        dot(led)
        dot(led)
        dot(led)
        time.sleep(1.2)   # word gap

Why @inline? DigitalInOut is a zero-cost abstraction — it holds no SRAM state. Passing it to a regular function would require materializing it on the stack. @inline expands the function body at every call site instead, preserving the zero-cost property.


Traffic Light#

A three-LED traffic light state machine using only digitalio and time. UK-style sequence: Red → Red+Yellow → Green → Yellow → repeat.

import board
import time
from digitalio import DigitalInOut, Direction

def main():
    red    = DigitalInOut(board.D11)
    yellow = DigitalInOut(board.D12)
    green  = DigitalInOut(board.D13)

    red.direction = Direction.OUTPUT
    yellow.direction = Direction.OUTPUT
    green.direction = Direction.OUTPUT

    while True:
        # Red — stop (3 s)
        red.value = True; yellow.value = False; green.value = False
        time.sleep(3.0)

        # Red + Yellow — prepare to go (500 ms)
        red.value = True; yellow.value = True; green.value = False
        time.sleep(0.5)

        # Green — go (3 s)
        red.value = False; yellow.value = False; green.value = True
        time.sleep(3.0)

        # Yellow — slow down (1 s)
        red.value = False; yellow.value = True; green.value = False
        time.sleep(1.0)

Wiring:

D11 (PB3)  ←→  Red    LED + 220 Ω to GND
D12 (PB4)  ←→  Yellow LED + 220 Ω to GND
D13 (PB5)  ←→  Green  LED (built-in, or external)

ADC-controlled PWM Dimmer#

Read a potentiometer on A0 and use the value to control LED brightness via PWM on D6. The duty cycle tracks the ADC value in real time.

import board
import time
from analogio import AnalogIn
from pwmio import PWMOut

def main():
    pot = AnalogIn(board.A0)
    led = PWMOut(board.D6, duty_cycle=0)

    while True:
        adc_value = pot.value          # 0–65535 (16-bit scaled from 10-bit ADC)
        led.duty_cycle = adc_value     # directly drives OCR0A
        time.sleep(0.01)

Wiring:

A0 (PC0)   ←→  potentiometer wiper
D6 (PD6)   ←→  LED + 220 Ω to GND   (OC0A — hardware PWM pin)
5V         ←→  potentiometer end
GND        ←→  potentiometer end

Note: pot.value returns a 16-bit scaled value. PWMOut maps this directly to the 8-bit hardware compare register — no floating-point math is generated.


SSD1306 OLED (Adafruit driver)#

The unmodified Adafruit CircuitPython simpletest for a 128×64 I2C module. Same wiring as the native SSD1306 OLED Display driver; this path uses adafruit_ssd1306 + adafruit_framebuf so the firmware you flash is the library you would run on a CircuitPython board.

import board
import adafruit_ssd1306

i2c = board.I2C()
# 128x64 is the common 0.96" module. Change height to 32 for the
# Adafruit FeatherWing / PiOLED. Some boards sit at 0x3D:
#   display = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3D)
display = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c)
display.fill(0)
display.pixel(0, 0, 1)
display.pixel(127, 63, 1)

def main():
    display.show()

Wiring (Arduino Uno):

SDA  ←→  A4 (PC4)   — 4.7 kΩ pull-up to 3.3V
SCL  ←→  A5 (PC5)   — 4.7 kΩ pull-up to 3.3V
VCC  ←→  3.3V        (most SSD1306 modules are 3.3V)
GND  ←→  GND

Build with stdlib = ["circuitpython"] and the Adafruit library files on sys.path the same way the other CircuitPython examples do. A 128x32 panel is the same class: pass 32 as the height.