Python CircuitPython for Hardware — Deep Dive
CircuitPython’s USB Architecture
CircuitPython’s USB drive workflow is not a convenience hack — it is a fundamental architectural decision. The firmware implements a composite USB device that simultaneously exposes:
- Mass Storage (MSC) — The CIRCUITPY drive for code and libraries
- CDC Serial — A serial console for REPL access and print output
- HID — Optional Human Interface Device mode for keyboard/mouse emulation
- MIDI — Optional MIDI device mode for music projects
This composite USB stack means a single CircuitPython board can appear as a flash drive, a serial port, and a keyboard at the same time. The usb_hid and usb_midi modules let you build devices that interact with a computer as standard peripherals:
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
kbd = Keyboard(usb_hid.devices)
# Type a macro when a button is pressed
kbd.send(Keycode.CONTROL, Keycode.SHIFT, Keycode.T)
Customizing USB at Boot
The boot.py file runs once before code.py and configures USB behavior. You can disable the mass storage drive for security or rename the device:
# boot.py
import storage
import usb_cdc
import board
import digitalio
# Only enable USB drive if a button is held during boot
button = digitalio.DigitalInOut(board.BUTTON)
button.pull = digitalio.Pull.UP
if button.value: # button not pressed
storage.disable_usb_drive()
usb_cdc.disable()
This pattern is critical for deployed projects where you do not want end users accidentally modifying code.
Display Framework: displayio
CircuitPython includes displayio, a compositing display framework that manages screens, sprites, and text through a layered group system:
import board
import displayio
import terminalio
from adafruit_display_text import label
display = board.DISPLAY # built-in display on some boards
# Create a display group
splash = displayio.Group()
display.root_group = splash
# Background
bg_bitmap = displayio.Bitmap(display.width, display.height, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = 0x000033 # dark blue
bg_sprite = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)
splash.append(bg_sprite)
# Text label
text = label.Label(terminalio.FONT, text="Temp: 23.5°C",
color=0xFFFFFF, x=10, y=20)
splash.append(text)
The framework handles display refresh automatically. You modify the group contents, and displayio composites them to the screen at the appropriate refresh rate. This design avoids manual framebuffer management.
Supported Displays
CircuitPython supports a wide range of display hardware through driver modules:
- SSD1306 OLED (128x64, I2C/SPI)
- ST7789 and ILI9341 TFT (color, SPI)
- E-ink/E-paper displays (SSD1680, UC8151D)
- HUB75 RGB LED matrices (via
rgbmatrix) - NeoPixel strips treated as pixel arrays
Asynchronous Programming with asyncio
CircuitPython 7.1+ supports asyncio, enabling concurrent task patterns without threads:
import asyncio
import board
import digitalio
import neopixel
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10)
async def blink_led():
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
while True:
led.value = not led.value
await asyncio.sleep(0.5)
async def rainbow_cycle():
import rainbowio
offset = 0
while True:
for i in range(10):
pixels[i] = rainbowio.colorwheel((i * 25 + offset) % 255)
pixels.show()
offset = (offset + 1) % 255
await asyncio.sleep(0.02)
async def read_sensor():
import analogio
temp = analogio.AnalogIn(board.TEMPERATURE)
while True:
voltage = temp.value * 3.3 / 65535
temp_c = (voltage - 0.5) * 100
print(f"Temp: {temp_c:.1f}°C")
await asyncio.sleep(5)
async def main():
await asyncio.gather(
blink_led(),
rainbow_cycle(),
read_sensor()
)
asyncio.run(main())
This pattern lets you run multiple concurrent behaviors — animation, sensor reading, communication — without complex state machines.
Network Programming
CircuitPython on Wi-Fi-enabled boards (ESP32-S2, ESP32-S3, Pico W) provides networking through the wifi, socketpool, and adafruit_requests modules:
import wifi
import socketpool
import ssl
import adafruit_requests
wifi.radio.connect("MyNetwork", "MyPassword")
print("IP:", wifi.radio.ipv4_address)
pool = socketpool.SocketPool(wifi.radio)
session = adafruit_requests.Session(pool, ssl.create_default_context())
response = session.get("https://io.adafruit.com/api/v2/time/seconds")
print("Unix time:", response.text)
response.close()
Adafruit IO Integration
Adafruit IO is a cloud platform designed for IoT data. CircuitPython has first-class support:
from adafruit_io.adafruit_io import IO_HTTP
io = IO_HTTP("username", "aio_key", session)
# Send sensor data
io.send_data("temperature", 23.5)
# Receive commands
value = io.receive_data("led-control")
Memory Management Techniques
CircuitPython boards typically have 256 KB to 2 MB of RAM. Effective memory management is essential for complex projects:
import gc
# Check available memory
gc.collect()
print(f"Free: {gc.mem_free()} bytes")
# Pre-allocate buffers for repeated operations
sensor_buffer = bytearray(64)
display_buffer = bytearray(1024)
Reducing Library Memory Footprint
The library bundle offers both source (.py) and compiled (.mpy) versions. Always use .mpy files on the board — they use 30-50% less RAM than source files.
For very constrained boards, cherry-pick only the modules you need rather than copying entire library folders.
Building Custom Board Definitions
If you manufacture hardware or use an unsupported board, you can create a CircuitPython board definition:
- Fork the CircuitPython repository
- Create a board directory under
ports/<chip_family>/boards/ - Define
mpconfigboard.h(pin mappings, clock configuration) - Define
pins.c(board pin names exposed to Python) - Build with
make BOARD=your_board
The build system uses gcc-arm-none-eabi for ARM boards and xtensa-esp32s2-elf-gcc for ESP32 boards.
Testing and Development Workflow
Blinka: CircuitPython on Desktop
Adafruit’s Blinka library lets you run CircuitPython code on full Linux computers (Raspberry Pi, BeagleBone) using the same API:
pip install adafruit-blinka
# Same code works on CircuitPython board and Raspberry Pi
import board
import digitalio
led = digitalio.DigitalInOut(board.D18)
led.direction = digitalio.Direction.OUTPUT
led.value = True
This enables testing CircuitPython library code on a desktop before deploying to constrained hardware.
Continuous Integration
The CircuitPython project uses CI extensively. For your own libraries, Adafruit provides cookie-cutter templates that include pre-configured GitHub Actions for linting, building documentation, and running tests.
Production Deployment Considerations
For projects that ship to end users:
Lock down USB. Use boot.py to disable the drive and serial console. Provide a physical button combination to re-enable for updates.
Implement error recovery. CircuitPython shows errors on connected displays and falls back to the REPL. For headless deployments, add a try/except wrapper in code.py that logs errors to a file and restarts:
import supervisor
import time
while True:
try:
import main_app
main_app.run()
except Exception as e:
with open("error.log", "a") as f:
f.write(f"{time.monotonic()}: {e}\n")
time.sleep(5)
supervisor.reload()
Version your firmware. Track which CircuitPython version and library bundle version are deployed. Mismatched versions cause subtle bugs that are difficult to diagnose remotely.
One thing to remember: CircuitPython’s power is in its opinionated simplicity — USB-drive deployment, unified APIs, and a curated library ecosystem that trades flexibility for a development experience where hardware projects just work.
See Also
- Python Behavior Trees Robotics How robots make decisions using a tree-shaped rulebook that keeps them organized, like a flowchart that tells a robot what to do in every situation.
- Python Bluetooth Ble How Python connects to fitness trackers, smart locks, and wireless sensors using the invisible radio signals all around you.
- Python Computer Vision Autonomous How self-driving cars use cameras and Python to see the road, spot pedestrians, read signs, and understand traffic — like giving a car human eyes and a brain.
- Python Home Assistant Automation How Python turns your home into a smart home that reacts to you automatically, like a helpful invisible butler.
- Python Lidar Point Cloud Processing How self-driving cars use millions of laser dots to build a 3D picture of the world around them, and how Python helps make sense of it all.