How to Interface an RC522 RFID Reader with a Raspberry Pi Pico

How to Interface an RC522 RFID Reader with a Raspberry Pi Pico

Introduction

Applications for Radio Frequency Identification (RFID) technology are becoming more and more common, ranging from inventory management to security systems. The RC522 RFID Reader Module’s cost and simplicity of integration make it a popular component. You may build a reliable RFID-based project when you combine the flexible Raspberry Pi Pico with a microcontroller with strong processing capabilities and an intuitive development environment.

We’ll walk you through connecting the RC522 RFID Reader to a Raspberry Pi Pico in this tutorial. Regardless of your background—a student completing a project for class, or a hobbyist investigating new technologies—this comprehensive guide will assist you in configuring the hardware, installing required libraries, and writing the code to get your RFID reader up and running. By the end of this article, you will have a RFID system ready to be build your own requirements.

Hardware Requirements

Notice: There might be affiliate links to Amazon on this page. which implies that I may receive a tiny commission from the sale. This may be your way of indirectly assisting me. Regards
  • Raspberry Pi Pico (HERE)
  • 0.96″ I2C OLED Display (HERE)
  • Micro USB Cable (HERE)
  • RC522 RFID with Card (HERE)
  • Connecting Wires (HERE)
  • Breadboard (HERE)

Software Requirements

  • Thonny IDE (Download HERE)
  • SSD1306 library code (Download HERE)
  • OLED library code (All files are in the same folder)
  • RC522 library code (All files are in the same folder)

RFID RC522 Module

. This module operates at a 13.56 MHz frequency, intended for reading and writing data to RFID tags. Based on the MFRC522 IC, it offers numerous functions suitable for a wide range of RFID applications. Operating at the globally permitted RFID communication frequency of 13.56 MHz, it supports ISO 14443A/MIFARE protocols and is compatible with various RFID tags and cards. The typical read range is up to 5 cm, depending on ambient conditions and antenna configuration.

RX522 module communicates with microcontrollers using SPI (Serial Peripheral Interface), guaranteeing dependable and quick data flow.
Energy-efficient design qualifies it for battery-powered applications. Anyone interested in learning more about RFID technology can benefit greatly from the RFID RC522 module. This low-cost, user-friendly microcontroller is compatible with popular microcontrollers, making it a great option for novice and seasoned developers alike. We’ll walk through how to connect the RC522 module to a Raspberry Pi Pico in the upcoming parts so you may use this technology for your own projects.

You can use RFID to build many applications, such as access control, attendance systems, and inventory management. In the local market, you will get mainly two types of RFID readers. One with SPI communication and the other one with serial communication.

RFID types

In this tutorial we will use RC522 RFID Module.

The RC522 RFID reader typically comes with an RFID card and a key tag. It stores the tag message or tag ID in its 1KB memory. The RC522 RFID module creates an electromagnetic field at 13.6MHz to communicate with the tag or card, transmitting and receiving data. You can interface this RFID with any microcontroller, including PIC, AVR, Arduino, and Raspberry Pi, via SPI. The SPI type of UART communication protocol uses four wires, and in SPI mode, it can connect with the host at up to 10Mbps.

RC522 RFID module pinout

RFID RC522 module pin out

RC522 Pin description

  • SDA and SCL are the data and clock pins.
  • SS, SCK, MOSI, & MISO are the SPI communication pins.
  • TX & RX are UART pins for serial communication.
  • GND pin is the common ground pin and it is connected with the GND pin of Raspberry Pi Pico.
  • RST is the reset pin of the module.
  • VCC pin is a 3.3v supply pin of the module needed to connect with the 3.3v VCC pin of Raspberry Pi Pico.

Circuit Diagram

RC522 RFID Reader with a Raspberry Pi Pico Circuit Diagram

Circuit Explanation

To build the circuit as shown in the circuit diagram and please follow the below table for Raspberry Pi Pico and RC522 RFID modules.

RC522 ModuleRaspberry Pi Pico
SSGP5
SCKGP2
MOSIGP7
MISOGP4
IRQNC
GNDGND
RSTGP18
VCC3.3v

Now please follow the below table for connection between Raspberry Pi Pico and 0.96″ OLED display modules.

OLED ModuleRaspberry Pi Pico
SCLGP17
SDAGP16
VCC5v
GNDGND

Source Code

To make the process easier to understand, the source code has been divided up into four different parts. Please follow the steps one after another to get the best result.

  • The SSD1306 library code
  • The OLED library code.
  • The RC522 library code.
  • The main code that runs and checks the authentication

SSD1306 library code

Now create a new file in the thony IDE and copy the below code and paste all the code in the new file and then save the file as ssd1306.py file in the Raspberry Pi Pico board memory.

import time
# import framebuf
try:
    import framebuf
except:
    def const(x): return x


# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xa4)
SET_NORM_INV = const(0xa6)
SET_DISP = const(0xae)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xa0)
SET_MUX_RATIO = const(0xa8)
SET_COM_OUT_DIR = const(0xc0)
SET_DISP_OFFSET = const(0xd3)
SET_COM_PIN_CFG = const(0xda)
SET_DISP_CLK_DIV = const(0xd5)
SET_PRECHARGE = const(0xd9)
SET_VCOM_DESEL = const(0xdb)
SET_CHARGE_PUMP = const(0x8d)


class SSD1306:
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        # Note the subclass must initialize self.framebuf to a framebuffer.
        # This is necessary because the underlying data buffer is different
        # between I2C and SPI implementations (I2C needs an extra byte).
        self.poweron()
        self.init_display()

    def init_display(self):
        for cmd in (
            SET_DISP | 0x00,  # off
            # address setting
            SET_MEM_ADDR, 0x00,  # horizontal
            # resolution and layout
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
            SET_MUX_RATIO, self.height - 1,
            SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
            SET_DISP_OFFSET, 0x00,
            SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12,
            # timing and driving scheme
            SET_DISP_CLK_DIV, 0x80,
            SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
            SET_VCOM_DESEL, 0x30,  # 0.83*Vcc
            # display
            SET_CONTRAST, 0xff,  # maximum
            SET_ENTIRE_ON,  # output follows RAM contents
            SET_NORM_INV,  # not inverted
            # charge pump
            SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
                SET_DISP | 0x01):  # on
            self.write_cmd(cmd)
        self.fill(0)
        self.show()

    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def show(self):
        x0 = 0
        x1 = self.width - 1
        if self.width == 64:
            # displays with width of 64 pixels are shifted by 32
            x0 += 32
            x1 += 32
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_framebuf()

    def fill(self, col):
        self.framebuf.fill(col)

    def pixel(self, x, y, col):
        self.framebuf.pixel(x, y, col)

    def scroll(self, dx, dy):
        self.framebuf.scroll(dx, dy)

    def text(self, string, x, y, col=1):
        self.framebuf.text(string, x, y, col)


class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        # Add an extra byte to the data buffer to hold an I2C data/command byte
        # to use hardware-compatible I2C transactions.  A memoryview of the
        # buffer is used to mask this byte from the framebuffer operations
        # (without a major memory hit as memoryview doesn't copy to a separate
        # buffer).
        self.buffer = bytearray(((height // 8) * width) + 1)
        self.buffer[0] = 0x40  # Set first byte of data buffer to Co=0, D/C=1
        self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], width, height)
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_framebuf(self):
        # Blast out the frame buffer using a single I2C transaction to support
        # hardware I2C interfaces.
        self.i2c.writeto(self.addr, self.buffer)

    def poweron(self):
        pass


class SSD1306_SPI(SSD1306):
    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
        self.rate = 10 * 1024 * 1024
        dc.init(dc.OUT, value=0)
        res.init(res.OUT, value=0)
        cs.init(cs.OUT, value=1)
        self.spi = spi
        self.dc = dc
        self.res = res
        self.cs = cs
        self.buffer = bytearray((height // 8) * width)
        self.framebuf = framebuf.FrameBuffer1(self.buffer, width, height)
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs.high()
        self.dc.low()
        self.cs.low()
        self.spi.write(bytearray([cmd]))
        self.cs.high()

    def write_framebuf(self):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs.high()
        self.dc.high()
        self.cs.low()
        self.spi.write(self.buffer)
        self.cs.high()

    def poweron(self):
        self.res.high()
        time.sleep_ms(1)
        self.res.low()
        time.sleep_ms(10)
        self.res.high()

OLED library code

Now oled library can be install from Thonny IDE. Please follow the procedure.

Open Thonny IDE go to Tools then got to Manage Packages and search “oled” for the library find the “micropython-oled” library from the search items by the author: Yeison Cardona and you can find an Install button below. Now just click on install button and install it. It will be installed inside the Raspberry Pi Pico automatically. So, no need to check, It will work.

RC522 library code

Now again create a new file and copy the below code and paste the code in the new file and save the file as a mfrc522.py file in the Raspberry Pi Pico board memory.

from machine import Pin, SPI
from os import uname
 
 
class MFRC522:
 
    DEBUG = False
    OK = 0
    NOTAGERR = 1
    ERR = 2
 
    REQIDL = 0x26
    REQALL = 0x52
    AUTHENT1A = 0x60
    AUTHENT1B = 0x61
  
    PICC_ANTICOLL1 = 0x93
    PICC_ANTICOLL2 = 0x95
    PICC_ANTICOLL3 = 0x97
  
 
    def __init__(self, sck, mosi, miso, rst, cs,baudrate=1000000,spi_id=0):
 
        self.sck = Pin(sck, Pin.OUT)
        self.mosi = Pin(mosi, Pin.OUT)
        self.miso = Pin(miso)
        self.rst = Pin(rst, Pin.OUT)
        self.cs = Pin(cs, Pin.OUT)
 
        self.rst.value(0)
        self.cs.value(1)
        
        board = uname()[0]
 
        if board == 'WiPy' or board == 'LoPy' or board == 'FiPy':
            self.spi = SPI(0)
            self.spi.init(SPI.MASTER, baudrate=1000000, pins=(self.sck, self.mosi, self.miso))
        elif (board == 'esp8266') or (board == 'esp32'):
            self.spi = SPI(baudrate=100000, polarity=0, phase=0, sck=self.sck, mosi=self.mosi, miso=self.miso)
            self.spi.init()
        elif board == 'rp2':
            self.spi = SPI(spi_id,baudrate=baudrate,sck=self.sck, mosi= self.mosi, miso= self.miso)
        else:
            raise RuntimeError("Unsupported platform")
 
        self.rst.value(1)
        self.init()
 
    def _wreg(self, reg, val):
 
        self.cs.value(0)
        self.spi.write(b'%c' % int(0xff & ((reg << 1) & 0x7e)))
        self.spi.write(b'%c' % int(0xff & val))
        self.cs.value(1)
 
    def _rreg(self, reg):
 
        self.cs.value(0)
        self.spi.write(b'%c' % int(0xff & (((reg << 1) & 0x7e) | 0x80)))
        val = self.spi.read(1)
        self.cs.value(1)
 
        return val[0]
 
    def _sflags(self, reg, mask):
        self._wreg(reg, self._rreg(reg) | mask)
 
    def _cflags(self, reg, mask):
        self._wreg(reg, self._rreg(reg) & (~mask))
 
    def _tocard(self, cmd, send):
 
        recv = []
        bits = irq_en = wait_irq = n = 0
        stat = self.ERR
 
        if cmd == 0x0E:
            irq_en = 0x12
            wait_irq = 0x10
        elif cmd == 0x0C:
            irq_en = 0x77
            wait_irq = 0x30
 
        self._wreg(0x02, irq_en | 0x80)
        self._cflags(0x04, 0x80)
        self._sflags(0x0A, 0x80)
        self._wreg(0x01, 0x00)
        for c in send:
            self._wreg(0x09, c)
        self._wreg(0x01, cmd)
 
        if cmd == 0x0C:
            self._sflags(0x0D, 0x80)
 
        i = 2000
        while True:
            n = self._rreg(0x04)
            i -= 1
            if ~((i != 0) and ~(n & 0x01) and ~(n & wait_irq)):
                break
 
        self._cflags(0x0D, 0x80)
 
        if i:
            if (self._rreg(0x06) & 0x1B) == 0x00:
                stat = self.OK
 
                if n & irq_en & 0x01:
                    stat = self.NOTAGERR
                elif cmd == 0x0C:
                    n = self._rreg(0x0A)
                    lbits = self._rreg(0x0C) & 0x07
                    if lbits != 0:
                        bits = (n - 1) * 8 + lbits
                    else:
                        bits = n * 8
 
                    if n == 0:
                        n = 1
                    elif n > 16:
                        n = 16
 
                    for _ in range(n):
                        recv.append(self._rreg(0x09))
            else:
                stat = self.ERR
 
        return stat, recv, bits
 
    def _crc(self, data):
 
        self._cflags(0x05, 0x04)
        self._sflags(0x0A, 0x80)
 
        for c in data:
            self._wreg(0x09, c)
 
        self._wreg(0x01, 0x03)
 
        i = 0xFF
        while True:
            n = self._rreg(0x05)
            i -= 1
            if not ((i != 0) and not (n & 0x04)):
                break
 
        return [self._rreg(0x22), self._rreg(0x21)]
 
    def init(self):
 
        self.reset()
        self._wreg(0x2A, 0x8D)
        self._wreg(0x2B, 0x3E)
        self._wreg(0x2D, 30)
        self._wreg(0x2C, 0)
        self._wreg(0x15, 0x40)
        self._wreg(0x11, 0x3D)
        self.antenna_on()
 
    def reset(self):
        self._wreg(0x01, 0x0F)
 
    def antenna_on(self, on=True):
 
        if on and ~(self._rreg(0x14) & 0x03):
            self._sflags(0x14, 0x03)
        else:
            self._cflags(0x14, 0x03)
 
    def request(self, mode):
 
        self._wreg(0x0D, 0x07)
        (stat, recv, bits) = self._tocard(0x0C, [mode])
 
        if (stat != self.OK) | (bits != 0x10):
            stat = self.ERR
 
        return stat, bits
  
    def anticoll(self,anticolN):
 
        ser_chk = 0
        ser = [anticolN, 0x20]
 
        self._wreg(0x0D, 0x00)
        (stat, recv, bits) = self._tocard(0x0C, ser)
 
        if stat == self.OK:
            if len(recv) == 5:
                for i in range(4):
                    ser_chk = ser_chk ^ recv[i]
                if ser_chk != recv[4]:
                    stat = self.ERR
            else:
                stat = self.ERR
 
        return stat, recv
 
    
    def PcdSelect(self, serNum,anticolN):
        backData = []
        buf = []
        buf.append(anticolN)
        buf.append(0x70)
        #i = 0
        ###xorsum=0;
        for i in serNum:
            buf.append(i)
        #while i<5:
        #    buf.append(serNum[i])
        #    i = i + 1
        pOut = self._crc(buf)
        buf.append(pOut[0])
        buf.append(pOut[1])
        (status, backData, backLen) = self._tocard( 0x0C, buf)
        if (status == self.OK) and (backLen == 0x18):
            return  1
        else:
            return 0
    
    
    def SelectTag(self, uid):
        byte5 = 0
        
        #(status,puid)= self.anticoll(self.PICC_ANTICOLL1)
        #print("uid",uid,"puid",puid)
        for i in uid:
            byte5 = byte5 ^ i
        puid = uid + [byte5]
        
        if self.PcdSelect(puid,self.PICC_ANTICOLL1) == 0:
            return (self.ERR,[])
        return (self.OK , uid)
        
    def tohexstring(self,v):
        s="["
        for i in v:
            if i != v[0]:
                s = s+ ", "
            s=s+ "0x{:02X}".format(i)
        s= s+ "]"
        return s

    def SelectTagSN(self):
        valid_uid=[]
        (status,uid)= self.anticoll(self.PICC_ANTICOLL1)
        #print("Select Tag 1:",self.tohexstring(uid))
        if status != self.OK:
            return  (self.ERR,[])
        
        if self.DEBUG:   print("anticol(1) {}".format(uid))
        if self.PcdSelect(uid,self.PICC_ANTICOLL1) == 0:
            return (self.ERR,[])
        if self.DEBUG:   print("pcdSelect(1) {}".format(uid))
        
        #check if first byte is 0x88
        if uid[0] == 0x88 :
            #ok we have another type of card
            valid_uid.extend(uid[1:4])
            (status,uid)=self.anticoll(self.PICC_ANTICOLL2)
            #print("Select Tag 2:",self.tohexstring(uid))
            if status != self.OK:
                return (self.ERR,[])
            if self.DEBUG: print("Anticol(2) {}".format(uid))
            rtn =  self.PcdSelect(uid,self.PICC_ANTICOLL2)
            if self.DEBUG: print("pcdSelect(2) return={} uid={}".format(rtn,uid))
            if rtn == 0:
                return (self.ERR,[])
            if self.DEBUG: print("PcdSelect2() {}".format(uid))
            #now check again if uid[0] is 0x88
            if uid[0] == 0x88 :
                valid_uid.extend(uid[1:4])
                (status , uid) = self.anticoll(self.PICC_ANTICOLL3)
                #print("Select Tag 3:",self.tohexstring(uid))
                if status != self.OK:
                    return (self.ERR,[])
                if self.DEBUG: print("Anticol(3) {}".format(uid))
                if self.MFRC522_PcdSelect(uid,self.PICC_ANTICOLL3) == 0:
                    return (self.ERR,[])
                if self.DEBUG: print("PcdSelect(3) {}".format(uid))
        valid_uid.extend(uid[0:5])
        # if we are here than the uid is ok
        # let's remove the last BYTE whic is the XOR sum
        
        return (self.OK , valid_uid[:len(valid_uid)-1])
        #return (self.OK , valid_uid)

    def auth(self, mode, addr, sect, ser):
        return self._tocard(0x0E, [mode, addr] + sect + ser[:4])[0]
    
    def authKeys(self,uid,addr,keyA=None, keyB=None):
        status = self.ERR
        if keyA is not None:
            status = self.auth(self.AUTHENT1A, addr, keyA, uid)
        elif keyB is not None:
            status = self.auth(self.AUTHENT1B, addr, keyB, uid)
        return status
       
 
    def stop_crypto1(self):
        self._cflags(0x08, 0x08)
 
    def read(self, addr):
 
        data = [0x30, addr]
        data += self._crc(data)
        (stat, recv, _) = self._tocard(0x0C, data)
        return stat, recv
 
    def write(self, addr, data):
 
        buf = [0xA0, addr]
        buf += self._crc(buf)
        (stat, recv, bits) = self._tocard(0x0C, buf)
 
        if not (stat == self.OK) or not (bits == 4) or not ((recv[0] & 0x0F) == 0x0A):
            stat = self.ERR
        else:
            buf = []
            for i in range(16):
                buf.append(data[i])
            buf += self._crc(buf)
            (stat, recv, bits) = self._tocard(0x0C, buf)
            if not (stat == self.OK) or not (bits == 4) or not ((recv[0] & 0x0F) == 0x0A):
                stat = self.ERR
        return stat
 
 
    def writeSectorBlock(self,uid, sector, block, data, keyA=None, keyB = None):
        absoluteBlock =  sector * 4 + (block % 4)
        if absoluteBlock > 63 :
            return self.ERR
        if len(data) != 16:
            return self.ERR
        if self.authKeys(uid,absoluteBlock,keyA,keyB) != self.ERR :
            return self.write(absoluteBlock, data)
        return self.ERR
 
    def readSectorBlock(self,uid ,sector, block, keyA=None, keyB = None):
        absoluteBlock =  sector * 4 + (block % 4)
        if absoluteBlock > 63 :
            return self.ERR, None
        if self.authKeys(uid,absoluteBlock,keyA,keyB) != self.ERR :
            return self.read(absoluteBlock)
        return self.ERR, None
 
    def MFRC522_DumpClassic1K(self,uid, Start=0, End=64, keyA=None, keyB=None):
        for absoluteBlock in range(Start,End):
            status = self.authKeys(uid,absoluteBlock,keyA,keyB)
            # Check if authenticated
            print("{:02d} S{:02d} B{:1d}: ".format(absoluteBlock, absoluteBlock//4 , absoluteBlock % 4),end="")
            if status == self.OK:                    
                status, block = self.read(absoluteBlock)
                if status == self.ERR:
                    break
                else:
                    for value in block:
                        print("{:02X} ".format(value),end="")
                    print("  ",end="")
                    for value in block:
                        if (value > 0x20) and (value < 0x7f):
                            print(chr(value),end="")
                        else:
                            print('.',end="")
                    print("")
            else:
                break
        if status == self.ERR:
            print("Authentication error")
            return self.ERR
        return self.OK

Final Source Code

Now create a new file and copy the below code and paste the code in the file as main.py in the Raspberry Pi Pico board memory. Here we have saved the file as main.py this is to run the code automatically on raspberry pi pico bootup.

from machine import Pin, I2C
from mfrc522 import MFRC522
import utime
from ssd1306 import SSD1306_I2C
from oled import Write, GFX, SSD1306_I2C
from oled.fonts import ubuntu_mono_15, ubuntu_mono_20
       
rfid_reader = MFRC522(spi_id=0,sck=2,miso=4,mosi=7,cs=5,rst=18)
WIDTH  = 128                                            # oled display width
HEIGHT = 64                                             # oled display height
 
i2c = I2C(0, scl=Pin(17), sda=Pin(16), freq=200000)       # Init I2C using pins GP8 & GP9 (default I2C0 pins)
 
display = SSD1306_I2C(WIDTH, HEIGHT, i2c)                  # Init oled display
fontSize20 = Write(display, ubuntu_mono_20)

while True:
    rfid_reader.init()
    display.fill(0)       
    fontSize20.text("RC522", 40, 0)
    display.text("Interfacing", 25, 22)
    
    (card_status, card_type) = rfid_reader.request(rfid_reader.REQIDL)
    if card_status == rfid_reader.OK:
        (card_status, card_id) = rfid_reader.SelectTagSN()
        if card_status == rfid_reader.OK:
            rfid_card = int.from_bytes(bytes(card_id),"little",False)
            
            if rfid_card == 1497986713:
                display.text("Access",40,40)
                display.text("Accepted",36,50)
                
            elif rfid_card == 540378515:
                display.text("Access",40,40)
                display.text("Denied",40,50)
                
            else:
                print("Detected Card : "+ str(rfid_card))
    display.show()
    utime.sleep(1)

Leave a Comment

Your email address will not be published. Required fields are marked *