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.
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 |
|---|---|---|
| Display | board.DISPLAY | Pre-initialized (320×240, displayio) |
| D-pad ↑ ↓ ← → | board.SW_UP, SW_DOWN, SW_LEFT, SW_RIGHT | Pull-up input, pressed = False |
| A B X Y buttons | board.SW_A, SW_B, SW_X, SW_Y | Same as D-pad, pressed = False |
| User LED | board.LED | Active LOW (lit when False) |
| Buzzer / speaker | board.AUDIO | Use via simpleio.tone() or audiopwmio |
| microSD card | board.SD_SCK, SD_MOSI, SD_MISO, SD_CS | Dedicated SPI bus |
| Battery sense | board.BAT_SENSE | Details 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:
- Open circuitpython.org/libraries and download the bundle that matches your CircuitPython major version (e.g. 9.x bundle).
- Unzip. Inside is a
libfolder with dozens of.mpylibraries plusexamples. - Drag
adafruit_display_textfrom the bundle'slibintoCIRCUITPY/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=2doubles 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.
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 V | Fully charged |
| ~ 3.7 V | Half |
| ~ 3.4 V | Almost empty |
| < 3.2 V | Empty (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
- docs.circuitpython.org – complete module reference (board, displayio, wifi, synthio...).
- Welcome to CircuitPython – Adafruit's friendly intro guide.
- circuitpython.org/libraries – library bundles, always grab the version that matches your firmware.
- CircuitPython Essentials – worked examples for digital IO, ADC, PWM, IR, audio, displays...
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!