Module 10 - Extension Builds

33. MFRC522 RFID Access Log (VMA202)

Identify cards with an MFRC522, decide allow or deny, and append timestamped events to the Velleman VMA202 SD card.

Estimated time 6-8 hoursIn class Week 10 · Session 48

Learning outcomes

  • Explain in plain words how an RFID reader reads a card's UID (reader = PCD, card = PICC)
  • Name what the VMA202 shield adds: SD card, real-time clock and backup coin cell
  • Wire the MFRC522 at 3.3 V through a level shifter, on SS D7 so it does not clash with the SD card on D10
  • Test the clock, SD card and reader one at a time before running the full sketch
  • Add your own cards to an allow list using the UIDs printed in Serial Monitor
  • Write a timestamped ALLOW or DENY line to ACCESS.LOG for every scan
  • Time the unlock and the beeps with millis, without delay
  • Switch a solenoid lock safely with a MOSFET and a flyback diode

Parts and preparation

Uno; Velleman VMA202 data-logging shield with a CR1220 coin cell fitted; microSD card of 32 GB or smaller, formatted FAT32; MFRC522 RFID reader with at least two cards or fobs; a 4-channel logic-level shifter (or four 1 kΩ resistors); green LED, red LED and two 330 ohm resistors; passive buzzer. For the lock: IRLZ44N MOSFET, 220 ohm and 10 k ohm resistors, 1N4007 diode, a solenoid lock and its own supply. No lock yet? An LED and 330 ohm resistor on D5 stands in for it.

Before power: inspect wiring, confirm supply voltage and ensure all connected circuits share GND.

Libraries for this lesson

In Arduino IDE 2 open Tools → Manage Libraries…. Search by the Library Manager name and install the package by the exact author below. Similar names from other authors can use different APIs and will break the example.

IncludeLibrary Manager nameAuthorInstall note
SPI.hSPIArduinoBuilt into the Arduino core. No Library Manager install required.
MFRC522.hMFRC522GithubCommunityInstall "MFRC522" by GithubCommunity (Miguel Balboa) via Library Manager - not "MFRC522v2", which uses a different header and API.
SD.hSDArduinoBuilt-in Arduino library. No Library Manager install required.
Wire.hWireArduinoBuilt into the Arduino core. No Library Manager install required.
RTClib.hRTClibAdafruitInstall via Library Manager for VMA202 RTC timestamps (same as Lesson 26).
MFRC522 RFID Access Log (VMA202) instructional connection diagram

What this project does

You are building a small door-entry system, like the key-card readers on office or hotel doors. Every card and key fob carries its own ID number, called a UID. Your sketch keeps a short list of allowed UIDs - think of it as the guest list at a door.

When someone holds a card near the reader, the Uno reads the UID and checks the list. On the list: the green LED lights and the lock opens for three seconds. Not on the list: the red LED lights and the buzzer beeps three times. Either way, the Uno adds one line to a file called ACCESS.LOG on the SD card, with the date and time from the shield's clock - so you can see afterwards who scanned, when, and whether they got in.

It brings together SPI (Lesson 21), the real-time clock (Lesson 26), millis timing (Lesson 11) and safe high-current switching (Lesson 16). Nothing here is new on its own - the new part is making them work together.

What happens on every scan: the Uno waits for a card, reads its UID, checks the allow list, then either allows (green LED, unlock 3 s) or denies (red LED, 3 beeps), logs a line to ACCESS.LOG either way and goes back to waiting
Scan, check the list, act, log - then wait for the next card.

How RFID works

The reader board has a coil of copper track (its antenna) that sends out a weak radio field at 13.56 MHz. A card or fob has no battery. Inside it is a coil and a tiny chip. When you hold it within a few centimetres of the reader, the field powers the chip and the card answers with its UID.

The UID is usually 4 bytes (sometimes 7), shown in hex like DE:AD:BE:EF. Your sketch only compares that number with its list. It does not read anything secret from the card, so this is a learning project, not a bank-grade lock.

Datasheets and the library use two short names you will see in the code: PCD means the reader and PICC means the card.

The MFRC522 reader sends a 13.56 MHz field from its antenna; a battery-free card with a coil and chip is powered by the field when held a few centimetres away and replies with its UID, for example DE AD BE EF
The reader powers the card; the card answers with its UID; your sketch decides.
WordMeaning
Reader (PCD)The MFRC522 board wired to the Uno
Card or fob (PICC)What you hold near the reader - no battery inside
UIDThe card's ID number, 4 or 7 bytes - your allow-list key
13.56 MHzThe radio frequency these kit readers and cards use
RangeA few centimetres - hold the card flat over the antenna

Typical kit reader, card and fob

Most Arduino RFID kits include an MFRC522 (RC522) reader board, a white card and a key fob. The large copper coil area on the board is the antenna - hold tags a few centimetres from that face. The 8-pin header is labelled SDA, SCK, MOSI, MISO, IRQ, GND, RST and 3.3V.

The card and the fob each have their own UID. Enrol one as allowed and keep the other as your 'stranger' to test DENY. Metal desks and stacked cards can stop a read.

RFID-RC522 reader module with pin header, blue key fob and white proximity card
Typical kit: RC522 reader (antenna coil on the left, 8-pin header on the right), key fob and card.Photo: Arduino Project Hub - RFID-RC522 Sensor (person87).

Wiring the reader

Two things on this board catch beginners out.

First, the pin labelled SDA is not I2C here. On this board it is the SPI chip-select pin (also called SS). It goes to D7.

Second, the RC522 is a 3.3 V chip. Power it from the Uno's 3.3V pin - never 5 V. The Uno's output pins are 5 V, so the four wires the Uno drives (SS, SCK, MOSI and RST) pass through a level shifter, which turns 5 V signals into 3.3 V signals (Lesson 21). Its HV side connects to the Uno's 5V, its LV side to the Uno's 3.3V, and both GND pins to GND. MISO is the one wire coming back from the reader: 3.3 V is still read as HIGH by the Uno, so it goes straight to D12. No level shifter? Put a 1 kΩ resistor in series in each of the four Uno-driven wires instead.

IRQ is not used - leave it unconnected. In the sketch, MFRC522 rfid(7, 9) tells the library which pins are SS and RST.

MFRC522 wiring: Uno D7, D13, D11 and D9 go through the 5 V side of a level shifter to the reader's SDA, SCK, MOSI and RST on the 3.3 V side; MISO goes straight to D12; reader 3.3V to Uno 3.3V, GND to GND, IRQ not connected
Four Uno-driven wires go through the level shifter. MISO goes direct. Power from 3.3 V only.
Reader pinWhat it doesConnect to
SDA (SS)SPI chip selectD7, through the level shifter
SCKSPI clockD13, through the level shifter
MOSIData from Uno to readerD11, through the level shifter
MISOData from reader to UnoD12, direct
IRQInterrupt (not used)Leave unconnected
GNDGroundGND
RSTReset the readerD9, through the level shifter
3.3VPowerUno 3.3V - never 5 V

What the VMA202 adds

The Velleman VMA202 is a data-logging shield: a board that plugs straight on top of the Uno. It adds a microSD card slot, a DS1307 real-time clock (RTC) with a CR1220 coin cell so it keeps the time when the Uno is unplugged, a reset button and a small soldering area you do not need for this build.

Because it plugs on top, its connections are made through the header pins - no wires. That also means it has already taken some pins: D10 to D13 for the SD card and A4/A5 for the clock. The reader, LEDs and buzzer use pins the shield leaves free.

Velleman VMA202 Arduino-compatible data logging shield showing SD card slot, RTC coin cell and prototyping area
VMA202: SD slot, RTC backup cell, stackable headers and prototyping pads.Photo: Velleman VMA202 product page.
FeatureWhat it does in this project
SD card slot (SPI)Holds ACCESS.LOG; chip select is D10
DS1307 clock (I2C)Gives each log line its date and time, on A4/A5
CR1220 coin cellKeeps the clock running when the Uno is off
Stackable headersThe shield sits on the Uno; wire the reader and outputs to free pins
Soldering areaNot needed for this build

Shared SPI: two chip selects

The SD card and the reader both talk SPI, and they share the same three wires: SCK (D13), MOSI (D11) and MISO (D12). Think of it as one phone line in a house with two phones. Each device also has its own chip-select wire. The Uno pulls one chip select LOW to say "I am talking to you"; the other device sees its own pin HIGH and ignores the traffic.

The shield's SD card is fixed on D10, so the reader gets D7. Many online RFID guides wire the reader to D10 - with the VMA202 fitted, that makes both devices answer at once and neither works. You never switch the chip selects yourself: each library does it inside its own functions.

SCK, MOSI and MISO are shared by the SD card and the reader; to write to the SD card the Uno pulls D10 LOW while D7 stays HIGH, and to read a card it pulls D7 LOW while D10 stays HIGH
Shared data wires, separate chip selects. D10 belongs to the SD card, D7 to the reader.
SPI.begin();          // start the shared wires
rfid.PCD_Init();      // the library uses D7 to talk to the reader
SD.begin(10);         // the library uses D10 to talk to the SD card

Course pin map

This map is the one to follow when the VMA202 is stacked. Other diagrams and photos - including the breadboard photo further down - may use different pins.

Course pin map: the VMA202 shield uses D10 to D13 and A4/A5 through its headers; the MFRC522 uses D7 (SS), D9 (RST) and the shared D11 to D13 at 3.3 V through a level shifter; green LED D3, red LED D4, lock driver D5, buzzer D6
Course pins with the VMA202 stacked.
FunctionUno pin
VMA202 SD card chip selectD10 (through the shield)
SPI MOSI / MISO / SCK (shared)D11 / D12 / D13
VMA202 clock SDA / SCLA4 / A5 (through the shield)
Reader SS (labelled SDA)D7
Reader RSTD9
Reader power3.3V (not 5 V)
Green LEDD3
Red LEDD4
Lock driver (MOSFET gate)D5
BuzzerD6

Build it in five tested steps

Do not start with the full sketch. If you wire everything and it does not work, you cannot tell which part is wrong. Test one part at a time and only move on when it passes:

1. Clock: run the clock sketch from Lesson 26. Pass: Serial Monitor shows the right time, ticking every second.

2. SD card: run the SD card test sketch (below the wiring list). Pass: "SD card OK", and TEST.TXT opens on a PC.

3. Reader: run the card reader test sketch. Pass: a reader version such as 0x91 or 0x92, and a UID for every card.

4. Full sketch as it comes. Pass: every card shows DENY (red LED, three beeps) and lines appear in ACCESS.LOG. That is expected - the placeholder UIDs match nobody's card.

5. Paste your card's UID into the sketch and upload again. Pass: your card shows ALLOW (green LED, lock opens for 3 s); the other card still shows DENY.

If something that worked stops working after you stack the shield, check the reader is on D7, not D10.

Five build steps, each with what to run and how you know it passed: clock sketch, SD card test, card reader test, full sketch with placeholder UIDs (every card DENY), then paste your UID (your card ALLOW)
Only move on when the step passes.

MFRC522 library call sequence

In setup(), SPI.begin() starts the shared wires and rfid.PCD_Init() wakes the reader. That happens once.

In loop(), the sketch asks two questions every pass. rfid.PICC_IsNewCardPresent() asks "is a new card near the reader?" and rfid.PICC_ReadCardSerial() reads its UID. If either answer is no, return ends this pass of loop() early - loop() simply starts again a moment later. That is how the sketch waits for a card without stopping anything else.

After a successful read, rfid.uid.uidByte holds the UID bytes and rfid.uid.size says how many there are (4 or 7). When you have acted and logged, rfid.PICC_HaltA() and rfid.PCD_StopCrypto1() tell the card you are finished. A card that stays on the reader is not read again - take it away and bring it back to scan again.

setup runs SPI.begin and PCD_Init once; each pass of loop asks PICC_IsNewCardPresent and PICC_ReadCardSerial, returns early if either is false, otherwise uses uid.uidByte and uid.size, acts and logs, then calls PICC_HaltA and PCD_StopCrypto1
Start once. Each pass: card near? read it? - then act, log and finish with the card.
if (!rfid.PICC_IsNewCardPresent()) return;   // no card: end this pass
if (!rfid.PICC_ReadCardSerial()) return;     // could not read: end this pass
// rfid.uid.uidByte[] now holds the UID, rfid.uid.size how many bytes
rfid.PICC_HaltA();         // finished with this card
rfid.PCD_StopCrypto1();

Enrol UIDs from Serial

The two UIDs in the worked sketch are placeholders. They are not your cards, so every card is denied until you add your own.

Upload the card reader test sketch, open Serial Monitor at 9600 baud and hold a card on the reader. It prints the UID and, on the next line, the same bytes ready to paste - for example {0x4A, 0x7F, 0x12, 0xC3}. The 0x in front of each byte just means the number is written in hex. Copy that line into allowedUid1 in the worked sketch, upload it, and scan the card again.

The number of bytes must match as well: a 4-byte entry never matches a 7-byte card.

Enrolling a card: Serial Monitor prints UID 4A:7F:12:C3 and a paste-ready line {0x4A, 0x7F, 0x12, 0xC3}; it replaces the placeholder bytes in allowedUid1, the sketch is uploaded again and the card now shows ALLOW. A 4-byte entry never matches a 7-byte card
Scan, copy, paste, upload, scan again.
// Card reader test printed:  Paste this: {0x4A, 0x7F, 0x12, 0xC3}
byte allowedUid1[] = {0x4A, 0x7F, 0x12, 0xC3};   // was the placeholder {0xDE, 0xAD, 0xBE, 0xEF}

Allow and deny behaviour

Allowed card: green LED on and the lock driver on for three seconds. Denied card: red LED on and three short beeps; the lock stays shut. Both are written to ACCESS.LOG.

The sketch does not use delay(3000). A delay would freeze the whole Uno for three seconds - no beeps, no card reading. Instead startAllow() switches the outputs on and notes the time with millis(), like starting a kitchen timer. updateOutputs() runs on every pass of loop() and switches them off when the time is up (Lesson 11).

ResultLEDs and buzzerLock
ALLOWGreen LED for 3 sDriver on for 3 s, then off
DENYRed LED + 3 short beepsStays off
EitherOne line in ACCESS.LOGDate and time + UID + result

Prepare the SD card and ACCESS.LOG

Format the microSD card as FAT32 on a PC before you start (FAT16 also works). Watch out: cards of 64 GB and bigger come formatted as exFAT, which the Arduino SD library cannot read - use a card of 32 GB or less.

Push the card into the VMA202 slot until it clicks. The sketch calls SD.begin(10) because the shield's chip select is D10.

Keep file names short: up to 8 characters, a dot, then up to 3 (ACCESS.LOG, TEST.TXT). Opening with FILE_WRITE adds new lines to the end, so old lines are kept. close() is what actually saves the line onto the card - the sketch closes the file after every scan, so pulling the power loses nothing that was already logged.

SD card checklist: format FAT32 on a PC using a card of 32 GB or less (exFAT will not work), push it into the VMA202 slot so SD.begin(10) finds it, use short 8.3 file names like ACCESS.LOG, and close the file after every line
FAT32, 32 GB or less, clicked in, short name, close every write.

ACCESS.LOG line format

Each scan adds one line, newest at the bottom: 2026-07-31 15:40:03,DE:AD:BE:EF,ALLOW

The date and time come from the shield's clock, the UID from the reader, and ALLOW or DENY from your allow-list check. Commas separate the parts (a CSV line), so the file also opens as neat columns in Excel or Google Sheets.

To check it, switch off, take the card out and open ACCESS.LOG on a PC. If the file is missing, SD.begin failed or no card was scanned. If the time is wrong, see the clock steps in Lesson 26.

ACCESS.LOG opened on a PC with several ALLOW and DENY lines; one line is split into date and time from the RTC, card UID from the reader, and ALLOW or DENY from the allow list
One line per scan: when, which card, and the result.

Solenoid safety

A solenoid lock needs far more current than an Uno pin can give, so the Uno only sends a small signal from D5. A MOSFET does the heavy switching, and the lock gets its own supply. This is the same low-side switch as Lesson 16.

D5 goes through a 220 ohm resistor to the MOSFET gate, and a 10 k ohm resistor from gate to GND keeps the lock off while the Uno is starting up. A diode across the coil (band towards +V) soaks up the voltage spike the coil makes when it switches off. Join the lock supply's GND to the Uno's GND.

No lock yet? Put an LED and 330 ohm resistor on D5 instead - it lights whenever the lock would open, and the sketch does not change.

Solenoid driver schematic: Uno D5 through 220 ohm to the IRLZ44N gate with a 10 k pull-down; the coil sits between the external +V and the drain with a diode across it, band to +V; source and both grounds joined. The big coil current loop never passes through the Uno
Small signal from D5. Big coil current from the lock's own supply. Diode across the coil.
PartWhy it is there
Separate lock supplyThe coil needs far more current than an Uno pin can give
MOSFET (IRLZ44N)A logic-level switch that a 5 V pin can turn fully on
220 ohm gate resistorLimits the current spike into the gate when D5 switches
10 k ohm pull-downHolds the gate LOW so the lock stays shut during reset
Diode across the coilStops the switch-off spike damaging the MOSFET
Joined GNDThe Uno's signal needs the same reference as the MOSFET

Assessment evidence

A complete demo pack for this project usually includes:

1. Your pin map showing D10 for the SD card and D7 for the reader

2. Serial Monitor capture from the card reader test with your UIDs

3. Your commented sketch with named pins and your own allow-list bytes

4. An ACCESS.LOG excerpt with both ALLOW and DENY lines

5. A photo or note of the MOSFET and diode lock drive (or the LED stand-in)

6. One fault you found and how you fixed it (often D10 instead of D7, or 5 V on the reader)

Wiring and safe build sequence

Breadboard wiring for 33. MFRC522 RFID Access Log (VMA202)
Idea only: this photo uses different pins (reader SS on D10) and has no shield or level shifter. Wire your build from the course pin map and the list below - with the VMA202 stacked, the reader's SS must be D7.
  1. Fit the CR1220 coin cell, put the microSD card in, then stack the VMA202 on the Uno. The shield connects D10-D13 and A4/A5 itself - no wires
  2. Level shifter: HV -> Uno 5V, LV -> Uno 3.3V, both GND pins -> GND
  3. Uno D7 -> shifter HV1, LV1 -> reader SDA (chip select - not D10)
  4. Uno D13 -> shifter HV2, LV2 -> reader SCK
  5. Uno D11 -> shifter HV3, LV3 -> reader MOSI
  6. Uno D9 -> shifter HV4, LV4 -> reader RST
  7. Reader MISO -> Uno D12 directly (no shifter needed)
  8. Reader 3.3V -> Uno 3.3V (never 5 V), reader GND -> GND, reader IRQ not connected
  9. No level shifter? Wire D7, D13, D11 and D9 straight to the reader, each through a 1 kΩ resistor
  10. Green LED: D3 -> 330 ohm -> LED long leg; short leg -> GND
  11. Red LED: D4 -> 330 ohm -> LED long leg; short leg -> GND
  12. Passive buzzer: + -> D6, other leg -> GND
  13. Lock: D5 -> 220 ohm -> IRLZ44N gate; 10 k ohm gate -> GND; drain -> lock coil -> lock supply +V; source -> GND
  14. 1N4007 diode across the lock coil, band (cathode) to +V; lock supply GND joined to Uno GND
Power rule: switch off before moving wires. Arduino I/O pins are control signals; high-current loads require a driver and suitable external supply.

Step 2 test: SD card

Download .ino

What this sketch is for: Checks the VMA202 SD card on its own, then writes one line to TEST.TXT. Run it with the shield stacked; the reader can stay wired. Open Serial Monitor at 9600 baud.

#include <SPI.h>
#include <SD.h>

const byte sdCsPin = 10;     // VMA202 SD card chip select (fixed on the shield)
const byte rfidSsPin = 7;    // reader chip select - held HIGH so the reader stays quiet

void setup() {
  Serial.begin(9600);
  pinMode(rfidSsPin, OUTPUT);
  digitalWrite(rfidSsPin, HIGH);

  if (!SD.begin(sdCsPin)) {
    Serial.println("SD card FAILED");
    Serial.println("Check: card pushed in until it clicks, FAT32, 32 GB or smaller");
    return;                  // leave setup early - nothing else can work
  }
  Serial.println("SD card OK");

  File testFile = SD.open("TEST.TXT", FILE_WRITE);   // opens (or creates) the file
  if (!testFile) {
    Serial.println("Could not open TEST.TXT");
    return;
  }
  testFile.println("Hello from the Uno");   // adds one line at the end
  testFile.close();                         // close() saves it onto the card
  Serial.println("Wrote a line to TEST.TXT - open it on a PC to check");
}

void loop() {
  // nothing to repeat - this test runs once
}

How the code works

  1. SD.begin(10) starts the card using the shield's chip select, D10. It returns false if no card is found or the card is not FAT16/FAT32.
  2. D7 is set HIGH first so the reader ignores the SPI traffic meant for the SD card.
  3. return inside setup() just ends setup() early - there is no point writing a file if the card failed.
  4. Each time you press reset, one more "Hello from the Uno" line is added to TEST.TXT. Several lines on the PC means it is working.

Step 3 test: card reader

Download .ino

What this sketch is for: Checks the reader on its own and prints the UID of every card you scan, plus a line you can paste straight into the allow list. Use it again whenever you want to add a card. Serial Monitor at 9600 baud.

#include <SPI.h>
#include <MFRC522.h>

const byte rfidSsPin = 7;    // reader SDA/SS pin
const byte rfidRstPin = 9;   // reader RST pin
const byte sdCsPin = 10;     // VMA202 SD chip select - held HIGH so the SD card stays quiet

MFRC522 rfid(rfidSsPin, rfidRstPin);

void setup() {
  Serial.begin(9600);
  pinMode(sdCsPin, OUTPUT);
  digitalWrite(sdCsPin, HIGH);

  SPI.begin();               // start the shared SPI wires
  rfid.PCD_Init();           // wake up the reader

  byte version = rfid.PCD_ReadRegister(rfid.VersionReg);
  Serial.print("Reader version: 0x");
  Serial.println(version, HEX);
  if (version == 0x00 || version == 0xFF) {
    Serial.println("No answer from the reader - check 3.3V, GND, D7 and the SPI wires");
  }
  Serial.println("Hold a card near the reader...");
}

void loop() {
  if (!rfid.PICC_IsNewCardPresent()) return;   // no new card yet - loop runs again
  if (!rfid.PICC_ReadCardSerial()) return;     // card moved away - try again

  // Line 1: the UID as text, e.g. UID: DE:AD:BE:EF (4 bytes)
  Serial.print("UID: ");
  for (byte i = 0; i < rfid.uid.size; i++) {
    if (rfid.uid.uidByte[i] < 0x10) Serial.print("0");   // keep two digits: 0A, not A
    Serial.print(rfid.uid.uidByte[i], HEX);
    if (i < rfid.uid.size - 1) Serial.print(":");
  }
  Serial.print(" (");
  Serial.print(rfid.uid.size);
  Serial.println(" bytes)");

  // Line 2: the same bytes, ready to paste into the allow list
  Serial.print("Paste this: {");
  for (byte i = 0; i < rfid.uid.size; i++) {
    Serial.print("0x");
    if (rfid.uid.uidByte[i] < 0x10) Serial.print("0");
    Serial.print(rfid.uid.uidByte[i], HEX);
    if (i < rfid.uid.size - 1) Serial.print(", ");
  }
  Serial.println("}");
  Serial.println();

  rfid.PICC_HaltA();         // tell the card we are finished
  rfid.PCD_StopCrypto1();    // reset the reader for the next card
}

How the code works

  1. The version line proves the reader answers. 0x91 or 0x92 is a genuine MFRC522; some clone boards show 0x88 or 0xB2 and still work. 0x00 or 0xFF means no answer - check 3.3V, GND, D7 and the level shifter.
  2. The two return lines make loop() wait for a card: no card means this pass ends early and loop() runs again.
  3. rfid.uid.uidByte is an array holding the UID bytes and rfid.uid.size says how many there are. The for loop prints them one by one.
  4. Serial.print(value, HEX) prints a number in hex. A byte below 0x10 would print as one digit, so a 0 is added first to keep every byte two digits long.
  5. Expected output for one card: UID: 4A:7F:12:C3 (4 bytes), then Paste this: {0x4A, 0x7F, 0x12, 0xC3}. Your numbers will be different.
  6. Holding a card still does not repeat the print. Take it away and bring it back to scan again.

Worked sketch: the full access logger

Download .ino

What this sketch is for: The finished project: reads a card, checks the allow list, drives the LEDs, buzzer and lock, and logs every scan. Run it after the two test sketches above pass. The first time, every card shows DENY - that is expected until you paste your own UIDs.

#include <SPI.h>
#include <MFRC522.h>
#include <SD.h>
#include <Wire.h>
#include <RTClib.h>

// ----- Pins (course pin map) -----
const byte sdCsPin = 10;       // VMA202 SD card chip select (fixed on the shield)
const byte rfidSsPin = 7;      // reader SDA/SS - NOT D10, the SD card uses that
const byte rfidRstPin = 9;     // reader RST
const byte greenLed = 3;
const byte redLed = 4;
const byte lockPin = 5;        // to the MOSFET gate (or a stand-in LED)
const byte buzzerPin = 6;      // passive buzzer

MFRC522 rfid(rfidSsPin, rfidRstPin);
RTC_DS1307 rtc;                // the clock chip on the VMA202

// ----- Allow list -----
// These are placeholders, not your cards. Run the card reader test sketch,
// then paste the {0x.., ...} line it prints for each card you want to allow.
byte allowedUid1[] = {0xDE, 0xAD, 0xBE, 0xEF};
byte allowedUid2[] = {0x01, 0x23, 0x45, 0x67};

// ----- Timing (milliseconds) -----
const unsigned long unlockMs = 3000;   // ALLOW: how long the lock stays open
const unsigned long denyMs = 900;      // DENY: how long the red LED stays on
const unsigned long beepGapMs = 180;   // DENY: time between beeps

// ----- Output state, used by updateOutputs() -----
bool unlocking = false;
unsigned long unlockStart = 0;
bool denying = false;
unsigned long denyStart = 0;
byte beepsLeft = 0;
unsigned long lastBeep = 0;

// true if the card just read has exactly the same bytes as one allow-list entry
bool cardMatches(byte allowed[], byte allowedLength) {
  if (rfid.uid.size != allowedLength) return false;   // a 4-byte entry never matches a 7-byte card
  for (byte i = 0; i < allowedLength; i++) {
    if (rfid.uid.uidByte[i] != allowed[i]) return false;
  }
  return true;
}

// true if the card just read is on the allow list
bool cardIsAllowed() {
  if (cardMatches(allowedUid1, sizeof(allowedUid1))) return true;
  if (cardMatches(allowedUid2, sizeof(allowedUid2))) return true;
  return false;
}

// The card's UID as text, e.g. "DE:AD:BE:EF"
String uidAsText() {
  String text = "";
  for (byte i = 0; i < rfid.uid.size; i++) {
    if (rfid.uid.uidByte[i] < 0x10) text += "0";   // keep two digits: 0A, not A
    text += String(rfid.uid.uidByte[i], HEX);
    if (i < rfid.uid.size - 1) text += ":";
  }
  text.toUpperCase();
  return text;
}

// Adds one line to ACCESS.LOG, e.g. 2026-07-31 15:40:03,DE:AD:BE:EF,ALLOW
void logScan(String uidText, bool allowed) {
  DateTime now = rtc.now();
  char stamp[20];
  snprintf(stamp, sizeof(stamp), "%04d-%02d-%02d %02d:%02d:%02d",
           now.year(), now.month(), now.day(),
           now.hour(), now.minute(), now.second());

  File logFile = SD.open("ACCESS.LOG", FILE_WRITE);   // FILE_WRITE adds to the end
  if (!logFile) {
    Serial.println("Could not open ACCESS.LOG");
    return;
  }
  logFile.print(stamp);
  logFile.print(",");
  logFile.print(uidText);
  if (allowed) {
    logFile.println(",ALLOW");
  } else {
    logFile.println(",DENY");
  }
  logFile.close();             // close() saves the line onto the card
}

void startAllow() {
  digitalWrite(redLed, LOW);
  digitalWrite(greenLed, HIGH);
  digitalWrite(lockPin, HIGH); // unlock
  noTone(buzzerPin);
  denying = false;
  beepsLeft = 0;
  unlocking = true;
  unlockStart = millis();
}

void startDeny() {
  digitalWrite(greenLed, LOW);
  digitalWrite(lockPin, LOW);  // stay locked
  digitalWrite(redLed, HIGH);
  unlocking = false;
  tone(buzzerPin, 1800, 80);   // first beep now...
  beepsLeft = 2;               // ...two more from updateOutputs()
  lastBeep = millis();
  denying = true;
  denyStart = millis();
}

// Called on every pass of loop(): switches outputs off when their time is up
void updateOutputs() {
  unsigned long now = millis();

  if (unlocking && now - unlockStart >= unlockMs) {
    digitalWrite(lockPin, LOW);  // lock again
    digitalWrite(greenLed, LOW);
    unlocking = false;
  }

  if (beepsLeft > 0 && now - lastBeep >= beepGapMs) {
    tone(buzzerPin, 1800, 80);
    beepsLeft--;
    lastBeep = now;
  }

  if (denying && now - denyStart >= denyMs) {
    digitalWrite(redLed, LOW);
    denying = false;
  }
}

void setup() {
  pinMode(greenLed, OUTPUT);
  pinMode(redLed, OUTPUT);
  pinMode(lockPin, OUTPUT);
  pinMode(buzzerPin, OUTPUT);
  digitalWrite(lockPin, LOW);  // start locked
  Serial.begin(9600);

  SPI.begin();
  rfid.PCD_Init();
  Serial.print("Reader version: 0x");
  Serial.println(rfid.PCD_ReadRegister(rfid.VersionReg), HEX);

  Wire.begin();
  if (!rtc.begin()) {
    Serial.println("RTC not found on the VMA202");
  }
  if (!rtc.isrunning()) {      // set the clock once, only if it has stopped
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  if (SD.begin(sdCsPin)) {
    Serial.println("SD card OK");
  } else {
    Serial.println("SD card FAILED (CS = D10)");
  }
  Serial.println("Ready - scan a card");
}

void loop() {
  updateOutputs();             // runs every pass, card or not

  if (!rfid.PICC_IsNewCardPresent()) return;   // no new card - loop runs again
  if (!rfid.PICC_ReadCardSerial()) return;     // card moved away - try again

  String uidText = uidAsText();
  bool allowed = cardIsAllowed();

  Serial.print("UID ");
  Serial.print(uidText);
  if (allowed) {
    Serial.println(" -> ALLOW");
    startAllow();
  } else {
    Serial.println(" -> DENY");
    startDeny();
  }

  logScan(uidText, allowed);

  rfid.PICC_HaltA();           // tell the card we are finished
  rfid.PCD_StopCrypto1();      // reset the reader for the next card
}

How the code works

  1. Pins first: every pin has a name, so the rest of the sketch says lockPin instead of 5. Note the reader is on D7 - D10 belongs to the SD card.
  2. The allow list: byte allowedUid1[] = {0xDE, 0xAD, 0xBE, 0xEF}; is an array of four bytes - one card's UID. These two are placeholders, so every card is denied until you paste your own from the card reader test.
  3. loop() runs thousands of times a second. It first calls updateOutputs(), then asks the reader for a card. If there is no card, return ends this pass and loop() starts again - so the sketch is always waiting, never stuck.
  4. cardIsAllowed() checks each allow-list entry with cardMatches(). cardMatches() first checks the length (4 or 7 bytes), then compares the bytes one by one in a for loop. One different byte and the answer is false.
  5. sizeof(allowedUid1) counts the bytes in that array, so a 7-byte card works too if you paste a 7-byte line.
  6. uidAsText() turns the bytes into text like DE:AD:BE:EF for Serial Monitor and the log file. String(value, HEX) writes a number in hex and toUpperCase() makes the letters capitals.
  7. startAllow() and startDeny() switch the outputs on and note the start time with millis(). They do not wait.
  8. updateOutputs() is the kitchen timer: on every pass it asks "has 3 seconds passed since unlockStart?" and switches the lock off when it has. The beeps work the same way. Writing it as millis() - start >= interval keeps it correct even when millis() rolls over after about 49 days.
  9. logScan() reads the time from the clock, formats it with snprintf (the same pattern as Lesson 26), opens ACCESS.LOG with FILE_WRITE so the line goes on the end, writes date and time, UID and result, then close() saves it.
  10. In setup(), rtc.adjust() sets the clock from your PC's time only if the clock has stopped - the set-once pattern from Lesson 26. The reader version line is the same check as the card reader test.
  11. PICC_HaltA() and PCD_StopCrypto1() finish with the card, so a card left on the reader is logged once, not hundreds of times.

Test and record evidence

Expected result: Serial Monitor (9600 baud) shows the reader version, SD card OK and Ready - scan a card. Each scan prints a line such as UID 4A:7F:12:C3 -> ALLOW. An allowed card lights the green LED and opens the lock for three seconds; any other card lights the red LED and beeps three times. ACCESS.LOG on the SD card has one line per scan with the date and time, UID and result.

Practical evidence checklist

Common faults and checks
  • SD card FAILED: card not pushed in until it clicks, or formatted exFAT (64 GB and bigger cards) - use a 32 GB or smaller card formatted FAT32.
  • Reader version 0x00 or 0xFF: the reader is not answering. Check 3.3V and GND, the level shifter's HV/LV/GND pins, and that SDA goes to D7.
  • Worked alone but stops after stacking the shield: the reader is on D10. Move it to D7 and change rfidSsPin.
  • Every card shows DENY: you have not pasted your UID yet, or a byte is copied wrong. Run the card reader test and paste its line exactly; check 4 vs 7 bytes.
  • A card on the reader is not read again: that is normal. Take it away and bring it back.
  • The same card logs many lines: PICC_HaltA() and PCD_StopCrypto1() are missing from the end of loop().
  • Lock is weak or the Uno resets when it clicks: the lock needs its own supply, a diode across the coil and a joined GND.
  • Wrong date or time in ACCESS.LOG: fit or replace the CR1220 cell and set the clock once as in Lesson 26.
Challenge (extension - optional): Store allowed UIDs in EEPROM and add a master card that enrols a new card into the allow list without re-uploading.

Check your understanding

Q1. In plain words, what is a UID?

Show answer

The card's own ID number (usually 4 or 7 bytes). The sketch compares it with the allow list.

Q2. What do PCD and PICC mean in this lesson?

Show answer

PCD is the reader (MFRC522); PICC is the card or fob.

Q3. Which VMA202 pin is SD chip select?

Show answer

D10.

Q4. Why must the reader's SS avoid D10 when the VMA202 is stacked?

Show answer

D10 is already the SD card's chip select. Both devices would answer at once on the shared SPI wires.

Q5. What voltage powers the MFRC522, and why do four of its wires go through a level shifter?

Show answer

3.3 V. The Uno's outputs are 5 V, so SS, SCK, MOSI and RST are shifted down to 3.3 V. MISO comes from the reader and goes straight to the Uno.

Q6. On the MFRC522 module, what does the SDA label mean?

Show answer

SPI chip select (SS), not I2C data.

Q7. Why does every card show DENY the first time you run the full sketch?

Show answer

The allow list still holds placeholder UIDs. Paste your own card's UID from the card reader test.

Q8. What three parts go in each ACCESS.LOG line?

Show answer

Date and time, the card's UID, and ALLOW or DENY.

Q9. Why call PICC_HaltA and PCD_StopCrypto1 after a read?

Show answer

To finish with that card, so a card left on the reader is not read over and over and the next card can be detected.

Q10. Why use millis for the 3 s unlock instead of delay(3000)?

Show answer

delay would freeze loop for 3 seconds - no card reading and no beeps. millis lets loop keep running while the timer counts.