Basics in CircuitPython

Step-by-step from your first blinking LED to your own mini game. No prior Python experience required – we keep things hands-on.

What is CircuitPython #

CircuitPython is Adafruit's flavor of Python designed to run directly on microcontrollers. You write regular Python, but the code runs right on the Picopad. Once the firmware is flashed, your Picopad shows up as a USB drive called CIRCUITPY – just drop a code.py file on it, and it runs automatically. Save the file and the board reboots itself. No upload button, no special tools required.

Picopad supports three programming languages. Pick whichever fits you:

Language For whom Speed
CircuitPython Beginners. Rich Adafruit libraries, edit code straight on a USB drive. Slower, but plenty for games and sensors.
MicroPython Very similar to CircuitPython, slightly smaller, no Adafruit ecosystem. Comparable to CircuitPython.
C / C++ Advanced users who want maximum performance (e.g. emulators). Fastest, but more involved project setup.

This tutorial uses CircuitPython. Big advantage: high-level libraries (displayio, adafruit_display_text, simpleio, keypad) and editing files straight over USB – no upload-from-editor dance. Ideal to start with.

What you'll need: an assembled Picopad, a Micro-USB cable, a Windows / macOS / Linux computer, and any text editor (Thonny with CircuitPython support is great, but VS Code or Notepad++ also work). We'll cover libraries and the first program in the next chapters.

Firmware installation #

Before you can write CircuitPython programs you need to flash the CircuitPython firmware to your Picopad. It's a one-time operation – once flashed, it stays on the device until you choose to overwrite it with a different language.

The procedure has its own page: How to install CircuitPython →

Tip: Picopad uses the official CircuitPython build from circuitpython.org/board/pajenicko_picopad/. Both currently shipping models (Picopad Wifi and Picopad Pro) carry the Pico W module, so the same build works for either – just pick the CircuitPython version you want.

First program – Hello Picopad #

With CircuitPython flashed, time to write something. CircuitPython makes life easy: the Picopad shows up as a USB drive called CIRCUITPY. Just edit the code.py file – when you save, the board reboots and runs the new code.

1) Find the CIRCUITPY drive

Connect the Picopad over USB and turn it on. A new drive named CIRCUITPY appears in your file explorer. Open it – you'll see a few files, mainly code.py (your program) and a lib folder (for libraries).

2) First line (via REPL)

Along with the USB drive, the board also exposes a serial port. Any terminal works (Thonny: View → Shell; on macOS: screen /dev/cu.usbmodem... 115200). Press Ctrl+C to interrupt the running code.py and you'll see >>>. Type:

print("Hello, Picopad!")

Press Enter and you'll get back:

Hello, Picopad!

3) Blink the LED (the code.py file)

In any editor (Thonny, VS Code, Notepad...) open code.py straight on the CIRCUITPY drive and replace it with:

import board
import digitalio
import time

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT

while True:
    led.value = False  # turns it on (LED is active LOW)
    time.sleep(0.5)
    led.value = True   # turns it off
    time.sleep(0.5)

Save. The Picopad reboots itself and the yellow user LED above the display starts blinking once a second.

Why does False mean "on"? The user LED is wired so it lights up when the pin is at logic LOW. That's a common hardware trick – setting digitalio value to False drives the pin low, which lights the LED.

4) Pins have names, not just numbers

Notice that you write board.LED, not "GPIO 22". That's a big difference compared to MicroPython or C – CircuitPython provides named pins for every Picopad component. board.SW_A is button A, board.AUDIO is the buzzer, board.DISPLAY is the already-initialized display. The next chapter has the full map.

5) What's next

The code.py file runs every time you turn on the Picopad. Unplug USB – the LED keeps blinking. That ends the "Hello world" part. The next chapters build on this: driving the display, reading buttons, playing sound, and more.

Picopad anatomy #

Before we dive into individual components, let's see what goes where. The diagram below is rendered straight from the Picopad's manufacturing files, so the buttons and LEDs are exactly where you'll find them on your console.

Picopad – front side with button and LED labels
Picopad front side – D-pad, A/B/X/Y buttons, status LEDs

Map of named pins

CircuitPython provides a named attribute in the board module for every Picopad peripheral. Reference them directly in code – no need to remember GPIO numbers.

Function CircuitPython Note
Displayboard.DISPLAYPre-initialized (320×240, displayio)
D-pad ↑ ↓ ← →board.SW_UP, SW_DOWN, SW_LEFT, SW_RIGHTPull-up input, pressed = False
A B X Y buttonsboard.SW_A, SW_B, SW_X, SW_YSame as D-pad, pressed = False
User LEDboard.LEDActive LOW (lit when False)
Buzzer / speakerboard.AUDIOUse via simpleio.tone() or audiopwmio
microSD cardboard.SD_SCK, SD_MOSI, SD_MISO, SD_CSDedicated SPI bus
Battery senseboard.BAT_SENSEDetails in the Battery chapter

External connector J2

On the side of the Picopad there's a 12-pin connector for sensors and modules (temperature sensors, ultrasonic distance, photoresistors...). The exposed pins:

  • Power: 3.3 V, GND, VBAT (raw battery), ADC_VREF, AGND
  • Digital / comms: board.D1, board.D2/board.RX/board.SCL, board.D3/board.TX/board.SDA
  • Analog inputs: board.A0 (= D0), board.A1 (= D4), board.A2 (= D5)

Tip: the "External connector" chapter shows a concrete example – reading a photoresistor over ADC and showing the value on the display.

Display #

The Picopad has a 2.0" IPS display at 320×240 pixels. CircuitPython exposes it as board.DISPLAY, already initialized – it feels almost as easy as drawing on a canvas in Python on a PC. For graphics use the built-in displayio, for text the adafruit_display_text library (download required).

Downloading libraries – the CircuitPython Library Bundle

Many examples need libraries from the Adafruit bundle. Steps:

  1. Open circuitpython.org/libraries and download the bundle that matches your CircuitPython major version (e.g. 9.x bundle).
  2. Unzip. Inside is a lib folder with dozens of .mpy libraries plus examples.
  3. Drag adafruit_display_text from the bundle's lib into CIRCUITPY/lib/ on the Picopad. Done.

Libraries in .mpy form are pre-compiled and use less RAM. You can also use .py versions (for tweaking or studying), but the RP2040 is RAM-limited, so .mpy is the safe default.

First text on screen

import board
import terminalio
from adafruit_display_text import label

display = board.DISPLAY
text_label = label.Label(terminalio.FONT, text="Hello, Picopad!", color=0xFFFFFF, scale=2)
text_label.x = 70
text_label.y = 110

display.root_group = text_label

while True:
    pass

What's happening:

  • board.DISPLAY – the display is already up, you just grab it.
  • terminalio.FONT – built-in 6×12 font, no download needed.
  • label.Label – text sprite. Color is RGB hex (0xFFFFFF = white), scale=2 doubles its size.
  • display.root_group – whatever you assign here is what's on screen. Swap it for a different group to switch "screens".
  • while True: pass – keeps the program alive. If the program exits, CircuitPython shows the console.

Groups, sprites and images

A real app has multiple elements – background, character, text. They go into a displayio.Group – a "scene". Here's a colored background plus text:

import board
import displayio
import terminalio
from adafruit_display_text import label

display = board.DISPLAY

# Group = scene
group = displayio.Group()

# Solid background: a 1px×1px bitmap stretched across the display
bg_bitmap = displayio.Bitmap(1, 1, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = 0x002244  # deep blue
bg_tile = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette,
                             width=display.width, height=display.height,
                             tile_width=1, tile_height=1)
group.append(bg_tile)

# Text
title = label.Label(terminalio.FONT, text="Picopad rocks", color=0xFFFFFF, scale=3)
title.x = 30
title.y = 100
group.append(title)

display.root_group = group

while True:
    pass

To load a BMP/PNG image, use adafruit_imageload (from the bundle):

import adafruit_imageload
import displayio

bitmap, palette = adafruit_imageload.load("/picture.bmp")
sprite = displayio.TileGrid(bitmap, pixel_shader=palette, x=20, y=20)

Full games in displayio: the GitHub repo has Pixel Snake and DinoRun – commented sources you can learn a lot from.

Buttons #

Picopad has eight buttons: the D-pad (↑ ↓ ← →) and the A/B/X/Y action cluster. CircuitPython has three ways to read them, ranging from simple to robust.

1) Simplest: digitalio

import board
from digitalio import DigitalInOut, Pull
import time

btn_a = DigitalInOut(board.SW_A)
btn_a.pull = Pull.UP

while True:
    if not btn_a.value:   # pressed = pin is at 0, .value is False
        print("A!")
    time.sleep(0.05)

Works, but holding the button prints "A!" a hundred times per second. And mechanical buttons "bounce" – one press registers as several.

2) With a debouncer (adafruit_debouncer library)

Drop adafruit_debouncer.mpy from the bundle into /lib. Then:

import board
from digitalio import DigitalInOut, Pull
from adafruit_debouncer import Debouncer
import time

pin = DigitalInOut(board.SW_A)
pin.pull = Pull.UP
btn_a = Debouncer(pin)

while True:
    btn_a.update()
    if btn_a.fell:        # just got pressed (pin fell from 1 to 0)
        print("A pressed")
    if btn_a.rose:        # just got released
        print("A released")
    time.sleep(0.01)

3) Best: keypad.Keys (built into CircuitPython)

The keypad module scans buttons in the background and queues press/release events – you never miss a press, even if your main loop has a long sleep.

import board
import keypad

KEY_A, KEY_B, KEY_X, KEY_Y, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT = range(8)

keys = keypad.Keys(
    (board.SW_A, board.SW_B, board.SW_X, board.SW_Y,
     board.SW_UP, board.SW_DOWN, board.SW_LEFT, board.SW_RIGHT),
    value_when_pressed=False,  # buttons are active LOW
    pull=True,
)

while True:
    event = keys.events.get()
    if event:
        if event.pressed:
            print("Pressed key #", event.key_number)
        if event.released:
            print("Released key #", event.key_number)

For game-style input, use keypad – the least code, the most reliable behavior.

Don't forget the pull-up: all Picopad buttons are active LOW (closed switch = pin at 0). Forgetting pull=Pull.UP leaves the pin "floating" and reads will be random – buttons appear to work only sometimes.

User LED #

The yellow LED above the display (label USR) is free for your programs to use. Connect to it via board.LED.

On/off + blinking

import board
import digitalio
import time

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT

# Blink 5 times
for _ in range(5):
    led.value = False  # on (active LOW)
    time.sleep(0.2)
    led.value = True   # off
    time.sleep(0.2)

Dimming with PWM

Smooth fading needs PWM (pulse-width modulation), not just on/off. CircuitPython has pwmio:

import board
import pwmio
import time

led = pwmio.PWMOut(board.LED, frequency=1000, duty_cycle=0)

# Fade up and down (LED is active LOW, so we "invert" the duty_cycle)
while True:
    for d in range(0, 65536, 1024):
        led.duty_cycle = 65535 - d  # 65535 = off, 0 = fully lit
        time.sleep(0.01)
    for d in range(65535, -1, -1024):
        led.duty_cycle = 65535 - d
        time.sleep(0.01)

duty_cycle ranges 0 to 65535. Because the LED is active LOW, a higher PWM value = darker. If that confuses you, treat "brightness" as 65535 - duty_cycle.

microSD card #

The Picopad has a microSD slot under the display. It's perfect for game saves, log files, images, or sound assets that wouldn't fit in the chip's flash.

CircuitPython has SD support built in – once mounted, the card behaves like any normal disk and you write to it with open(...).

import board
import busio
import digitalio
import sdcardio
import storage

# SPI bus for the SD slot
spi = busio.SPI(clock=board.SD_SCK, MOSI=board.SD_MOSI, MISO=board.SD_MISO)
cs = digitalio.DigitalInOut(board.SD_CS)

# SDCard object + mount under /sd
sd = sdcardio.SDCard(spi, cs)
vfs = storage.VfsFat(sd)
storage.mount(vfs, "/sd")

# Write – just like a regular file
with open("/sd/log.txt", "a") as f:
    f.write("Hello from Picopad!\n")

# Read
with open("/sd/log.txt") as f:
    print(f.read())

Format: the SD card must be FAT/FAT32. Cards using exFAT (typical for 64+ GB) need to be reformatted to FAT32. We recommend microSD cards up to 32 GB.

One quirk: CircuitPython protects its own filesystem on the chip flash from being written by code (otherwise it would conflict with USB editing). The card mounted at /sd doesn't have that restriction – write freely.

Buzzer and sound #

The Picopad has a tiny speaker (label SPEAKER) driven through board.AUDIO. A single line gives you a beep; for music you can go further with PWM and synthesis.

Simple beep (simpleio)

Drop simpleio.mpy from the bundle into /lib:

import board
import simpleio

# 880 Hz beep for 0.2 s
simpleio.tone(board.AUDIO, 880, 0.2)

Short melody

import board
import simpleio
import time

# Note frequencies in Hz
notes = [
    ("C5", 523),
    ("E5", 659),
    ("G5", 784),
    ("C6", 1047),
]

for name, freq in notes:
    print(name)
    simpleio.tone(board.AUDIO, freq, 0.3)
    time.sleep(0.05)

Advanced: synthio (synthesis)

For real game-grade sound effects there's synthio – a synthesizer built into CircuitPython that handles polyphony, FM modulation, filters. Inspiration: the monosynth example in the Picopad repo.

The speaker is tiny, so don't expect hi-fi. For game sounds it's plenty. Volume is controlled via PWM duty_cycle – the maximum (32767) is "loud enough" for almost everything.

On Picopad Pro: there's an upgraded amplifier and a 3.5mm headphone jack – plug in headphones to play comfortably without bothering anyone around you. A hardware mute switch silences the speaker with a single click, no code change required.

Battery monitoring #

The Picopad runs from a Li-ion battery (3.7 V nominal). To show "battery low" in your game you need to read its voltage via analogio.AnalogIn on board.BAT_SENSE.

import board
import analogio
import time

vbat_pin = analogio.AnalogIn(board.BAT_SENSE)

# Reference is 3.3 V, ADC returns 0..65535. Voltage at the pin:
def battery_voltage():
    raw = vbat_pin.value          # 0..65535
    voltage_at_pin = (raw / 65535) * 3.3
    # BAT_SENSE has the battery voltage through a 1:2 divider, so double it
    return voltage_at_pin * 2

while True:
    v = battery_voltage()
    print("Battery: {:.2f} V".format(v))
    time.sleep(2)

Rough mapping to a percentage:

Voltage State
~ 4.1 VFully charged
~ 3.7 VHalf
~ 3.4 VAlmost empty
< 3.2 VEmpty (Picopad will shut down soon)

Readings vary slightly between units – the RP2040 ADC is only ~12-bit and noisy. Fine for gameplay UI; for science use a dedicated measuring circuit.

External connector #

The 12-pin J2 connector on the side gives you digital pins D0..D5, analog inputs A0..A2, I2C, UART and power. Use it for temperature sensors, OLED displays, rotary encoders, distance sensors...

Example: photoresistor (ambient light)

Wire the photoresistor between 3.3 V and pin D0 (= A0), and a 10 kΩ resistor from D0 to GND – the classic voltage divider.

import board
import analogio
import time

light = analogio.AnalogIn(board.A0)

while True:
    raw = light.value
    voltage = (raw / 65535) * 3.3
    print("Light (raw): {}, voltage: {:.2f} V".format(raw, voltage))
    time.sleep(0.5)

Cover it with your hand – the number drops; shine a phone flashlight on it – it spikes. From here it's one step to showing the value on the display.

Example: I2C device (e.g. BME280 temperature)

Most I2C sensors with Adafruit libraries work out of the box:

import board
import time
from adafruit_bme280 import basic as adafruit_bme280

i2c = board.I2C()                     # SDA = board.SDA, SCL = board.SCL
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c)

while True:
    print("Temp: {:.1f} C, humidity: {:.0f} %, pressure: {:.0f} hPa"
          .format(sensor.temperature, sensor.relative_humidity, sensor.pressure))
    time.sleep(2)

The adafruit_bme280 library is in the bundle. The same pattern connects SCD4x (CO₂), SSD1306 (OLED), and so on.

Inspiration: circuitpython/sensors has finished examples for DS18B20 (1-Wire temperature), HCSR04 (ultrasonic distance) and a photoresistor.

On Picopad Pro – more connectors: beyond the classic J2 the Pro adds the PICOBUS expansion connector for snap-on cards (extra buttons, gamepads, RTC modules…) and a dedicated Stemma/Qwiic-compatible I2C connector – plug in any of the hundreds of ready-made sensors and OLED screens from Adafruit, Sparkfun and the community, no soldering or voltage divider needed.

WiFi #

Picopad Wifi and Picopad Pro both carry the Raspberry Pi Pico W module, so WiFi and Bluetooth are part of the deal. The CircuitPython API is super clean – the wifi module with the wifi.radio object.

Storing credentials (settings.toml)

To keep secrets out of code.py, CircuitPython reads them from settings.toml in the root of CIRCUITPY:

CIRCUITPY_WIFI_SSID = "my_wifi"
CIRCUITPY_WIFI_PASSWORD = "secret_password"

Connect and ping

import os
import wifi
import socketpool
import ipaddress
import time

print("Connecting to WiFi...")
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"),
                   os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print("Connected, IP:", wifi.radio.ipv4_address)

pool = socketpool.SocketPool(wifi.radio)
google = ipaddress.ip_address("8.8.4.4")

while True:
    ms = wifi.radio.ping(google)
    if ms is None:
        print("No response")
    else:
        print("Ping: {:.1f} ms".format(ms * 1000))
    time.sleep(15)

Scan nearby networks

import wifi

print("SSID                          RSSI  CHN")
for net in wifi.radio.start_scanning_networks():
    print("{:30s} {:5d}  {:3d}".format(net.ssid, net.rssi, net.channel))
wifi.radio.stop_scanning_networks()

HTTP request

Drop adafruit_requests.mpy from the bundle into /lib:

import os
import wifi
import socketpool
import ssl
import adafruit_requests

wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"),
                   os.getenv("CIRCUITPY_WIFI_PASSWORD"))

pool = socketpool.SocketPool(wifi.radio)
session = adafruit_requests.Session(pool, ssl.create_default_context())

resp = session.get("https://api.github.com/repos/Pajenicko/Picopad")
data = resp.json()
print("Stars:", data["stargazers_count"])

Finished examples are in circuitpython/network: webserver (polling, SSE, WebSocket), NTP clock, Telegram bot, transit-departure scraper, and more.

Mini project – "Reflex" #

Time to put it all together. The game tests your reaction time: after a random delay the LED flashes, you have to hit A as fast as you can. The measured time shows up on the display.

import board
import digitalio
import keypad
import simpleio
import displayio
import terminalio
import random
import time
from adafruit_display_text import label

# LED
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led.value = True   # off

# Button A
keys = keypad.Keys((board.SW_A,), value_when_pressed=False, pull=True)

# Display
display = board.DISPLAY
group = displayio.Group()

title = label.Label(terminalio.FONT, text="REFLEX TEST", color=0xFFFFFF, scale=3)
title.x = 50
title.y = 60
group.append(title)

info = label.Label(terminalio.FONT, text="Press A to start", color=0xAAAAAA, scale=2)
info.x = 35
info.y = 130
group.append(info)

result = label.Label(terminalio.FONT, text="", color=0x44FF44, scale=3)
result.x = 70
result.y = 200
group.append(result)

display.root_group = group


def wait_for_a():
    keys.events.clear()
    while True:
        ev = keys.events.get()
        if ev and ev.pressed:
            return


while True:
    info.text = "Press A to start"
    result.text = ""
    wait_for_a()

    info.text = "Wait..."
    result.text = ""
    time.sleep(random.uniform(1.5, 4.0))

    led.value = False        # flash
    info.text = "NOW!"
    start = time.monotonic()

    keys.events.clear()
    while True:
        ev = keys.events.get()
        if ev and ev.pressed:
            elapsed_ms = int((time.monotonic() - start) * 1000)
            led.value = True
            simpleio.tone(board.AUDIO, 880, 0.1)
            result.text = "{} ms".format(elapsed_ms)
            info.text = "A = retry"
            break

Try it. How many milliseconds do you get? Human average is around 250 ms.

Improvements to think about: Add false starts (LED flashes earlier than you should react)? Save the best score to the SD card? Build a 2-player tournament A vs. B? You already know all the building blocks.

Further reading #

This tutorial covered the basics. To go deeper:

Finished projects in the repo

  • Pajenicko/Picopad – CircuitPython – hello_world, games (Snake, DinoRun), sensors, networking, macro keyboard, monosynth, slideshow, alarm clock...
  • network/ – webserver (polling/SSE/WebSocket), Golemio transit, Telegram bot, NTP clock, Teletext, ŽivýObraz.eu.
  • sensors/ – I2C sensors (SCD4x, BME280), 1-Wire (DS18B20), ultrasonic (HC-SR04), photoresistor.

Official documentation

Community inspiration

For ideas of what people have already built – GameBoy emulator, ZX Spectrum 48k, DOOM port, MakeCode Arcade patcher, custom 3D-printed cases, improved button caps – head to the Community page.

Feedback

This tutorial gets better with your input. If you spot a bug, want another chapter, or built something cool you'd like to show off, ping us via Pajeníčko or the GitHub repo. Happy hacking!