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.
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.
- 1 × Raspberry Pi Pico W
- 1 × RFID reader, MFRC522In Escape Box Kit.
- 1 × Power, 3.3 V
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 pin | Connects to |
|---|---|
| RFID reader, MFRC522 pin RST | Raspberry Pi Pico W pin GP9 |
| RFID reader, MFRC522 pin MOSI | Raspberry Pi Pico W pin GP19 |
| RFID reader, MFRC522 pin SCK | Raspberry Pi Pico W pin GP18 |
| RFID reader, MFRC522 pin SDA | Raspberry Pi Pico W pin GP17 |
| RFID reader, MFRC522 pin MISO | Raspberry Pi Pico W pin GP16 |
| RFID reader, MFRC522 pin GND | Ground |
| RFID reader, MFRC522 pin 3.3V | 3.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.
# 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: 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.