Built-in project · Raspberry Pi Pico W

Pico W MFRC522 reads and writes RFID cards

MicroPython on a Pico W: an MFRC522 on SPI0 reads a card’s UID and the text stored on it, and writes text typed in the Serial Monitor to a card. The whole build, a Raspberry Pi Pico W 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 $9. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Wiring an SPI device to a Pico W
  • Reading a card's UID
  • Reading and writing a 16 byte block on a card
  • Using a driver module in its own file
  • Reading typed input without stopping the loop
  • Difficulty: Advanced
  • Time: About 20 minutes
  • Board: Raspberry Pi Pico W
  • Parts: 3

How it works

The MFRC522 reader talks to the Pico W over SPI0: SCK on GP18, MOSI on GP19, MISO on GP16. Its chip select (the SDA pin) is on GP17 and reset is on GP9. It runs on 3.3 V.

Every tenth of a second the script asks the reader if a card is present. If one is, it prints the card's UID as hex bytes joined by dashes, then reads block 8 and prints the text stored there.

To store text, type a line in the Serial Monitor and press Enter. The script holds it as pending and writes it to block 8 of the next card you lay on the reader. It then waits for you to take the card away.

Parts list

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

Kits that can build it

These 2 starter kits have 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 RSTRaspberry Pi Pico W pin GP9
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

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
# Radio frequency identification: an MFRC522 on SPI0 (SCK GP18, MOSI GP19,
# MISO GP16), chip select on GP17 and reset on GP9. Lay a card on the
# reader and its UID and the text in block 8 are printed. Type a line in
# the Serial Monitor first and it is written to the next card instead.
from machine import Pin, SPI
from mfrc522 import MFRC522
import select
import sys
import time

spi = SPI(0, baudrate=1000000, sck=Pin(18), mosi=Pin(19), miso=Pin(16))
reader = MFRC522(spi, Pin(17), Pin(9))
BLOCK = 8
pending = None
typed = ""
poll = select.poll()
poll.register(sys.stdin, select.POLLIN)
print("Lay a card on the reader, or type text to store")

while True:
    while poll.poll(0):
        ch = sys.stdin.read(1)
        if ch in "\r\n":
            if typed:
                pending = typed
                print("Now lay a card on the reader to store:", pending)
            typed = ""
        else:
            typed += ch
    uid = reader.uid()
    if uid:
        name = "-".join("%02X" % b for b in uid)
        if pending is not None:
            ok = reader.write(BLOCK, uid, pending.encode())
            print("card", name, "stored" if ok else "could not be written")
            pending = None
        else:
            data = reader.read(BLOCK, uid)
            text = data.rstrip(b"\x00").decode() if data else "?"
            print("card", name, "says:", text)
        reader.done()
        while reader.uid():            # wait for the card to be taken away
            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

Troubleshooting

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

  • Nothing prints when you lay a card on the reader. This project
    Check the SPI wires: SCK to GP18, MOSI to GP19, MISO to GP16, and SDA to GP17. Make sure the reader has 3.3 V and ground.
  • The card says ? instead of text. This project
    Nothing has been stored in block 8 yet, or the block could not be read. Type a line and write it to the card first.
  • Your typed text is not stored. This project
    Press Enter after typing. The message Now lay a card on the reader appears, then lay a card down.
  • 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.
  • Pins are powered from 5 V, or a 5 V signal goes into a GPIO. The RP2040 pins are 3.3 V only and are not 5 V tolerant. Raspberry Pi Pico W
    Power the board from VBUS or VSYS at 5 V, but keep GPIO signals at 3.3 V. Use a level shifter or a divider for 5 V parts.
  • WiFi code fails to connect, or the on-board LED code does nothing. The LED on the Pico W is not on GP25 like on the plain Pico. Raspberry Pi Pico W
    Use the board's LED_BUILTIN name so the library drives it through the WiFi chip. Join a 2.4 GHz network only, with the right SSID and password, and wait for the link before sending.

Change it and keep it

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