Community project

RFID Face Attendance System

Photo of RFID Face Attendance System
Generated with AI

Hossein Panda

Published November 6, 2026

This RFID face attendance system combines card-based identification with visual documentation using an ESP32-S3 microcontroller. The setup uses an MFRC522 RFID reader to scan attendance cards, displays status on a compact ST7735 screen, and provides audio feedback through a piezo buzzer. An optional camera module captures attendance moments for record-keeping.

The guide provides a complete wiring diagram showing connections between the RFID module, display, buzzer, indicator LED, and ESP32 development board. You'll receive a parts list with all components, step-by-step assembly instructions including proper GPIO mapping, and firmware code that separates camera and attendance controller functions across the system. The modular design keeps low-current components isolated and simplifies troubleshooting.

Wiring diagram

Wiring diagram for RFID Face Attendance System
Loading diagram…

Gather all the parts

QtyComponent
1

MFRC522 RFID Module

13.56 MHz RFID reader/writer module based on the NXP MFRC522 IC. Communicates over SPI and is commonly sold as an RC522 breakout with an onboard antenna.

1

ST7735 128x160 SPI screen (pictured 8-pin module)

ST7735 128x160 SPI screen (pictured 8-pin module)

1

Two-pin 3.3V active buzzer

Direct-drive option requires a self-oscillating 3.3V active piezo buzzer drawing no more than 5 mA. Verify its rated current before connecting; not a magnetic buzzer or passive piezo.

1

ESP32-S3-DevKitC-1 v1.1 attendance controller

Separately programmed attendance controller with original wiring.

1

LED

green

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

LED

yellow

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

100 ohm (backlight current limiting)

Through-hole resistor (current-limiting in series with an LED)

Assemble it in 6 steps

1. Disconnect USB before wiring

With both USB cables unplugged, connect Freenove 5V to the DevKitC pin marked 5V0 (the 5V/VIN supply input), and Freenove GND to DevKitC GND. Power only the Freenove from a stable USB supply; leave both DevKitC USB sockets unplugged during shared-power operation. Do not connect the 3V3 pins together. The diagram endpoint VIN is the physical DevKitC 5V0 pin. Camera data still travels over Wi-Fi through the server. DevKitC sends card requests to the PC, the PC fetches Freenove images and returns the result. Leave TX/RX serial pins unconnected; this firmware does not use a serial link between boards.

  • Before deploying to the DevKitC by USB, unplug all power and remove the 5V jumper. Deploy with DevKitC USB connected, then unplug it before restoring the jumper. Never connect DevKitC USB power and the shared 5V jumper together.
  • If either board resets during Wi-Fi or camera capture, disconnect the jumper and return to separate USB supplies. The Freenove USB path must supply both boards and all modules.

2. Connect the shared screen and card bus

Connect RC522 SCK and screen SCK to GPIO12 (clock). Connect RC522 MOSI and screen SDA to GPIO11 (data). RC522 MISO goes to GPIO13, SDA to GPIO10 (select), RST to GPIO14. Screen CS goes to GPIO15, A0/DC to GPIO16, RESET to GPIO17. Both VCC pins go to 3V3 and both GND pins to GND. Leave RC522 IRQ unused.

  • Connect DevKitC 5V0 through the 100 ohm resistor to screen LED (backlight power). Keep screen VCC on 3V3. Do not bypass the resistor because this module has no confirmed onboard backlight current limiting or direct-5V rating. The screen is assumed to be 128 by 160 pixels with black-tab initialization.
  • Never power RC522 from 5V.

3. Connect the low-current active piezo buzzer

With power unplugged, connect buzzer plus to DevKitC GPIO7 (beep output) and buzzer minus to DevKitC GND. GPIO7 HIGH sounds the buzzer. No transistor, base resistors or diode are used. Existing firmware beep timing is unchanged.

  • Direct wiring requires a 3.3V active piezo buzzer rated at no more than 5 mA. Check its specifications before powering it. A buzzer sounding correctly does not confirm safe current. Do not directly connect an unknown-current or magnetic buzzer; those require a driver.

4. Add one resistor per indicator

GPIO4 to the green 330 ohm resistor then green LED long leg. GPIO5 to the yellow resistor then yellow LED long leg. GPIO6 to the red resistor then red LED long leg. All short legs go to GND. Clear LED bodies do not identify their colour; test each one through its resistor.

5. Position the camera

With USB unplugged, seat the OV5640 ribbon fully in the Freenove camera socket and close its latch. The OV5640 connects through this ribbon, not through jumper wires. Mount the camera at face height with even front lighting. Keep card-reader wiring short and away from the camera ribbon.

6. Keep firmware roles separate

The selected build remains Freenove N16R8 camera firmware. Deploy only to the Freenove, not the DevKitC. The DevKitC keeps its existing firmware. All restored module GPIO connections belong to the DevKitC, not the Freenove. No upload is needed just to restore this diagram.

Review all connections

1. Connections between "rfid" and "ESP32"

FunctionrfidESP32
powerVCC → ESP32-S3-DevKitC-1 v1.1 attendance controller 3V3EXT
groundGND → ESP32-S3-DevKitC-1 v1.1 attendance controller GNDEXT
spiSCK → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO12EXT
spiMOSI → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO11EXT
spiMISO → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO13EXT
spiSDA → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO10EXT
digitalRST → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO14EXT

2. Connections between "screen" and "ESP32"

FunctionscreenESP32
powerVCC → ESP32-S3-DevKitC-1 v1.1 attendance controller 3V3EXT
groundGND → ESP32-S3-DevKitC-1 v1.1 attendance controller GNDEXT
spiSCK → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO12EXT
spiSDA → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO11EXT
spiCS → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO15EXT
digitalA0 → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO16EXT
digitalRESET → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO17EXT
powerLED → Resistor P2EXT

3. Connections between "r_bl" and "ESP32"

Functionr_blESP32
digitalP1 → ESP32-S3-DevKitC-1 v1.1 attendance controller 5V0EXT

4. Connections between "r_green" and "ESP32"

Functionr_greenESP32
digitalP1 → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO4EXT
digitalP2 → LED ANODEEXT

5. Connections between "green" and "ESP32"

FunctiongreenESP32
groundGND → ESP32-S3-DevKitC-1 v1.1 attendance controller GNDEXT

6. Connections between "r_yellow" and "ESP32"

Functionr_yellowESP32
digitalP1 → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO5EXT
digitalP2 → LED ANODEEXT

7. Connections between "yellow" and "ESP32"

FunctionyellowESP32
groundGND → ESP32-S3-DevKitC-1 v1.1 attendance controller GNDEXT

8. Connections between "r_red" and "ESP32"

Functionr_redESP32
digitalP1 → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO6EXT
digitalP2 → LED ANODEEXT

9. Connections between "red" and "ESP32"

FunctionredESP32
groundGND → ESP32-S3-DevKitC-1 v1.1 attendance controller GNDEXT

10. Connections between "terminal" and "ESP32"

FunctionterminalESP32
power5V05V
groundGNDGND

11. Connections between "buzzer" and "ESP32"

FunctionbuzzerESP32
powerPLUS → ESP32-S3-DevKitC-1 v1.1 attendance controller GPIO7EXT
digitalMINUS → ESP32-S3-DevKitC-1 v1.1 attendance controller GNDEXT

Deploy the firmware

// Diagram restored: external modules remain on the separate DevKitC; camera GPIO mapping is unchanged.
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include "config.h"
#if CAMERA_NODE
#include "esp_camera.h"
#include <WebServer.h>
// Freenove ESP32S3_EYE ribbon mapping; reset and power-down are not GPIO-controlled.
WebServer web(80);
bool ready=false;
void setup(){
 Serial.begin(115200);
 camera_config_t c={};
 c.ledc_channel=LEDC_CHANNEL_0;c.ledc_timer=LEDC_TIMER_0;
 c.pin_d0=11;c.pin_d1=9;c.pin_d2=8;c.pin_d3=10;c.pin_d4=12;c.pin_d5=18;c.pin_d6=17;c.pin_d7=16;
 c.pin_xclk=15;c.pin_pclk=13;c.pin_vsync=6;c.pin_href=7;
 c.pin_sccb_sda=4;c.pin_sccb_scl=5;c.pin_pwdn=-1;c.pin_reset=-1;
 c.xclk_freq_hz=10000000;c.pixel_format=PIXFORMAT_JPEG;c.frame_size=FRAMESIZE_VGA;
 c.jpeg_quality=10;c.fb_count=1;c.fb_location=CAMERA_FB_IN_PSRAM;c.grab_mode=CAMERA_GRAB_WHEN_EMPTY;
 ready=psramFound() && esp_camera_init(&c)==ESP_OK;
 if(ready){auto s=esp_camera_sensor_get();s->set_hmirror(s,1);s->set_vflip(s,0);s->set_brightness(s,1);s->set_saturation(s,0);s->set_ae_level(s,-3);}
 else Serial.println("Camera failed: check ribbon, OV5640 support and PSRAM");
 WiFi.begin(WIFI_SSID,WIFI_PASSWORD);
 const char* headers[]={"X-Key"};web.collectHeaders(headers,1);
 web.on("/capture",HTTP_GET,[]{
  if(String(API_KEY).startsWith("CHANGE_ME") || web.header("X-Key")!=API_KEY){web.send(403);return;}
  if(!ready){web.send(503);return;}
  // Discard old buffered frame so this photograph belongs to the current request.
  auto old=esp_camera_fb_get();if(old)esp_camera_fb_return(old);
  auto f=esp_camera_fb_get();if(!f){web.send(503);return;}
  web.sendHeader("Cache-Control","no-store");web.setContentLength(f->len);web.send(200,"image/jpeg","");
  auto client=web.client();size_t sent=0;unsigned long start=millis();
  while(sent<f->len && client.connected() && millis()-start<5000){size_t n=client.write(f->buf+sent,min((size_t)1460,f->len-sent));if(!n)break;sent+=n;}
  esp_camera_fb_return(f);
 });web.begin();
}
void loop(){web.handleClient();static bool shown=false;if(WiFi.status()==WL_CONNECTED&&!shown){Serial.println(WiFi.localIP());shown=true;}delay(2);}
#else
#include <SPI.h>
#include <Wire.h>
#include <MFRC522.h>
#include <RTClib.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
// Confirmed terminal wiring: SPI 12/13/11; RC522 CS10/RST14; TFT CS15/DC16/RST17.
// GPIO15 is software-controlled chip select, not a hardware SPI signal.
MFRC522 rfid(10,14);RTC_DS3231 rtc;Adafruit_ST7735 tft(15,16,17);
bool clockOK=false,readerOK=false;String lastText;uint32_t lastScan=0;
void show(String text,int led=0){
 digitalWrite(4,led==4);digitalWrite(5,led==5);digitalWrite(6,led==6);
 if(lastText==text)return;lastText=text;tft.fillScreen(ST77XX_BLACK);tft.setCursor(0,8);tft.setTextColor(ST77XX_WHITE);tft.setTextSize(1);tft.println(text);Serial.println(text);
}
String post(const char* path,const String& body){
 if(WiFi.status()!=WL_CONNECTED)return "";
 HTTPClient http;http.begin(String(SERVER_URL)+path);http.setTimeout(12000);http.addHeader("X-Key",API_KEY);http.addHeader("Content-Type","text/plain");
 int status=http.POST(body);String result=status==200?http.getString():"";http.end();result.trim();return result;
}
void result(String text,int led){show(text,led);bool ok=led==4;digitalWrite(7,HIGH);delay(ok?120:400);digitalWrite(7,LOW);delay(2200);}
void setup(){
 Serial.begin(115200);for(int p:{4,5,6,7,10,15}){pinMode(p,OUTPUT);digitalWrite(p,(p==10||p==15)?HIGH:LOW);}
 SPI.begin(12,13,11);tft.initR(INITR_BLACKTAB);tft.setRotation(1);
 Wire.begin(8,9);clockOK=rtc.begin();
 // Keep the display deselected while starting the other device on this bus.
 digitalWrite(15,HIGH);
 for(int attempt=0;attempt<3 && !readerOK;attempt++){
  rfid.PCD_Init();delay(100);
  byte v=rfid.PCD_ReadRegister(MFRC522::VersionReg);
  Serial.printf("RC522 startup attempt %d: VersionReg=0x%02X (CS10 RST14 SCK12 MISO13 MOSI11)\n",attempt+1,v);
  readerOK=v!=0 && v!=255;
  if(!readerOK)delay(200);
 }
 WiFi.begin(WIFI_SSID,WIFI_PASSWORD);show("Connecting Wi-Fi",5);
}
void loop(){
 if(String(WIFI_SSID)=="CHANGE_ME" || String(API_KEY).startsWith("CHANGE_ME")){show("Set include/config.h\nbefore Deploy",5);delay(100);return;}
 if(WiFi.status()!=WL_CONNECTED){show("Wi-Fi disconnected\nNo attendance saved",5);delay(100);return;}
 if(!clockOK){show("RTC module missing\nCheck wiring",6);delay(200);return;}
 if(!readerOK){show("RFID reader missing\nCheck 3.3V / wiring",6);delay(200);return;}
 static uint32_t syncAt=0;
 if(clockOK && (!syncAt || millis()-syncAt>3600000UL)){
  String epoch=post("/time","");unsigned long n=strtoul(epoch.c_str(),nullptr,10);
  if(n>1700000000UL){rtc.adjust(DateTime(n));syncAt=millis();}
 }
 if(rtc.lostPower() || !syncAt){show("Time sync unavailable\nCheck server / key",5);delay(200);return;}
 show("Scan student card");
 if(millis()-lastScan<3000 || !rfid.PICC_IsNewCardPresent())return;
 if(!rfid.PICC_ReadCardSerial()){lastScan=millis();result("Card read failed\nTry again",6);return;}
 String uid;for(byte i=0;i<rfid.uid.size;i++){if(rfid.uid.uidByte[i]<16)uid+='0';uid+=String(rfid.uid.uidByte[i],HEX);}uid.toUpperCase();
 rfid.PICC_HaltA();rfid.PCD_StopCrypto1();lastScan=millis();Serial.println("Card UID: "+uid);
 show("Card read successfully\nChecking registration...",4);String session=post("/card",uid);
 if(!session.startsWith("SESSION:")){result(session=="DENIED"?"Card not registered":"Server unavailable",session=="DENIED"?6:5);return;}
 show("Card accepted\nLook at camera\nHold still...",4);delay(1800);
 String reply=post("/verify",session.substring(8)+"\n"+String(rtc.now().unixtime()));
 if(reply=="ACCEPT" || reply=="DUPLICATE")result(reply=="ACCEPT"?"Face matched\nAttendance recorded":"Face matched\nAlready recorded",4);
 else if(reply=="REJECT")result("Face not matched",6);
 else if(reply=="CAMERA_ERROR")result("Camera module error\nCheck camera / ribbon",6);
 else result("Server/connection error\nNot recorded",5);
}
#endif

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik