What is MicroPython #
MicroPython is a lean version of Python designed to run directly on microcontrollers. You write the same language as on a regular computer, but the code runs right on the Picopad. When you connect it over USB you can talk to it in real time – type a line, hit Enter, and the Picopad runs it immediately.
Picopad supports three programming languages. Pick whichever fits you:
| Language | For whom | Speed |
|---|---|---|
| MicroPython | Beginners and hobby projects. Simple syntax, instant results. | Slower than C, plenty fast for games and sensors. |
| CircuitPython | Very similar to MicroPython, with Adafruit's library ecosystem. | Comparable to MicroPython. |
| C / C++ | Advanced users who want maximum performance (e.g. emulators). | Fastest, but with a heavier toolchain. |
This tutorial follows the MicroPython path. Once you've nailed the basics, switching to CircuitPython or C is a small step away.
What you need: an assembled Picopad, a Micro-USB cable, a Windows / macOS / Linux computer, and the free Thonny editor. We'll cover installing those in the next chapter.
Firmware installation #
Before you can write MicroPython programs, you need to flash the MicroPython firmware onto the Picopad. It's a one-off task – flash it once and the Picopad keeps it until you decide to switch to a different language.
Full step-by-step in a separate guide: How to install MicroPython →
Tip: Picopad uses the stock official MicroPython from micropython.org – no special Pajenicko build. Both currently shipping models (Picopad Wifi and Picopad Pro) carry the Raspberry Pi Pico W module, so the same RPI_PICO_W build works for either.
First program – Hello Picopad #
With MicroPython flashed, it's time to write something. We'll use Thonny, a free editor with built-in MicroPython support.
1) Install Thonny
Download Thonny from thonny.org and install it. On first launch pick the Standard initial settings.
2) Connect Picopad and pick the interpreter
Plug the Picopad in over USB and turn it on. In Thonny open Run → Configure interpreter… and on the Interpreter tab choose:
- Interpreter: MicroPython (Raspberry Pi Pico)
- Port: the one where Picopad shows up (e.g.
/dev/cu.usbmodem...on macOS,COM3on Windows)
After confirming, the bottom Shell pane shows MicroPython v1.xx ... and >>>. That's the REPL – MicroPython's interactive console. Type into it and the Picopad will answer instantly.
3) First line in the REPL
Click into the Shell and type:
print("Hello, Picopad!")
After Enter you'll see the reply:
Hello, Picopad!
Congratulations – you just ran your first program on the Picopad. The REPL is great for quick experiments, but as soon as you unplug USB everything is gone. For programs that should run on their own we need a file: main.py.
4) Blinking the LED (main.py)
In Thonny open a new file (File → New) and type:
from machine import Pin
from time import sleep
led = Pin(22, Pin.OUT)
while True:
led.value(0) # LED on (it's active-low)
sleep(0.5)
led.value(1) # LED off
sleep(0.5)
Save the file directly onto the Picopad as main.py: File → Save as…, pick Raspberry Pi Pico, name it main.py.
Press the green Run button (or F5). The yellow user LED on the Picopad's top edge starts blinking once a second.
Why does 0 mean ON? The user LED is wired so that it lights up when its pin is at logic LOW. That's a common hardware trick – we'll explain it in later chapters.
5) What's next
The main.py file runs every time the Picopad powers on. You can unplug USB, turn the Picopad on standalone, and the LED still blinks. That's the end of the "Hello world" portion. The remaining chapters build on this foundation: driving the display, reading buttons, playing sounds, and more.
Picopad anatomy #
Before diving into individual hardware features, let's map out where everything is. The diagram below is rendered straight from Picopad's manufacturing files, so the buttons and LEDs sit exactly where you'll find them on your console.
GPIO pin map
Picopad uses the Raspberry Pi Pico as its brain. Below is the wiring – which Pico pin (GPIO number) connects to what. In code you'll refer to peripherals by these numbers.
| Function | GPIO | Notes |
|---|---|---|
| Display (SPI 0) | SCK=18, MOSI=19 | + CS=21, DC=17, RES=20, backlight=16 |
| D-pad ↑ ↓ ← → | UP=4, DOWN=5, LEFT=3, RIGHT=2 | Inputs with pull-up; pressed = LOW |
| Buttons A B X Y | A=7, B=6, X=9, Y=8 | Same as the D-pad; pressed = LOW |
| User LED | 22 | Active-low (lights up when value=0) |
| Buzzer / speaker | 15 | Driven via PWM |
| microSD card (SPI 1) | SCK=10, MOSI=11, MISO=12, CS=13 | Separate SPI bus |
| Battery sensing (VSYS) | ADC 3 (pin 29) | Details in the Battery chapter |
External connector J2
Picopad's side has a 12-pin header where you can attach sensors and modules (temperature sensor, ultrasonic ranger, photoresistor, etc.). The exposed pins are:
- Power: 3.3 V, GND, BAT (raw battery), ADC_VREF, AGND
- Digital / comms: GPIO 0, GPIO 1 (usable as UART, I2C or generic), GPIO 14
- Analog inputs: GPIO 26 (ADC0), GPIO 27 (ADC1), GPIO 28 (ADC2)
Heads up: the "External connector" chapter walks through a concrete example – reading a photoresistor over ADC and showing the value on the display.
Display #
Picopad has a 2-inch IPS color display, 320 × 240 pixels, with the ST7789 controller. It's wired over SPI. MicroPython can't drive it on its own – you need a library (driver) and a font.
1) Grab the library from GitHub
Pajeníčko ships the driver and fonts on GitHub: Pajenicko/Picopad → micropython/lib. Download the entire lib folder. You should end up with:
lib/
├── st7789.py
└── fonts/
├── fonts_vga1_16x32.py
└── fonts_vga2_8x8.py
2) Upload the library to Picopad
In Thonny, open View → Files. The left pane is your computer, the right pane is the Picopad. Find the downloaded lib folder, right-click it and choose Upload to /. After a moment the lib folder shows up in the right pane.
Sanity check: in the Thonny shell type import os; os.listdir(). The output should include lib.
3) Initialize the display
Before drawing anything, the display needs to know which GPIO pins it sits on and how to talk SPI. This boilerplate appears in most programs – consider it the standard intro:
from machine import Pin, SPI
from fonts import fonts_vga1_16x32
import st7789
spi = SPI(0, 62500000, sck=Pin(18), mosi=Pin(19), polarity=1, phase=1)
display = st7789.ST7789(
spi, 320, 240,
reset=Pin(20, Pin.OUT),
dc=Pin(17, Pin.OUT),
cs=Pin(21, Pin.OUT),
backlight=Pin(16, Pin.OUT),
rotation=1,
)
4) First text on screen
Fill the screen black, then write some text:
display.fill(0x0000) # black background
display.text(fonts_vga1_16x32, "Hello, Picopad!",
20, 100, 0xFFE0, 0x0000) # yellow text
The text arguments are: font, string, x, y, foreground color, background color. Coordinate (0, 0) is the top-left corner; x grows to the right, y downwards.
5) RGB565 colors
The display uses 16-bit RGB565 colors. In code you write them as a hex number. Quick cheat sheet:
| Color | RGB565 |
|---|---|
| Black | 0x0000 |
| White | 0xFFFF |
| Red | 0xF800 |
| Green | 0x07E0 |
| Blue | 0x001F |
| Yellow | 0xFFE0 |
| Cyan | 0x07FF |
6) Things to try
display.fill_rect(x, y, w, h, color)– filled rectangledisplay.pixel(x, y, color)– a single pixeldisplay.line(x1, y1, x2, y2, color)– a line- Try the smaller
fonts_vga2_8x8font – more text fits
User LED #
The yellow LED labelled USR on Picopad's top edge is wired to GPIO 22. Use it as a status indicator – blink while waiting, light up while something is happening, whatever you need.
Active-low – why 0 means ON
The LED is wired between 3.3 V and the GPIO pin. For the LED to light up, current must flow into the pin, so the pin must be at 0 V (LOW). When the pin is HIGH (3.3 V), there's no voltage across the LED and it stays dark.
In code: led.value(0) = on, led.value(1) = off. Most built-in microcontroller LEDs use the same convention.
Blinking
from machine import Pin
from time import sleep
led = Pin(22, Pin.OUT)
led.value(1) # start off
while True:
led.value(0) # on
sleep(0.5)
led.value(1) # off
sleep(0.5)
Smooth dimming with PWM
Instead of just on/off we can adjust brightness using PWM (rapid pulsing). Here's a "breathing" effect – the LED ramps up and back down:
from machine import Pin, PWM
from time import sleep
led = PWM(Pin(22))
led.freq(1000) # 1 kHz – the eye doesn't see flicker
while True:
# ramp up
for level in range(0, 65536, 500):
led.duty_u16(65535 - level) # active-low: higher duty = less light
sleep(0.005)
# ramp down
for level in range(65535, 0, -500):
led.duty_u16(65535 - level)
sleep(0.005)
duty_u16 takes 0–65535. Because of the active-low wiring we invert the value (65535 - level) so that "more brightness" really means more light.
LED + button
Putting it together: pressing button A toggles the LED (press once → on, press again → off).
from machine import Pin
from time import sleep
led = Pin(22, Pin.OUT)
led.value(1)
button_a = Pin(7, Pin.IN, Pin.PULL_UP)
is_on = False
while True:
if button_a.value() == 0:
is_on = not is_on
led.value(0 if is_on else 1)
sleep(0.2) # simple debounce
microSD card #
Picopad has a microSD slot on its back. The card lives on its own SPI bus (SPI 1), so it doesn't compete with the display – your app can talk to both at once. Use it for high-scores, logs, image assets or game data.
1) Get the SDCard driver
The official driver lives in micropython-lib. Download sdcard.py and upload it into the lib folder on the Picopad (same way you did with st7789.py).
2) Mounting the card
from machine import Pin, SPI
import sdcard
import os
# microSD on SPI 1
spi = SPI(1, baudrate=1_000_000,
sck=Pin(10), mosi=Pin(11), miso=Pin(12))
sd = sdcard.SDCard(spi, Pin(13))
os.mount(sd, "/sd")
print(os.listdir("/sd"))
After os.mount the card looks like another folder in the Picopad's filesystem (path /sd). You can read and write to it with regular Python file operations.
3) Writing and reading a file
# write
with open("/sd/score.txt", "w") as f:
f.write("highscore: 1240\n")
# read it back
with open("/sd/score.txt") as f:
print(f.read())
4) Show the card contents on the display
# (assuming display is already initialized)
display.fill(0x0000)
y = 10
for name in os.listdir("/sd"):
display.text(fonts_vga2_8x8, name[:38], 10, y, 0xFFFF, 0x0000)
y += 12
if y > 230:
break
Before pulling out the card call os.umount("/sd"), otherwise unsaved data may be lost.
Buzzer and sound #
Picopad's speaker is driven via PWM (rapid pulsing) on GPIO 15. The pulse frequency sets the pitch, the pulse width sets the volume. It's not hi-fi, but for game pings, melodies and sound effects it's plenty.
A helper for playing tones
from machine import Pin, PWM
from time import sleep
buzzer = PWM(Pin(15))
def tone(freq, duration_s, gap_s=0.05):
"""Play a tone at the given frequency for the given duration."""
buzzer.duty_u16(int(65535 * 0.05)) # ~5%, audible but quiet
buzzer.freq(freq)
sleep(duration_s)
buzzer.duty_u16(0)
sleep(gap_s)
buzzer.duty_u16(0) silences the tone. The short gap_s between notes is useful – without it adjacent notes blur together.
Note frequencies
| Note | Frequency (Hz) |
|---|---|
| C4 (middle C) | 262 |
| D4 | 294 |
| E4 | 330 |
| F4 | 349 |
| G4 | 392 |
| A4 (concert A) | 440 |
| B4 | 494 |
| C5 | 523 |
One octave up = frequency × 2, one octave down = / 2.
A short melody
# Opening of "Ode to Joy"
melody = [
(330, 0.4), (330, 0.4), (349, 0.4), (392, 0.4),
(392, 0.4), (349, 0.4), (330, 0.4), (294, 0.4),
(262, 0.4), (262, 0.4), (294, 0.4), (330, 0.4),
(330, 0.6), (294, 0.2), (294, 0.6),
]
for freq, dur in melody:
tone(freq, dur)
buzzer.deinit()
Buttons as keys
Each button plays a different note – a simple instrument:
from machine import Pin, PWM
from time import sleep
buzzer = PWM(Pin(15))
buzzer.duty_u16(0)
keys = {
Pin(3, Pin.IN, Pin.PULL_UP): 262, # LEFT → C
Pin(4, Pin.IN, Pin.PULL_UP): 330, # UP → E
Pin(5, Pin.IN, Pin.PULL_UP): 392, # DOWN → G
Pin(2, Pin.IN, Pin.PULL_UP): 523, # RIGHT → C (octave)
}
while True:
playing = False
for btn, freq in keys.items():
if btn.value() == 0:
buzzer.duty_u16(int(65535 * 0.05))
buzzer.freq(freq)
playing = True
break
if not playing:
buzzer.duty_u16(0)
sleep(0.02)
On Picopad Pro: a 3.5mm headphone jack and an upgraded amplifier are on board, perfect for playing on the bus without bothering anyone – the headphones are comfortably loud. A hardware mute switch silences the speaker with a single click, no code change required.
Battery monitoring #
Picopad runs from a Li-ion cell (500 mAh on Picopad Wifi, 600 mAh on Picopad Pro). The Pico W can read its own supply voltage (VSYS) through its internal ADC, and from that you can estimate the state of charge. A fully charged cell sits at ~4.2 V, an empty one at ~3.2 V. The reading lives on pin 29 (ADC channel 3).
The vsys() helper
On the Pico W, pin 29 is shared with the WiFi module, so before reading you have to disable WiFi temporarily and re-enable it afterwards. This works on both currently shipping models (Wifi and Pro), since both carry a Pico W:
from machine import Pin, ADC
import network
def vsys():
wlan = network.WLAN(network.STA_IF)
was_active = wlan.active()
try:
wlan.active(False)
Pin(25, mode=Pin.OUT, pull=Pin.PULL_DOWN).high()
Pin(29, Pin.IN)
adc = ADC(3)
# on-board divider scales 1:3, ADC reference is 3.3 V
voltage = adc.read_u16() * 3 * 3.3 / 65535
voltage += 0.311 # compensates for the protection diode drop
return voltage
finally:
Pin(29, Pin.ALT, pull=Pin.PULL_DOWN, alt=7)
wlan.active(was_active)
print(f"VSYS = {vsys():.2f} V")
Battery state on the display
from time import sleep
while True:
v = vsys()
percent = max(0, min(100, int((v - 3.2) / (4.2 - 3.2) * 100)))
display.fill(0x0000)
display.text(fonts_vga1_16x32, f"{v:.2f} V", 60, 80, 0xFFE0, 0x0000)
display.text(fonts_vga1_16x32, f"{percent} %", 60, 130, 0xFFFF, 0x0000)
sleep(2)
About linearization: mapping voltage to percentage like above is rough. Lithium cells don't discharge linearly. For a game it's fine; for accurate fuel gauging you'd use a proper discharge curve.
External connector #
The J2 header on Picopad's side opens the door to add-ons. Pajeníčko sells ready-made modules (DS18B20 temperature probe, HC-SR04 ultrasonic ranger, GL5516 photoresistor), but you can plug in anything – motion sensors, OLED displays, RFID readers.
Recap of pins from the Anatomy chapter:
- Power: 3.3 V, GND, BAT, ADC_VREF, AGND
- Digital: GPIO 0, GPIO 1, GPIO 14
- Analog (ADC): GPIO 26 (ADC0), GPIO 27 (ADC1), GPIO 28 (ADC2)
Example: photoresistor (light sensor)
A photoresistor changes its resistance with light intensity. Wire one end to 3.3 V, the other to an ADC pin and through a pull-down resistor (~10 kΩ) to GND. The ADC then reads a voltage that rises with brightness.
from machine import Pin, ADC
from time import sleep
photoresistor = ADC(Pin(26)) # GPIO 26 = ADC0 on the J2 header
while True:
raw = photoresistor.read_u16() # 0 - 65535
percent = raw * 100 // 65535
print(f"Light: {percent}%")
sleep(0.2)
Bar gauge on the display
Threshold values depend on lighting conditions. Instead of numbers, draw a "candle" – a bar whose height tracks the intensity:
# (display already initialized)
display.fill(0x0000)
display.text(fonts_vga1_16x32, "Light sensor", 60, 20, 0x07FF, 0x0000)
while True:
raw = photoresistor.read_u16()
height = int(raw / 65535 * 180) # 0 - 180 px
# erase the old bar
display.fill_rect(140, 50, 40, 180, 0x0000)
# draw the new one (from the bottom up)
display.fill_rect(140, 50 + 180 - height, 40, height, 0xFFE0)
sleep(0.05)
Ready-made Pajeníčko modules
Pajeníčko sells three pre-wired modules with a connector that snaps onto J2. Sample code for each is in the Picopad repo:
module_ds18b20.py– DS18B20 digital thermometer (OneWire)module_hc-sr04.py– HC-SR04 ultrasonic rangermodule_photoresistor.py– the photoresistor variant we just built
Picopad Pro – more connectors: in addition to 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 without soldering.
WiFi #
Picopad Wifi and Picopad Pro both ship with the Raspberry Pi Pico W module, so you get 2.4 GHz WiFi 802.11n and Bluetooth 5.2 on board.
Sanity check
The network module only exists on a Pico W. On any current Picopad it should import cleanly:
try:
import network
print("Pico W - WiFi available")
except ImportError:
print("Plain Pico - this chapter isn't for you")
Scanning for networks
import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
for s in wlan.scan():
ssid = s[0].decode("utf-8", "replace")
rssi = s[3]
print(f"{ssid:30s} signal: {rssi} dBm")
Connecting to a network
import network
from time import sleep
SSID = "MyNetwork"
PASS = "secretpassword"
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASS)
# wait up to 15 seconds for the connection
for _ in range(30):
if wlan.isconnected():
break
sleep(0.5)
if wlan.isconnected():
ip, mask, gw, dns = wlan.ifconfig()
print(f"Connected! IP: {ip}")
else:
print("Failed to connect")
HTTP request and on-screen result
Let's fetch the current time from a public API and show it on the display:
import urequests
# the device must already be connected to WiFi
r = urequests.get("https://worldtimeapi.org/api/timezone/Europe/Prague")
data = r.json()
r.close()
now = data["datetime"][11:19] # slice out HH:MM:SS
display.fill(0x0000)
display.text(fonts_vga1_16x32, "Prague time:", 50, 70, 0x07FF, 0x0000)
display.text(fonts_vga1_16x32, now, 100, 120, 0xFFE0, 0x0000)
HTTPS note: the bundled urequests doesn't verify TLS certificates. Fine for hobby projects; for production-grade work consider pinning the host MAC or using a more robust TLS library.
Bluetooth is a big topic on its own – the aioble library, BLE GATT, and friends. Let us know if you want a chapter for it.
Mini project – reaction tester #
Time to put it all together. We'll build a small reaction-time game:
- The screen shows instructions and waits for A.
- After the press, it waits a random 1–4 seconds.
- Suddenly the screen flashes green, the buzzer beeps – "GO!"
- The player slams A as fast as they can.
- The game prints how many milliseconds passed between GO and the press.
- If the player presses before GO, it's a false start.
Together this exercises the display, buttons, buzzer, timers and the random module. Save the code as main.py and run:
from machine import Pin, SPI, PWM
from fonts import fonts_vga1_16x32, fonts_vga2_8x8
from time import sleep, ticks_ms, ticks_diff
import st7789
import random
# --- HW init ---
spi = SPI(0, 62500000, sck=Pin(18), mosi=Pin(19), polarity=1, phase=1)
display = st7789.ST7789(spi, 320, 240,
reset=Pin(20, Pin.OUT), dc=Pin(17, Pin.OUT),
cs=Pin(21, Pin.OUT), backlight=Pin(16, Pin.OUT),
rotation=1)
button_a = Pin(7, Pin.IN, Pin.PULL_UP)
buzzer = PWM(Pin(15))
buzzer.duty_u16(0)
# --- Helpers ---
def text_center(font, text, y, fg, bg):
"""Draw text horizontally centered."""
width = len(text) * font.WIDTH
x = (320 - width) // 2
display.text(font, text, x, y, fg, bg)
def beep(freq, ms):
buzzer.duty_u16(int(65535 * 0.05))
buzzer.freq(freq)
sleep(ms / 1000)
buzzer.duty_u16(0)
# --- Main loop ---
def wait_for_press():
while button_a.value() != 0:
sleep(0.01)
while button_a.value() == 0: # wait for release
sleep(0.01)
while True:
# Title screen
display.fill(0x0000)
text_center(fonts_vga1_16x32, "REACTION TEST", 60, 0x07FF, 0x0000)
text_center(fonts_vga2_8x8, "Press A to start", 110, 0xFFFF, 0x0000)
wait_for_press()
# Get ready
display.fill(0x0000)
text_center(fonts_vga1_16x32, "Get ready...", 100, 0xFFE0, 0x0000)
# Random wait 1-4 s, watch for false start
wait_ms = random.randint(1000, 4000)
start = ticks_ms()
false_start = False
while ticks_diff(ticks_ms(), start) < wait_ms:
if button_a.value() == 0:
false_start = True
break
sleep(0.005)
if false_start:
display.fill(0xF800) # red = error
text_center(fonts_vga1_16x32, "FALSE START!", 100, 0xFFFF, 0xF800)
beep(200, 400)
sleep(2)
continue
# GO!
display.fill(0x07E0) # green
text_center(fonts_vga1_16x32, "GO!", 100, 0x0000, 0x07E0)
beep(880, 80)
go_time = ticks_ms()
# Measure
while button_a.value() != 0:
sleep(0.001)
reaction = ticks_diff(ticks_ms(), go_time)
# Result
display.fill(0x0000)
text_center(fonts_vga1_16x32, f"{reaction} ms", 80, 0xFFE0, 0x0000)
if reaction < 250:
verdict = "Lightning reflex!"
elif reaction < 400:
verdict = "Solid reaction."
else:
verdict = "Try again."
text_center(fonts_vga2_8x8, verdict, 130, 0xFFFF, 0x0000)
text_center(fonts_vga2_8x8, "Press A for another round", 200, 0x07FF, 0x0000)
sleep(0.5) # tiny cooldown so it doesn't restart immediately
wait_for_press()
Ideas to extend it: save the best score on the microSD card, add a 3-2-1 countdown before GO, support multiple players (rotating ABXY), or add a "difficulty level" that changes the random wait range.
Further reading #
We've covered everything Picopad has to offer: display, buttons, LED, sound, SD card, battery, external connector and WiFi. If MicroPython hooked you, here's where to head next.
Official documentation
- MicroPython quick reference for RP2040 – an overview of every module available (Pin, SPI, I2C, ADC, PWM, …)
- MicroPython library reference – the full API
- Connecting to the Internet with Pico W – the Raspberry Pi Foundation PDF that complements the WiFi chapter
Pajeníčko modules and more hardware
- Pajenicko/Picopad –
micropythonfolder – sample code includingmodule_ds18b20.py,module_hc-sr04.pyandmodule_photoresistor.py - Pajeníčko store – kits, replacement parts, J2 modules
When you outgrow MicroPython
MicroPython is great for fast prototyping and pixel-art games. Once you hit a wall (a complex emulator, smooth 3D, real-time audio), step up to the C SDK:
- Pajeníčko Picopad SDK – the original C SDK plus bootloader
- picopad-template – a CMAKE template for CLion
- Alternative Picopad SDK – fork built on the Pico SDK
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!