Built-in project · Raspberry Pi Pico W

Pico W RFID music player with a WS2812 strip

MicroPython on a Pico W: lay an RFID card on an MFRC522 and the notes stored on it play on a passive buzzer while a WS2812 strip flashes a color per note. The whole build, a Raspberry Pi Pico W and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Advanced About 40 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $12.55. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Reading data stored on an RFID card over SPI
  • Turning note letters into frequencies with a lookup table
  • Making tones on a passive buzzer with PWM
  • Driving a buzzer through a transistor
  • Setting colors on a WS2812 strip with NeoPixel
  • Difficulty: Advanced
  • Time: About 40 minutes
  • Board: Raspberry Pi Pico W
  • Parts: 9

How it works

An MFRC522 reader sits on the Pico W's SPI pins (SCK GP18, MOSI GP19, MISO GP16, SDA GP17, RST GP9). The main loop keeps asking it whether a card is present. When one is, the program reads block 8 of the card, which holds the tune as note letters such as C D E F G A B.

Each letter plays on a passive buzzer on GP15. The Pico drives the buzzer through a 1k resistor and an NPN transistor. A strip of 8 addressable LEDs on GP0 flashes a random color for every note.

If the card has nothing stored, the player falls back to the tune CDEC. Then it waits until you take the card away, so one tap plays one tune.

Parts list

9 parts, plus the jumper wires. They retail for about $12.55; on Mokxi they're free.

Kits that can build it

This starter kit has every part in the circuit. Each kit's page lists what else it can build.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Addressable RGB LEDs pin DINRaspberry Pi Pico W pin GP0
RFID reader, MFRC522 pin RSTRaspberry Pi Pico W pin GP9
Resistor, 1k Ω pin 1Raspberry Pi Pico W pin GP15
RFID reader, MFRC522 pin MOSIRaspberry Pi Pico W pin GP19
RFID reader, MFRC522 pin SCKRaspberry Pi Pico W pin GP18
RFID reader, MFRC522 pin SDARaspberry Pi Pico W pin GP17
RFID reader, MFRC522 pin MISORaspberry Pi Pico W pin GP16
RFID reader, MFRC522 pin GNDGround
RFID reader, MFRC522 pin 3.3V3.3 V
Addressable RGB LEDs pin VDD5 V
Addressable RGB LEDs pin VSSGround
Piezo speaker pin 13.3 V
Transistor (NPN or PNP) pin CPiezo speaker pin 2
Resistor, 1k Ω pin 2Transistor (NPN or PNP) pin B
Transistor (NPN or PNP) pin EGround

The code

The MicroPython code exactly as the editor opens it. Change a line there and press Run: the board runs it on the official MicroPython firmware, with nothing to compile.

main.py · Raspberry Pi Pico W
# RFID music player: a card carries a tune as note letters (C D E F G A B),
# stored with lesson 6.5's program. Lay it on the reader: each note plays
# on the buzzer (GP15) and the strip (GP0) flashes a color for it.
from machine import Pin, PWM, SPI
from mfrc522 import MFRC522
import neopixel
import random
import time

NOTES = {"C": 262, "D": 294, "E": 330, "F": 349, "G": 392, "A": 440, "B": 494}
spi = SPI(0, baudrate=1000000, sck=Pin(18), mosi=Pin(19), miso=Pin(16))
reader = MFRC522(spi, Pin(17), Pin(9))
strip = neopixel.NeoPixel(Pin(0), 8)
buzzer = PWM(Pin(15))
buzzer.duty_u16(0)

def play(tune):
    for letter in tune:
        hz = NOTES.get(letter.upper())
        if not hz:
            continue
        color = (random.randint(0, 64), random.randint(0, 64), random.randint(0, 64))
        strip.fill(color)
        strip.write()
        buzzer.freq(hz)
        buzzer.duty_u16(30000)
        print("note", letter, hz)
        time.sleep_ms(250)
        buzzer.duty_u16(0)
        time.sleep_ms(50)
    strip.fill((0, 0, 0))
    strip.write()

print("Lay a card with a tune on it")
while True:
    uid = reader.uid()
    if uid:
        data = reader.read(8, uid)
        reader.done()
        tune = data.rstrip(b"\x00").decode() if data else ""
        print("tune:", tune or "(nothing stored)")
        play(tune or "CDEC")
        while reader.uid():
            reader.done()
            time.sleep_ms(100)
    time.sleep_ms(100)
mfrc522.py · Raspberry Pi Pico W
# mfrc522.py: the MFRC522 reader on SPI. Enough to find a MIFARE Classic
# card, read its UID, and read or write a 16-byte block behind the
# factory key, which is what a lesson that stores text on a card needs.
from machine import Pin, SPI
import time

OK, NOTAG, ERR = 0, 1, 2

class MFRC522:
    def __init__(self, spi, cs, rst):
        self.spi = spi
        self.cs = cs
        self.rst = rst
        cs.init(Pin.OUT, value=1)
        rst.init(Pin.OUT, value=0)
        time.sleep_ms(10)
        rst.value(1)
        time.sleep_ms(10)
        self._w(0x01, 0x0F)          # soft reset
        time.sleep_ms(10)
        self._w(0x2A, 0x8D)          # timer: about 25 ms before a card that
        self._w(0x2B, 0x3E)          # does not answer is given up on
        self._w(0x2D, 30)
        self._w(0x2C, 0)
        self._w(0x15, 0x40)          # 100 % ASK
        self._w(0x11, 0x3D)          # CRC preset 0x6363
        self._w(0x14, self._r(0x14) | 0x03)   # antenna on

    def _w(self, reg, value):
        self.cs.value(0)
        self.spi.write(bytes([(reg << 1) & 0x7E, value]))
        self.cs.value(1)

    def _r(self, reg):
        self.cs.value(0)
        self.spi.write(bytes([((reg << 1) & 0x7E) | 0x80]))
        value = self.spi.read(1)[0]
        self.cs.value(1)
        return value

    def _card(self, command, send):
        irq_wait = 0x12 if command == 0x0E else 0x30
        self._w(0x02, 0x80 | (0x12 if command == 0x0E else 0x77))
        self._w(0x04, 0x7F)          # clear the interrupt bits
        self._w(0x0A, 0x80)          # flush the FIFO
        self._w(0x01, 0x00)          # idle
        for b in send:
            self._w(0x09, b)
        self._w(0x01, command)
        if command == 0x0C:
            self._w(0x0D, self._r(0x0D) | 0x80)   # start sending
        for _ in range(2000):
            n = self._r(0x04)
            if n & irq_wait or n & 0x01:
                break
        self._w(0x0D, self._r(0x0D) & 0x7F)
        if not (n & irq_wait):
            return NOTAG, [], 0
        if self._r(0x06) & 0x1B:
            return ERR, [], 0
        back, bits = [], 0
        if command == 0x0C:
            count = self._r(0x0A)
            last = self._r(0x0C) & 0x07
            bits = (count - 1) * 8 + last if last else count * 8
            back = [self._r(0x09) for _ in range(count)]
        return OK, back, bits

    def _crc(self, data):
        self._w(0x05, 0x04)
        self._w(0x0A, 0x80)
        for b in data:
            self._w(0x09, b)
        self._w(0x01, 0x03)
        for _ in range(255):
            if self._r(0x05) & 0x04:
                break
        return [self._r(0x22), self._r(0x21)]

    def uid(self):
        """The UID of the card on the reader, selected, or None."""
        self._w(0x0D, 0x07)
        stat, back, bits = self._card(0x0C, [0x26])      # REQA
        if stat != OK or bits != 0x10:
            return None
        self._w(0x0D, 0x00)
        stat, back, _ = self._card(0x0C, [0x93, 0x20])   # anticollision
        if stat != OK or len(back) != 5:
            return None
        frame = [0x93, 0x70] + back
        stat, _, _ = self._card(0x0C, frame + self._crc(frame))   # select
        return back[:4] if stat == OK else None

    def _auth(self, block, uid):
        key = [0xFF] * 6
        stat, _, _ = self._card(0x0E, [0x60, block] + key + uid)
        return stat == OK and self._r(0x08) & 0x08

    def read(self, block, uid):
        if not self._auth(block, uid):
            return None
        frame = [0x30, block]
        stat, back, _ = self._card(0x0C, frame + self._crc(frame))
        return bytes(back[:16]) if stat == OK else None

    def write(self, block, uid, data):
        if not self._auth(block, uid):
            return False
        frame = [0xA0, block]
        stat, back, _ = self._card(0x0C, frame + self._crc(frame))
        if stat != OK:
            return False
        data = list(data[:16]) + [0] * (16 - len(data[:16]))
        stat, back, _ = self._card(0x0C, data + self._crc(data))
        return stat == OK

    def done(self):
        self._w(0x08, self._r(0x08) & 0xF7)   # stop the encryption

Code walkthrough

The key functions of the sketch, and what each one does.

play main.py

This plays a tune, one letter at a time. It looks each letter up in NOTES to get a frequency and skips any letter it does not know. For a known note it fills the strip with a random color, sets the buzzer to that frequency, and sounds it for 250 ms. Then it pauses 50 ms and moves on. When the tune ends, the strip is turned off.

def play(tune):
    for letter in tune:
        hz = NOTES.get(letter.upper())
        if not hz:
            continue
        color = (random.randint(0, 64), random.randint(0, 64), random.randint(0, 64))
        strip.fill(color)
        strip.write()
        buzzer.freq(hz)
        buzzer.duty_u16(30000)
        print("note", letter, hz)
        time.sleep_ms(250)
        buzzer.duty_u16(0)
        time.sleep_ms(50)
    strip.fill((0, 0, 0))
    strip.write()

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • No sound, but the strip flashes This project
    Check the buzzer circuit. GP15 goes through the 1k resistor to the transistor base, the buzzer sits between 3.3 V and the collector, and the emitter goes to ground.
  • The card is never read This project
    Check the four SPI wires and the RST pin. SDA goes to GP17 and RST to GP9. Also make sure the reader is powered from 3.3 V, not 5 V.
  • The strip stays dark This project
    Its data wire must go to GP0, and it needs 5 V on VDD and ground on VSS.
  • The reader is damaged or behaves oddly on 5 V power. RFID reader
    The MFRC522 runs on 3.3 V only. Power it from the 3.3 V pin, never 5 V. Ideally divide 5 V signals from an Uno down for SCK, MOSI and SS.
  • No card is detected. RFID reader
    Check SDA (SS), RST, SCK, MOSI and MISO against your board's SPI pins and the constructor in your code. It reads 13.56 MHz cards only, not 125 kHz key fobs.
  • The first LED flickers, shows wrong colors, or the whole strip glitches. Addressable RGB LEDs
    Join the grounds of the strip and the controller. Add a 300 to 500 ohm resistor in series with the data line. Put a 1000 uF capacitor across the strip's 5 V and GND.
  • The strip works at low brightness but dims, turns yellow, or resets when many LEDs go white. Addressable RGB LEDs
    Each LED can draw about 60 mA at full white. Use a 5 V supply sized for the count, feed power in at several points, and lower the brightness in code.
  • A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
    A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.