Built-in project · Raspberry Pi Pico

MicroPython RFID on a Pico: write a card and read it back

MicroPython on a Raspberry Pi Pico: text typed in the Serial Monitor is written to block 8 of a MIFARE card on an RC522 reader and read back. 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

  • Writing a 16 byte block on a MIFARE card
  • Reading a block back to check a write
  • Waiting in a loop until a card appears
  • Taking typed input with input
  • Using SoftSPI on any pins of a Pico
  • 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. The reader runs on 3.3 V.

The script asks for text in the Serial Monitor, up to 16 characters. Then it prints 'Lay a card on the reader' and keeps looking until a card answers.

It logs in to block 8 with the factory key, writes your text there, reads the block back and prints both. If they match, the card really stored it. Then it asks for new text.

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
# Write to a card and read it back: type up to 16 characters in the Serial
# Monitor, lay a card on the reader, and the text goes into block 8; it is
# then read back to check it.
from machine import Pin, SoftSPI
import time
from mfrc522 import MFRC522, OK

Pin(6, Pin.OUT, value=1)
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

def card():
    """The UID of a card on the reader, selected, or None."""
    if reader.request() != OK:
        return None
    status, uid = reader.anticoll()
    if status != OK or reader.select(uid) != OK:
        return None
    return uid

while True:
    text = input("Text for block 8: ")[:16]
    print("Lay a card on the reader")
    uid = None
    while uid is None:
        uid = card()
        time.sleep_ms(100)
    if reader.auth(8, KEY, uid) == OK and reader.write_block(8, text.encode()) == OK:
        back = bytes(reader.read_block(8)).rstrip(b"\x00").decode()
        print("wrote", repr(text), "read back", repr(back))
    else:
        print("could not write the card")
    reader.stop_crypto()
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

Code walkthrough

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

card main.py

Looks for a card and returns its UID, or None. It sends a request, reads the UID with anticoll, and selects the card. If any step fails, it returns None, so the main loop just tries again a tenth of a second later.

def card():
    """The UID of a card on the reader, selected, or None."""
    if reader.request() != OK:
        return None
    status, uid = reader.anticoll()
    if status != OK or reader.select(uid) != OK:
        return None
    return uid

Troubleshooting

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

  • It prints could not write the card. This project
    Login or the write failed. Keep the card steady on the reader, and check the SPI wires, especially MOSI and MISO.
  • Only part of your text is stored. This project
    A block holds 16 bytes, and the script cuts your text to 16 characters.
  • The script waits forever after Lay a card on the reader. This project
    No card is being detected. Check the wiring and that RST is on GP6.
  • 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.