Built-in project · Arduino Uno R3

RFID attendance system on an Arduino Uno

RFID attendance system on an Arduino: tap a card on an RC522, the LCD shows IN, LATE or OUT with the DS3231 time, and a CSV on the SD card logs it. The whole build, an Arduino Uno and 5 more parts, 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 Runs in your browser. Free, and no account needed.

The parts retail for about $41. On Mokxi they're free.

RFID attendance system · Arduino Uno R3live0.000 s 0.00x
Press Run, then click a card on the reader
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// RFID attendance system: tap a card, and the Uno looks the student up, takes
// the time from a DS3231 clock, shows it on a 16x2 LCD and writes a line to a
// CSV file on a microSD card. On an Arduino Uno.
//
//   RC522 reader   SDA -> 10, SCK -> 13, MOSI -> 11, MISO -> 12, RST -> 9,
//                  3.3V -> 3.3V, GND -> GND
//   microSD        CS -> 4, SCK -> 13, MOSI -> 11, MISO -> 12, VCC -> 5 V
//   DS3231 clock   SDA -> SDA, SCL -> SCL, VCC -> 5 V, GND -> GND
//   I2C LCD        SDA -> A4, SCL -> A5, VCC -> 5 V, GND -> GND (0x27)
//   piezo          pin 8 and GND
//
// Each student has a card. The first tap of the day checks them in, IN before
// 9:00 and LATE after it; the next tap checks them out, and so on. A card
// that is not on the class list gets a low buzz and is logged as UNKNOWN, so
// the teacher can see who tried. Every tap adds a line to ATTEND.CSV:
//
//   date,time,card,name,status
//   2026-10-19,08:59:40,C3 5A 1E 94,Ana,IN
//
// Open the SD card in the simulator to watch the file grow and download it:
// it opens in any spreadsheet.
//
// # Two buses, four modules
//
// The reader and the SD card share the SPI bus (pins 11, 12 and 13), and
// each has a chip select pin of its own, 10 and 4. Only the module whose chip
// select is low listens. The clock and the LCD share the I2C bus; each
// answers to its own address, 0x68 and 0x27. The Uno's SDA and SCL pins
// beside AREF are the same wires as A4 and A5.
//
// # Why PICC_HaltA()
//
// A card left on the reader would be read again on every pass of loop().
// PICC_HaltA() puts the card to sleep after it has been read, so it has to be
// taken away and tapped again to count twice.

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

const uint8_t READER_SS = 10;
const uint8_t READER_RST = 9;
const uint8_t SD_CS = 4;
const uint8_t PIEZO = 8;

MFRC522 reader(READER_SS, READER_RST);
RTC_DS3231 rtc;
LiquidCrystal_I2C lcd(0x27, 16, 2);

// The class list: each card's UID and the student's name.
const uint8_t STUDENTS = 3;
const uint8_t CARD[STUDENTS][4] = {
    {0xC3, 0x5A, 0x1E, 0x94},
    {0x7B, 0x02, 0xD4, 0x19},
    {0x91, 0xE6, 0x3C, 0xA7},
};
const char *const NAME[STUDENTS] = {"Ana", "Ben", "Chloe"};
bool here[STUDENTS];  // checked in right now?

const uint8_t LATE_HOUR = 9;  // checking in at 9:00 or after is late

bool cardReady = false;
unsigned long messageAt = 0;

// Which student has the card on the reader, or -1 for none.
int8_t findStudent() {
  if (reader.uid.size != 4) return -1;  // the 7-byte sticker is never on it
  for (uint8_t s = 0; s < STUDENTS; s++) {
    if (memcmp(reader.uid.uidByte, CARD[s], 4) == 0) return s;
  }
  return -1;
}

void print2(Print &out, uint8_t n) {
  if (n < 10) out.print('0');
  out.print(n);
}

// The card's UID as hex bytes with spaces, like "C3 5A 1E 94".
void printUid(Print &out) {
  const char *digits = "0123456789ABCDEF";
  for (uint8_t i = 0; i < reader.uid.size; i++) {
    uint8_t b = reader.uid.uidByte[i];
    char hex[4] = {digits[b >> 4], digits[b & 0x0F], ' ', '\0'};
    if (i == reader.uid.size - 1) hex[2] = '\0';
    out.print(hex);
  }
}

// One line of the CSV, to the Serial Monitor or to the file.
void writeLine(Print &out, const DateTime &t, const char *name, const char *status) {
  out.print(t.year());
  out.print('-');
  print2(out, t.month());
  out.print('-');
  print2(out, t.day());
  out.print(',');
  print2(out, t.hour());
  out.print(':');
  print2(out, t.minute());
  out.print(':');
  print2(out, t.second());
  out.print(',');
  printUid(out);
  out.print(',');
  out.print(name);
  out.print(',');
  out.println(status);
}

void logTap(const DateTime &t, const char *name, const char *status) {
  writeLine(Serial, t, name, status);
  if (!cardReady) return;
  File file = SD.open("ATTEND.CSV", FILE_WRITE);
  if (!file) {
    Serial.println("could not open ATTEND.CSV");
    return;
  }
  writeLine(file, t, name, status);
  file.close();  // close it every time, so a pulled card loses nothing
}

void showReady() {
  lcd.clear();
  lcd.print("Tap your card");
  DateTime t = rtc.now();
  lcd.setCursor(0, 1);
  print2(lcd, t.hour());
  lcd.print(':');
  print2(lcd, t.minute());
}

void setup() {
  Serial.begin(115200);
  pinMode(PIEZO, OUTPUT);
  lcd.init();
  lcd.backlight();
  SPI.begin();
  reader.PCD_Init();
  if (!rtc.begin()) Serial.println("no DS3231 at 0x68: check SDA and SCL");

  cardReady = SD.begin(SD_CS);
  if (!cardReady) {
    Serial.println("no SD card: logging to the Serial Monitor only");
  } else if (!SD.exists("ATTEND.CSV")) {
    File file = SD.open("ATTEND.CSV", FILE_WRITE);
    file.println("date,time,card,name,status");
    file.close();
  }
  Serial.println("Attendance ready: tap a card");
  showReady();
}

void loop() {
  // Put "Tap your card" back three seconds after a message.
  if (messageAt != 0 && millis() - messageAt > 3000) {
    messageAt = 0;
    showReady();
  }

  if (!reader.PICC_IsNewCardPresent() || !reader.PICC_ReadCardSerial()) return;

  DateTime t = rtc.now();
  int8_t s = findStudent();
  lcd.clear();
  if (s < 0) {
    tone(PIEZO, 150, 600);
    logTap(t, "?", "UNKNOWN");
    lcd.print("Unknown card");
    lcd.setCursor(0, 1);
    lcd.print("See the teacher");
  } else {
    here[s] = !here[s];
    const char *status = "OUT";
    if (here[s]) status = t.hour() >= LATE_HOUR ? "LATE" : "IN";
    tone(PIEZO, here[s] ? 1760 : 1320, 120);
    logTap(t, NAME[s], status);
    lcd.print(here[s] ? "Welcome " : "Goodbye ");
    lcd.print(NAME[s]);
    lcd.setCursor(0, 1);
    print2(lcd, t.hour());
    lcd.print(':');
    print2(lcd, t.minute());
    lcd.print(' ');
    lcd.print(status);
  }
  messageAt = millis();
  reader.PICC_HaltA();
}

Parts list

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

How it is wired

18 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Arduino Uno R3 pin SCL; Real-time clock, DS3231 pin SCL
  • Arduino Uno R3 pin SDA; Real-time clock, DS3231 pin SDA
  • Ground: Arduino Uno R3 pin GND; Real-time clock, DS3231 pin GND
  • Arduino Uno R3 pin 13; RFID reader, MFRC522 pin SCK; microSD card module pin SCK
  • Arduino Uno R3 pin 12; RFID reader, MFRC522 pin MISO; microSD card module pin MISO
  • Arduino Uno R3 pin 11; RFID reader, MFRC522 pin MOSI; microSD card module pin MOSI
  • Arduino Uno R3 pin 10; RFID reader, MFRC522 pin SDA
  • Arduino Uno R3 pin 9; RFID reader, MFRC522 pin RST
  • Arduino Uno R3 pin 8; Piezo speaker pin 1
  • Arduino Uno R3 pin 4; microSD card module pin CS
  • Arduino Uno R3 pin A4; Character LCD with I2C backpack pin SDA
  • Arduino Uno R3 pin A5; Character LCD with I2C backpack pin SCL
  • Ground: RFID reader, MFRC522 pin GND; microSD card module pin GND
  • 3.3 V: RFID reader, MFRC522 pin 3.3V; microSD card module pin VCC
  • 5 V: Real-time clock, DS3231 pin VCC
  • Ground: Character LCD with I2C backpack pin GND
  • 5 V: Character LCD with I2C backpack pin VCC
  • Ground: Piezo speaker pin 2

Change it and keep it

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