Pico:ed Comprehensive Demo (CircuitPython)
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 descriptionPico: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
- Pico:ed vs. micro:bit v2 — what's actually onboard
- Requirements
- Installation
- Controls
- Menu / demo reference
- Wiring an external sensor
- API reference used (
picoedmodule) - Code architecture
- Extending it
- Troubleshooting
- Verification notes
- Credits / references
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 thepicoedfrozen 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
CIRCUITPYmass-storage drive (Finder/Explorer,cp,mpremote, Thonny, etc.) - (optional) an I2C sensor breakout for the
SENSORSdemo — see Wiring an external sensor
Installation
- Plug in the Pico:ed via USB. It should mount as a drive named
CIRCUITPY. - Copy
code.pyfrom this gist to the root of that drive, replacing the existingcode.py(back up the original first if you want to keep whatever sample was on it:cp CIRCUITPY/code.py CIRCUITPY/code.py.bak). - CircuitPython auto-reloads on save — the demo starts within a
second or two: you'll see a short splash (
PICO:EDscroll + heart icon + two beeps) and then the menu showsBUTTONS. - (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* 115200ormpremote connect <port> - Windows: PuTTY / Tera Term on the assigned
COMxport
- macOS/Linux:
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:
- 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. - 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). - 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/0x02and the data burst-read order is X, Z, Y (not X, Y, Z), each as big-endianint16. Add a second small driver class alongsideQMC5883. - BME280 / BMP280 for real ambient temperature + humidity/pressure
(I2C
0x76/0x77) — more registers and a calibration-constant readout, but well documented. - NeoPixel support —
neopixelis 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()formusic.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'sstorage.remount()docs if you want to write from code.py while also mounted as USB drive).
Troubleshooting
- Board shows as
CIRCUITPYbut nothing happens after copyingcode.py— checkboot_out.txtfor a traceback, or open a serial console; CircuitPython prints tracebacks there. Everyprint()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 asRPI-RP2, drag the.uf2on).SENSORSalways shows only0x74— 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 checkP19/P20and 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(), andmusic.pitch()were each executed live and their real return values checked.microcontroller.cpu.temperaturewas read live (34.16,34.2°Cacross 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 — hence0x74always appears in scans).- The final
code.pywas 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
- ELECFREAKS Pico:ed product wiki
elecfreaks/circuitpython_picoed— the frozenpicoedmodule (MIT license)- CircuitPython Pico:ed firmware downloads
- Adafruit IS31FL3731 CircuitPython driver (underlies
picoed.display) - Tom's Hardware: Elecfreaks Pico:ed Review
- The Pi Hut: Getting Started with the Elecfreaks Pico:ed
Written and hardware-verified for the ELECFREAKS Pico:ed V1, CircuitPython 9.0.3.