Building a basic RFID card reader


Access cards are used everywhere on a day to day business, ranging from highly secure facilities to medium scale enterprises to small businesses. Most access cards use some kind of Radio Frequency ID (RFID) technology.


Software: Arduino IDE or VSCode (with Arduino plugin).
Hardware: Arduino Uno, Adafruit PN532 Shield, Adafruit RGB LCD Screen, SD Card Reader (Optional)
Code: Modified Adafruit PN532 Library (Link provided)


The PN532 Breakout Board comes with its own library which I slightly modified in order to boost its capabilities. Below we can see the actual board with the soldered pins.

Adafruit RGB LCD Shield Kit

The RGB LCD Shield kit comes equipped with everything you would need for a menu screen selector. It comes with buttons, which can be configured or simply left unused. Its support library is pretty complete, although the buttons do not have any de-bouncing methods, but we manage to overcome this, as shown later.

#include <Wire.h> #include <SPI.h> #include <Adafruit_PN532.h> #include <Adafruit_RGBLCDShield.h> #include <utility/Adafruit_MCP23017.h> // If using the breakout with SPI, define the pins for SPI communication. #define PN532_SCK (2) #define PN532_MOSI (3) #define PN532_SS (4) #define PN532_MISO (5) // If using the breakout or shield with I2C, define just the pins connected // to the IRQ and reset lines. Use the values below (2, 3) for the shield! #define PN532_IRQ (2) #define PN532_RESET (3) // Not connected by default on the NFC Shield // Use this line for a breakout with a software SPI connection (recommended): Adafruit_PN532 nfc(PN532_SCK, PN532_MISO, PN532_MOSI, PN532_SS); Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield(); #if defined(ARDUINO_ARCH_SAMD) // for Zero, output on USB Serial console, remove line below if using programming port to program the Zero! // also change #define in Adafruit_PN532.cpp library file #define Serial SerialUSB #endif

Modified Library

Big thanks to Adafruit for having an open source library with examples, as well as making their hardware compatible with the NFClib library. We won’t be looking at the NFClib library in this project, but it is widely used among very popular devices such as the proxmark3, and could be used for cloning cards with this setup (I have yet to explore if it’s possible using the PN532 library but I will be sure to update this once I have).

/**************************************************************************/ /*! Sets the MxRtyPassiveActivation byte of the RFConfiguration register @param sakValue Holds the value of the SAK attribute @returns 1 if everything executed properly, 0 for an error */ /**************************************************************************/ bool Adafruit_PN532::readCardSAK(uint8_t cardbaudrate, uint8_t * sakValue, uint16_t timeout) { pn532_packetbuffer[0] = PN532_COMMAND_INLISTPASSIVETARGET; pn532_packetbuffer[1] = 1; // max 1 cards at once (we can set this to 2 later) pn532_packetbuffer[2] = cardbaudrate; if (!sendCommandCheckAck(pn532_packetbuffer, 3, timeout)) { #ifdef PN532DEBUG PN532DEBUGPRINT.println(F("No card(s) read")); #endif return 0x0; // no cards read } // wait for a card to enter the field (only possible with I2C) if (!_usingSPI) { #ifdef PN532DEBUG PN532DEBUGPRINT.println(F("Waiting for IRQ (indicates card presence)")); #endif if (!waitready(timeout)) { #ifdef PN532DEBUG PN532DEBUGPRINT.println(F("IRQ Timeout")); #endif return 0x0; } } // read data packet readdata(pn532_packetbuffer, 20); /* ISO14443A card response should be in the following format: byte Description ------------- ------------------------------------------ b0..6 Frame header and preamble b7 Tags Found b8 Tag Number (only one used in this example) b9..10 SENS_RES b11 SEL_RES b12 NFCID Length b13..NFCIDLen NFCID */ if (pn532_packetbuffer[7] != 1) return 0; *sakValue = pn532_packetbuffer[11]; return 1; }
#ifdef MIFAREDEBUG PN532DEBUGPRINT.print(F("ATQA: 0x")); PN532DEBUGPRINT.println(sens_res, HEX); PN532DEBUGPRINT.print(F("SAK: 0x")); PN532DEBUGPRINT.println(pn532_packetbuffer[11]); #endif

LCD Screen Functions

I found it tedious to continuously have to call certain functions to format printing on the screen. It was adding too much to my code and making it look ugly. Instead I decided to make a separate file where I created functions to format the printing methods, making it easier to read the code.

#define WHITE 0x7 int selector = 0; String toDisplay[3] = {"1.Get UID", "2.Card Info", "3.Clone Card"}; void startScreen() { lcd.begin(16, 2); lcd.setBacklight(WHITE); }
void start() { // Got ok data, print it out! lcd.clear(); lcd.setCursor(0, 0); lcd.print(toDisplay[0]); lcd.setCursor(15, 0); lcd.print("<"); lcd.setCursor(0, 1); lcd.print(toDisplay[1]); }
void printFirstLine(String s1) { lcd.setCursor(0,0); lcd.print(s1); } void printSecondLine(String s1) { lcd.setCursor(0,1); lcd.print(s1); } void printDataDEC(uint32_t data) { lcd.print(data, DEC); } void printDataHEX(uint32_t data) { lcd.print(data, HEX); } void printUID(uint8_t num) { lcd.print(num, HEX); } void reset() { lcd.clear(); lcd.setCursor(0,0); } void printLines(String s1, String s2) { lcd.clear(); lcd.setCursor(0, 0); lcd.print(s1); lcd.setCursor(0,1); lcd.print(s2); }unsigned long lastDebounceTime = 0; unsigned long debounceDelay = 50; void menu() { uint8_t buttons = lcd.readButtons(); if (buttons) { reset(); if (buttons & BUTTON_UP) { if ((millis() - lastDebounceTime) > debounceDelay) { switch(selector) { case 0: printLines(toDisplay[selector], toDisplay[selector+1]); lcd.setCursor(15, selector); lcd.print("<"); break; case 1: selector--; printLines(toDisplay[selector], toDisplay[selector+1]); lcd.setCursor(15, selector); lcd.print("<"); break; case 2: selector--; printLines(toDisplay[selector], toDisplay[selector+1]); lcd.setCursor(15, selector-1); lcd.print("<"); break; } } } if (buttons & BUTTON_DOWN) { if ((millis() - lastDebounceTime) > debounceDelay) { switch(selector) { case 0: selector++; printLines(toDisplay[selector-1], toDisplay[selector]); lcd.setCursor(15, selector); lcd.print("<"); break; case 1: selector++; printLines(toDisplay[selector-1], toDisplay[selector]); lcd.setCursor(15, selector-1); lcd.print("<"); break; case 2: printLines(toDisplay[selector-1], toDisplay[selector]); lcd.setCursor(15, selector-1); lcd.print("<"); break; } } } if (buttons & BUTTON_SELECT) { if (selector == 0) { getMifareUID(); } else if (selector == 1) { readCard(); } else { clone(); } } lastDebounceTime = millis(); } }


If you’re like me and haven’t heard of de-bouncing before, it occurs when you press a button one time, but it registers multiple presses (electricity works fast). The process of making it so that only one press is registered is known as de-bouncing.

RFID Funtions

The PN532 is primarily utilised through the CardFunctions.ino file, which contains all the methods I’ve implemented for this project.

void startReader() { nfc.begin(); } void getBoardFirmware(uint32_t versiondata) { versiondata = nfc.getFirmwareVersion(); } void configToRead() { // configure board to read RFID tags nfc.SAMConfig(); }
//Returns the UID of the card being read void getMifareUID() { uint8_t success; uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; // Buffer to store the returned UID uint8_t uidLength; // Length of the UID (4 or 7 bytes depending on ISO14443A card type) // Wait for an ISO14443A type cards (Mifare, etc.). When one is found // 'uid' will be populated with the UID, and uidLength will indicate // if the uid is 4 bytes (Mifare Classic) or 7 bytes (Mifare Ultralight) printLines("Waiting for an", "ISO14443A Card..."); success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength); if (success) { // Display some basic information about the card printLines("Found ISO14443A card", "UID: "); for (int i = 0; i < uidLength; i++) { printUID(uid[i]); } } }// Finds the SAK value of the card and then determines what type // of RFID the card is void readCard() { uint8_t sakValue; uint8_t success; printLines("Waiting for an", "ISO14443A Card..."); success = nfc.readCardSAK(PN532_MIFARE_ISO14443A, &sakValue); if (success) { reset(); switch (sakValue) { case 0: printLines("Mifare Ultralight", "Detected"); break; case 8: printLines("Mifare Classic", "1K Detected"); break; case 18: printLines("Mifare Classic", "4K Detected"); break; case 24: printLines("Mifare DESFire", "Detected"); break; default: printLines("Could not read", "card"); break; } } }


Moving onto the main event, the RFID_Main.ino file, which brings all the functions we saw together to create what we see in the demo below.

void setup() { Serial.begin(115200); startReader(); startScreen(); uint32_t versiondata = nfc.getFirmwareVersion(); if (! versiondata) { reset(); printFirstLine("Couldn't find"); printSecondLine("board"); while (1); // halt } printFirstLine("Found chip PN5"); printDataHEX((versiondata >> 24) & 0xFF); printSecondLine("Firmware ver. "); printDataDEC((versiondata >> 16) & 0xFF); delay(1500); start(); configToRead(); }
void loop() { menu(); }



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store

Leading with strategy, design and architecture, we connect cloud, data, and cyber to engineer and deliver large-scale, complex transformations.