Built-in project · Raspberry Pi Pico

MicroPython RFID on a Pico: read a card’s UID, RC522

MicroPython on a Raspberry Pi Pico: an RC522 reader on SoftSPI (GP2 to GP5) prints a MIFARE card’s UID and reads block 8 with the factory key. The whole build, a Raspberry Pi Pico and 1 more part, 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 20 minutes to build and run. Runs in your browser. Free, and no account needed.

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

The circuit runs here once the page has loaded.

What you'll learn

  • Using SoftSPI on any pins of a Pico
  • Waking and selecting a MIFARE card
  • What a card's UID is
  • Logging in to a block with a key
  • Splitting a driver into its own mfrc522.py file
  • Difficulty: Advanced
  • Time: About 20 minutes
  • Board: Raspberry Pi Pico
  • Parts: 3

How it works

The RC522 reader is wired to the Pico with software SPI: SCK on GP2, MOSI on GP3, MISO on GP4 and chip select (the SDA pin) on GP5. GP6 goes to RST and is held high so the reader runs. It is powered from 3.3 V.

The script asks the reader every tenth of a second whether a card is in range. When one answers, anticoll returns its UID, and select picks that card out. The four UID bytes print as hex, like 'card UID: 1A 2B 3C 4D'.

It then logs in to block 8 with the factory key KEY, which is six 0xFF bytes, and prints the 16 bytes stored there. After that it waits one second before looking for a card again.

Parts list

3 parts, plus the jumper wires. They retail for about $7; 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
RFID reader, MFRC522 pin SCKRaspberry Pi Pico pin GP2
RFID reader, MFRC522 pin MOSIRaspberry Pi Pico pin GP3
RFID reader, MFRC522 pin MISORaspberry Pi Pico pin GP4
RFID reader, MFRC522 pin SDARaspberry Pi Pico pin GP5
RFID reader, MFRC522 pin RSTRaspberry Pi Pico pin GP6
RFID reader, MFRC522 pin GNDGround
RFID reader, MFRC522 pin 3.3V3.3 V

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
# An RC522 RFID reader on SoftSPI (SCK GP2, MOSI GP3, MISO GP4, CS GP5).
# When a card is laid on it: its UID, then block 8 read with the factory
# key. mfrc522.py, in the next tab, speaks to the reader.
from machine import Pin, SoftSPI
import time
from mfrc522 import MFRC522, OK

Pin(6, Pin.OUT, value=1)                 # RST high: the reader runs
spi = SoftSPI(baudrate=100000, polarity=0, phase=0, sck=Pin(2), mosi=Pin(3), miso=Pin(4))
reader = MFRC522(spi, cs=5)
KEY = [0xFF] * 6
print("Lay a card on the reader")

while True:
    if reader.request() == OK:
        status, uid = reader.anticoll()
        if status == OK and reader.select(uid) == OK:
            print("card UID:", " ".join("%02X" % b for b in uid[:4]))
            if reader.auth(8, KEY, uid) == OK:
                print("block 8:", reader.read_block(8))
            reader.stop_crypto()
            time.sleep(1)
    time.sleep_ms(100)
mfrc522.py · Raspberry Pi Pico
# mfrc522.py: an MFRC522 RFID reader on SPI, and MIFARE Classic cards.
# The reader is a set of registers: a byte with the register's address in
# bits 6 to 1 (bit 7 set to read), then the data. Talking to a card is
# filling the reader's FIFO, starting a command and reading the answer back.
from machine import Pin
import time

OK, NOTAG, ERR = 0, 1, 2

class MFRC522:
    def __init__(self, spi, cs):
        self.spi = spi
        self.cs = Pin(cs, Pin.OUT, value=1)
        self.write_reg(0x01, 0x0F)          # soft reset
        time.sleep_ms(50)
        self.write_reg(0x2A, 0x8D)          # timer: give up on a silent card
        self.write_reg(0x2B, 0x3E)
        self.write_reg(0x2D, 30)
        self.write_reg(0x2C, 0)
        self.write_reg(0x15, 0x40)          # 100 % ASK modulation
        self.write_reg(0x11, 0x3D)          # CRC preset 0x6363
        self.set_bits(0x14, 0x03)           # antenna on

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

    def read_reg(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 set_bits(self, reg, mask):
        self.write_reg(reg, self.read_reg(reg) | mask)

    def clear_bits(self, reg, mask):
        self.write_reg(reg, self.read_reg(reg) & ~mask)

    def command(self, cmd, data):
        """Run Transceive (0x0C) or MFAuthent (0x0E) on data; (status, bytes back, bits back)."""
        wait = 0x30 if cmd == 0x0C else 0x10
        self.write_reg(0x04, 0x7F)          # clear every interrupt flag
        self.set_bits(0x0A, 0x80)           # empty the FIFO
        self.write_reg(0x01, 0x00)          # idle
        for b in data:
            self.write_reg(0x09, b)
        self.write_reg(0x01, cmd)
        if cmd == 0x0C:
            self.set_bits(0x0D, 0x80)       # start sending
        for _ in range(2000):
            irq = self.read_reg(0x04)
            if irq & wait or irq & 0x01:
                break
        self.clear_bits(0x0D, 0x80)
        if not irq & wait:
            return NOTAG, [], 0
        if self.read_reg(0x06) & 0x1B:
            return ERR, [], 0
        back = []
        bits = 0
        if cmd == 0x0C:
            n = self.read_reg(0x0A)
            last = self.read_reg(0x0C) & 0x07
            bits = (n - 1) * 8 + last if last else n * 8
            back = [self.read_reg(0x09) for _ in range(n)]
        return OK, back, bits

    def crc(self, data):
        self.clear_bits(0x05, 0x04)
        self.set_bits(0x0A, 0x80)
        for b in data:
            self.write_reg(0x09, b)
        self.write_reg(0x01, 0x03)          # CalcCRC
        for _ in range(255):
            if self.read_reg(0x05) & 0x04:
                break
        return [self.read_reg(0x22), self.read_reg(0x21)]

    def request(self):
        """Wake a card in the field: OK when one answers."""
        self.write_reg(0x0D, 0x07)          # REQA is a 7-bit frame
        status, back, bits = self.command(0x0C, [0x26])
        return OK if status == OK and bits == 0x10 else NOTAG

    def anticoll(self):
        """The card's four UID bytes, checked against their XOR byte."""
        self.write_reg(0x0D, 0x00)
        status, back, _ = self.command(0x0C, [0x93, 0x20])
        if status != OK or len(back) != 5 or back[0] ^ back[1] ^ back[2] ^ back[3] != back[4]:
            return ERR, []
        return OK, back

    def select(self, uid5):
        frame = [0x93, 0x70] + uid5
        status, back, bits = self.command(0x0C, frame + self.crc(frame))
        return OK if status == OK and bits == 0x18 else ERR

    def auth(self, block, key, uid5):
        status, _, _ = self.command(0x0E, [0x60, block] + key + uid5[:4])
        if status != OK or not self.read_reg(0x08) & 0x08:
            return ERR
        return OK

    def stop_crypto(self):
        self.clear_bits(0x08, 0x08)

    def read_block(self, block):
        frame = [0x30, block]
        status, back, _ = self.command(0x0C, frame + self.crc(frame))
        return back[:16] if status == OK and len(back) >= 16 else None

    def write_block(self, block, data):
        frame = [0xA0, block]
        status, back, bits = self.command(0x0C, frame + self.crc(frame))
        if status != OK or bits != 4 or back[0] & 0x0F != 0x0A:
            return ERR
        data = list(data[:16]) + [0] * (16 - len(data))
        status, back, bits = self.command(0x0C, data + self.crc(data))
        if status != OK or bits != 4 or back[0] & 0x0F != 0x0A:
            return ERR
        return OK

Troubleshooting

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

  • No card is ever detected. This project
    Check SCK on GP2, MOSI on GP3, MISO on GP4 and SDA on GP5. MOSI and MISO are easy to swap.
  • The UID prints but block 8 does not. This project
    The login with the factory key failed. The card may use a different key, so the script skips the read.
  • The reader seems dead. This project
    Check that RST goes to GP6 and the reader has 3.3 V and ground.
  • 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 Pico does not show up as a drive, or the program will not load. Raspberry Pi Pico
    Hold the BOOTSEL button while you plug in USB. A drive named RPI-RP2 appears. Then copy the UF2 file onto it.
  • A 5 V signal or sensor is wired to a GPIO pin. Raspberry Pi Pico
    Pico GPIO pins are 3.3 V only and are not 5 V tolerant. Use a level shifter or a voltage divider for any 5 V output going into the board.

Change it and keep it

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