Basics in MicroPython

From your first blinking LED to your own mini game, step by step. No prior Python experience needed – we'll explain everything as we go.

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, COM3 on 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.

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

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=2Inputs with pull-up; pressed = LOW
Buttons A B X YA=7, B=6, X=9, Y=8Same as the D-pad; pressed = LOW
User LED22Active-low (lights up when value=0)
Buzzer / speaker15Driven via PWM
microSD card (SPI 1)SCK=10, MOSI=11, MISO=12, CS=13Separate 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:

ColorRGB565
Black0x0000
White0xFFFF
Red0xF800
Green0x07E0
Blue0x001F
Yellow0xFFE0
Cyan0x07FF

6) Things to try

  • display.fill_rect(x, y, w, h, color) – filled rectangle
  • display.pixel(x, y, color) – a single pixel
  • display.line(x1, y1, x2, y2, color) – a line
  • Try the smaller fonts_vga2_8x8 font – more text fits

Buttons #

Picopad has 8 game buttons: 4 directional (the D-pad) and 4 face buttons (A, B, X, Y). They all work the same way – each is connected to a GPIO pin with the internal pull-up resistor enabled. That means:

  • While a button is not pressed, the pin reads 1 (HIGH).
  • When you press it, the pin shorts to ground and reads 0 (LOW).

So in code we ask "is the pin value 0?", and that tells us the button is currently held down.

Defining the buttons

from machine import Pin

buttons = {
    "LEFT":  Pin(3, Pin.IN, Pin.PULL_UP),
    "RIGHT": Pin(2, Pin.IN, Pin.PULL_UP),
    "UP":    Pin(4, Pin.IN, Pin.PULL_UP),
    "DOWN":  Pin(5, Pin.IN, Pin.PULL_UP),
    "A":     Pin(7, Pin.IN, Pin.PULL_UP),
    "B":     Pin(6, Pin.IN, Pin.PULL_UP),
    "X":     Pin(9, Pin.IN, Pin.PULL_UP),
    "Y":     Pin(8, Pin.IN, Pin.PULL_UP),
}

Show the pressed button on the display

Combining what we know about the display: every iteration we check all the buttons and, if any is pressed, show its name on the screen.

from machine import Pin, SPI
from fonts import fonts_vga1_16x32
from time import sleep
import st7789

# Display init (shortened – see the Display chapter)
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)

buttons = {
    "LEFT":  Pin(3, Pin.IN, Pin.PULL_UP),
    "RIGHT": Pin(2, Pin.IN, Pin.PULL_UP),
    "UP":    Pin(4, Pin.IN, Pin.PULL_UP),
    "DOWN":  Pin(5, Pin.IN, Pin.PULL_UP),
    "A":     Pin(7, Pin.IN, Pin.PULL_UP),
    "B":     Pin(6, Pin.IN, Pin.PULL_UP),
    "X":     Pin(9, Pin.IN, Pin.PULL_UP),
    "Y":     Pin(8, Pin.IN, Pin.PULL_UP),
}

display.fill(0x0000)
display.text(fonts_vga1_16x32, "Press a button",
             50, 100, 0x07FF, 0x0000)

last = ""
while True:
    pressed = ""
    for name, btn in buttons.items():
        if btn.value() == 0:
            pressed = name
            break
    if pressed != last:
        display.fill(0x0000)
        if pressed:
            display.text(fonts_vga1_16x32, pressed,
                         100, 100, 0xFFFF, 0x0000)
        else:
            display.text(fonts_vga1_16x32, "Press a button",
                         50, 100, 0x07FF, 0x0000)
        last = pressed
    sleep(0.05)

The last trick keeps us from redrawing the screen every iteration – we only redraw when the state changes. It also makes the on-screen feedback feel snappy.

What about debouncing? Mechanical buttons "bounce" – they make multiple rapid contacts on press. For a game loop running at 50 ms (like above) it doesn't matter. If you start counting presses, you'll hit it – the simplest fix is to add a short delay after detecting a press, or test twice in a row.

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

NoteFrequency (Hz)
C4 (middle C)262
D4294
E4330
F4349
G4392
A4 (concert A)440
B4494
C5523

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:

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:

  1. The screen shows instructions and waits for A.
  2. After the press, it waits a random 1–4 seconds.
  3. Suddenly the screen flashes green, the buzzer beeps – "GO!"
  4. The player slams A as fast as they can.
  5. The game prints how many milliseconds passed between GO and the press.
  6. 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

Pajeníčko modules and more hardware

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:

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!