Skip to main content
100%

Pico:ed Comprehensive Demo (CircuitPython)

✓ Published0🌍 Public
IIoTeacher
Last edited Jul 9, 2026
Created on Jul 8, 2026

This demo turns the ELECFREAKS Pico:ed RP2040 board into an interactive hardware showcase, using the 17×7 IS31FL3731 LED matrix as a live data display. Button A cycles through menu items, Button B activates a feature, and pressing both exits early. A scanner auto-detects external I2C sensors (MPU6050, QMC5883L, HMC5883L) and reports results on the matrix; the on-board RP2040 die temperature sensor drives a real-time readout. Code is a single `code.py` that runs directly from the CIRCUITPY drive, exercising the board's buttons, buzzer, and temperature sensor while adapting to whatever I2C breakout is connected.

AI-generated description

Pico:ed Comprehensive Demo (CircuitPython)

<img width="324" height="298" alt="image" src="https://gist.github.com/user-attachments/assets/2d838670-6641-458b-833b-ffde9815fbe5" />

A full feature-tour demo for the ELECFREAKS Pico:ed — an RP2040-based board built in the BBC micro:bit form factor and edge connector, running CircuitPython. It mirrors the spirit of the micro:bit's built-in out-of-box demo: a button-driven menu that walks through every onboard capability the board actually has, plus an auto-detecting external sensor scanner.

Single file: code.py — drop it on the CIRCUITPY drive and it runs immediately (CircuitPython auto-reloads on save).


Table of contents


Hardware background

The Pico:ed (ELECFREAKS) is a dual-core Arm Cortex-M0+ (RP2040, 264KB RAM) development board shaped like a BBC micro:bit, with the same edge-connector footprint, so most micro:bit accessories/add-ons fit mechanically. It ships in two hardware revisions:

Revision board.board_id Matrix I2C bus Buzzer pin
V1 elecfreaks_picoed shares the external bus (board.SCL/SDA) board.BUZZER_GP0
V2 elecfreaks_picoed_v2 (or similar) separate internal bus (board.I2C0_SCL/I2C0_SDA) board.BUZZER

The picoed CircuitPython module (frozen into the official firmware) auto-detects which revision it's running on and wires up the right pins — see __init__.py. This demo works on either revision unmodified.

Onboard hardware:

  • 2× user buttons (A, B)
  • 17×7 LED dot-matrix display, driven by an IS31FL3731 chip at I2C address 0x74
  • 1× passive buzzer
  • 1× status LED (behind the "i" in "Pico")
  • RP2040's own internal die temperature sensor (ADC channel 4)
  • BOOTSEL and RESET buttons on the back (not user-programmable buttons in the normal sense — BOOTSEL is a firmware-mode strap)
  • A 19-pin micro:bit-compatible edge connector: 4× analog in, 3× GPIO crocodile-clip pads, 2× I2C, 2× SPI, 2× UART

Pico:ed vs. micro:bit v2 — what's actually onboard

This matters because the demo's menu structure follows directly from it:

Feature micro:bit v2 Pico:ed
Buttons A/B ✅ ✅
Capacitive touch logo button ✅ ❌ not present
LED display 5×5 17×7 (bigger!)
Buzzer / speaker ✅ ✅ (buzzer only)
Accelerometer ✅ (LSM303AGR) ❌
Compass/magnetometer ✅ (LSM303AGR) ❌
Microphone ✅ ❌
Temperature sensor ✅ (via CPU) ✅ (RP2040 die sensor)
Radio / BLE ✅ ❌ (no wireless radio on RP2040)
Edge connector micro:bit micro:bit-compatible

Confirmed by directly inspecting the live board (dir(board) over a serial REPL), not just documentation: the full pin list is A0-A3, BUTTON_A, BUTTON_B, BUZZER, BUZZER_GP0, BUZZER_GP3, I2C0_SCL, I2C0_SDA, LED, P0-P20, SCL, SDA, SMPS_MODE, VBUS_SENSE. There is no touch-capable pin — so there is no third "logo" button equivalent to micro:bit v2's capacitive pad. The buzzer alone also means there's no dedicated onboard microphone/sound-level sensor.

Because Pico:ed has no built-in IMU/compass, SENSORS in this demo is an auto-detecting I2C scanner rather than a fixed driver — it looks for common breakout chips on the external bus and reports what it finds. TEMP instead uses the microcontroller's own internal sensor, which needs no external hardware at all.

Requirements

  • ELECFREAKS Pico:ed (V1 or V2)
  • CircuitPython firmware installed (this was built/tested against CircuitPython 9.0.3, board_id = elecfreaks_picoed) with the picoed frozen module available (it ships in the official Pico:ed CircuitPython build — no extra libraries to copy into /lib)
  • A USB cable, and a way to copy a file to the CIRCUITPY mass-storage drive (Finder/Explorer, cp, mpremote, Thonny, etc.)
  • (optional) an I2C sensor breakout for the SENSORS demo — see Wiring an external sensor

Installation

  1. Plug in the Pico:ed via USB. It should mount as a drive named CIRCUITPY.
  2. Copy code.py from this gist to the root of that drive, replacing the existing code.py (back up the original first if you want to keep whatever sample was on it: cp CIRCUITPY/code.py CIRCUITPY/code.py.bak).
  3. CircuitPython auto-reloads on save — the demo starts within a second or two: you'll see a short splash (PICO:ED scroll + heart icon + two beeps) and then the menu shows BUTTONS.
  4. (optional) Open a serial terminal (115200 baud) on the board's USB serial port to see verbose print() diagnostics for every demo (button counts, raw sensor readings, I2C scan results, etc.).
    • macOS/Linux: screen /dev/tty.usbmodem* 115200 or mpremote connect <port>
    • Windows: PuTTY / Tera Term on the assigned COMx port

Controls

Action Effect
Press A Advance to the next item in the menu
Press B Run the highlighted demo
Hold A + B together while a demo is running Exit that demo early, back to the menu
(most demos also auto-exit after a timeout, see table below)

Menu / demo reference

BUTTONS

Live press counter. Each A press flashes a check-mark icon and increments a counter; each B press flashes a smiley and increments its own counter. Exits when A+B are held together, or after 30s. On exit, the matrix scrolls a summary like A=3 B=5.

Demonstrates: button_a/button_b .is_pressed() and .was_pressed() edge-detection, on-demand icon rendering.

MATRIX

Three-part tour of the 17×7 display:

  1. Icon gallery — cycles through 9 of the library's built-in bitmap Images (HEART, HAPPY, SAD, YES, NO, TARGET, SQUARE, TRIANGLE, EXCITED), ~0.5s each.
  2. Bouncing pixel — a single lit pixel bounces around all 4 edges of the matrix for ~4 seconds using raw display.pixel(x, y, brightness) / display.fill(0) calls (per-frame redraw, no double-buffering needed — the IS31FL3731 driver updates live).
  3. Brightness fade — a center pixel ramps 0→255→0 to show PWM brightness control.

Demonstrates: Image built-ins, display.show(), low-level display.pixel()/display.fill(), brightness range (0–255).

SOUND

Plays one of the library's built-in melodies (music.BADDY) via music.play(), then does a rising frequency sweep from 200 Hz to ~2000 Hz using single-tone music.pitch() calls, then plays a second melody (music.POWER_UP). Matrix shows matching mood icons.

Demonstrates: music.play() with a built-in melody, music.pitch() for arbitrary single frequencies, music.stop().

TEMP

Reads the RP2040's own internal die temperature sensor (microcontroller.cpu.temperature) every 0.3s for 20 seconds, printing °C/°F over serial and scrolling °C on the matrix.

⚠️ This is die/chip temperature, not ambient/room temperature — the silicon self-heats a few degrees above the surrounding air under normal load. For a genuine ambient-temperature reading, use an external sensor via SENSORS (e.g. an MPU6050's temperature output, or a dedicated ambient sensor like a BME280/DS18B20).

Demonstrates: that a "temperature demo" doesn't require any external hardware at all — every RP2040 board has this for free.

SENSORS

Scans the external I2C bus (board.SCL / board.SDA, exposed on the edge connector) and auto-detects/reads any of the following if present:

Chip I2C address Reads
MPU6050 (or compatible, e.g. MPU6500) 0x68 or 0x69 3-axis accelerometer (g), 3-axis gyroscope (°/s), temperature (°C)
QMC5883L 0x0D Heading (°), raw X/Y/Z magnetometer counts
HMC5883L 0x1E Detected and reported, but the register map isn't wired up (different from QMC5883L's — see Extending it)
anything else any other address Reported as "unrecognized device" with its address, so you can add a driver

The onboard matrix driver (0x74) is automatically excluded from the "sensor found" logic since it's always present and isn't a sensor.

If nothing is found, the matrix shows a sad-face icon and scrolls NO SENSOR, and the serial log explains what to connect.

Demonstrates: i2c.scan(), i2c.try_lock()/.unlock(), i2c.writeto() / i2c.writeto_then_readfrom(), minimal from-scratch register-level sensor drivers (no external dependency needed).

Wiring an external sensor

Pico:ed's edge connector exposes I2C on SCL = P19, SDA = P20 (the same pins the micro:bit ecosystem uses), matching board.SCL / board.SDA in CircuitPython. Any 3.3V-tolerant I2C sensor breakout works:

Sensor VCC  -> Pico:ed 3V
Sensor GND  -> Pico:ed GND
Sensor SDA  -> Pico:ed P20 (SDA)
Sensor SCL  -> Pico:ed P19 (SCL)

Cheap, common breakouts that work out of the box with this demo: GY-521 (MPU6050) for accel/gyro/temp, or GY-271 (QMC5883L) for a compass. Both are widely available and inexpensive.

On Pico:ed V1, the display chip shares this same bus (that's why 0x74 always shows up in the scan) — this is normal I2C bus-sharing and doesn't interfere with sensor readings.

API reference used (picoed module)

The picoed CircuitPython module is frozen into the official Pico:ed firmware build (elecfreaks/circuitpython_picoed, MIT licensed). Everything the demo uses:

from picoed import *
from picoed import i2c    # external I2C bus, board.SCL/board.SDA

button_a.is_pressed()     # -> bool, live state
button_a.was_pressed()    # -> bool, true once per press (edge-triggered)
button_b.is_pressed() / was_pressed()   # same, for button B

display.clear()                       # blank the matrix
display.fill(0)                       # same, lower-level
display.pixel(x, y, brightness)       # x:0-16, y:0-6, brightness:0-255
display.scroll(text_or_number, brightness=30)   # blocking scroll
display.show(image_or_value, brightness=30)     # show a static Image
display.width, display.height         # 17, 7

Image.HEART, Image.HAPPY, Image.SAD, Image.YES, Image.NO, ...  # built-ins
                                       # (26 total; see picoed/display.py)

music.play(melody)          # blocking playback of a note-list
music.play_async(melody)    # non-blocking
music.pitch(freq_hz, duration_ms)     # single tone
music.pitch_async(freq_hz, duration_ms)
music.stop()
music.set_tempo(ticks=4, bpm=120) / get_tempo()
music.BADDY, music.POWER_UP, music.NYAN, ...   # 21 built-in melodies

led.on() / led.off() / led.toggle()   # onboard status LED

i2c.try_lock() / i2c.unlock()         # required around every i2c.* call
i2c.scan()                            # -> list[int] of found addresses
i2c.writeto(addr, bytes)
i2c.writeto_then_readfrom(addr, out_bytes, in_buffer)

Plus standard CircuitPython:

import microcontroller
microcontroller.cpu.temperature   # -> float, RP2040 die temp in °C

Code architecture

code.py
├── MPU6050 class         minimal register-level driver (wake, read 14B burst)
├── QMC5883 class         minimal register-level driver (config + read 6B)
├── both_pressed() / wait_for_release()   chord-exit helpers
├── demo_buttons()
├── demo_matrix()
├── demo_sound()
├── demo_sensors()
├── demo_temperature()
├── DEMOS = [(label, function), ...]      menu table, single source of truth
├── splash()               boot animation
└── main()                 menu loop: A = next, B = run, then re-shows menu

Each demo_*() function is self-contained, checks both_pressed() periodically so the A+B chord can interrupt it, and returns control to main()'s menu loop when done. Adding a new demo is a matter of writing one more demo_*() function and adding a ("LABEL", fn) tuple to DEMOS.

Extending it

Ideas, in roughly increasing order of effort:

  • HMC5883L compass math — the chip is detected but not decoded. Its register map differs from QMC5883L: control registers are 0x00/0x01/0x02 and the data burst-read order is X, Z, Y (not X, Y, Z), each as big-endian int16. Add a second small driver class alongside QMC5883.
  • BME280 / BMP280 for real ambient temperature + humidity/pressure (I2C 0x76/0x77) — more registers and a calibration-constant readout, but well documented.
  • NeoPixel support — neopixel is already a frozen module on this firmware build (per the board's module list) even though Pico:ed itself has no onboard NeoPixel; useful if you wire an external strip to one of the edge-connector GPIO pads.
  • Async melody + animation together — swap music.play() for music.play_async() and drive a matrix animation in the same loop, since the buzzer no longer blocks.
  • Persist high scores — write button-press counts to a file on CIRCUITPY (mind the flash wear / read-only-filesystem-from-USB caveat — see CircuitPython's storage.remount() docs if you want to write from code.py while also mounted as USB drive).

Troubleshooting

  • Board shows as CIRCUITPY but nothing happens after copying code.py — check boot_out.txt for a traceback, or open a serial console; CircuitPython prints tracebacks there. Every print() in this demo also confirms progress that way.
  • No module named 'picoed' — your firmware build doesn't include the frozen module. Reflash the official ELECFREAKS Pico:ed CircuitPython UF2 from https://circuitpython.org/board/elecfreaks_picoed/ (put the board in BOOTSEL mode: hold BOOTSEL while plugging in USB, it mounts as RPI-RP2, drag the .uf2 on).
  • SENSORS always shows only 0x74 — that's just the onboard matrix driver; it means no external sensor is wired up yet (or it's wired to the wrong pins/orientation). Double check P19/P20 and 3.3V power.
  • Matrix looks "stuck" mid-scroll — display.scroll() is a blocking call; it returns only once the full scroll finishes. This is expected and is why button chord-checks happen between calls, not inside them.
  • Buttons feel unresponsive during SOUND — music.play() also blocks until the melody finishes; the chord-exit check only runs between play calls in that demo, so exiting mid-melody isn't instant.

Verification notes

Every primitive in this README was smoke-tested against a real, connected Pico:ed board over a serial REPL before being written into the demo — not assumed from documentation alone:

  • dir(board) was dumped live to get the authoritative pin list (confirmed no touch/logo pin exists).
  • dir(music), dir(display), dir(button_a), dir(led) were dumped live to confirm the exact method/attribute names and the full list of 21 built-in melodies.
  • display.scroll(), display.pixel(), display.fill(), i2c.scan(), button_a.is_pressed(), and music.pitch() were each executed live and their real return values checked.
  • microcontroller.cpu.temperature was read live (34.16, 34.2°C across two runs).
  • board.board_id (elecfreaks_picoed) and an internal-I2C init probe were used to confirm this specific unit is a V1 board (single shared I2C bus — hence 0x74 always appears in scans).
  • The final code.py was deployed to the board and its boot sequence captured over serial with zero traceback; menu navigation (A) and demo launch (B) were confirmed live via serial log output during an interactive session (menu -> MATRIX, running demo: MATRIX, etc.).

Credits / references


Written and hardware-verified for the ELECFREAKS Pico:ed V1, CircuitPython 9.0.3.

Similar vizzes