项目概述
ESP32 NFC 考勤打卡系统是一个基于 ESP32 微控制器、RC522 RFID 读卡模块和 ST7789 TFT 彩屏的智能考勤解决方案。系统支持 NFC 卡片刷卡签到/签退、实时时钟显示、Wi-Fi 联网、Web 管理后台、卡片注册管理、考勤记录查询等完整功能,适用于中小企业、实验室、兴趣小组等场景的日常考勤管理。本项目通过 SPI 总线共享技术,RC522 读卡模块和 ST7789 显示屏共用同一 SPI 总线协同工作。考勤数据和卡片信息通过 ESP32 的 NVS(Non-Volatile Storage)持久化存储,断电不丢失。配套的 Web 管理界面提供卡片管理、实时考勤状态查看等操作。
项目亮点
- 稳定可靠的 SPI 总线共享方案:RC522 与 ST7789 共用 SPI 总线,通过 SPI.end()/SPI.begin() 复位机制解决冲突
- NVS 持久化存储:卡片信息和打卡记录全部保存在 ESP32 的 NVS 分区,无需外部存储芯片
- Web 管理后台:内建 Web 服务器,提供美观的响应式管理界面,支持卡片增删改查和实时考勤状态
- NTP 自动校时:连接 Wi-Fi 后自动从阿里云 NTP 服务器同步北京时间
- 去重打卡逻辑:同一张卡每天只能签到一次、签退一次,避免重复打卡
- 刷卡注册模式:管理员模式下可直接刷卡注册新卡,注册完成后自动退出管理模式
- 彩屏实时反馈:刷卡成功/失败/重复打卡时,屏幕显示对应动画和提示信息
- Web API 鉴权保护:写接口采用 X-Auth-Token 头部鉴权,防止未授权访问
项目难点及解决方案
难点一:SPI 总线共享冲突
RC522 和 ST7789 共用 ESP32 的 SPI 总线(SCK=GPIO18, MOSI=GPIO23, MISO=GPIO19)。TFT_eSPI 库初始化后会将 SPI 频率设置为屏幕的高速模式(40MHz),而 RC522 只能工作在较低频率(约 5MHz),且 SPI Mode 不同。直接调用 RC522 读寄存器会返回 0x00。
解决方案:
在 tft.init() 之后、rfid.PCD_Init() 之前,执行 SPI.end() 再 SPI.begin() 复位 SPI 总线,使 RC522 能从默认低速模式重新初始化。MFRC522 库内部使用 beginTransaction() 自行管理 SPI 配置,每次读卡时自动适配 RC522 的工作频率。
难点二:NVS 持久化的指针悬空问题
早期版本使用 #define NVS_CARD_KEY(i) (String("c") + (i)).c_str() 获取 NVS 键名,但 String 对象是临时变量,表达式结束后立即被销毁,导致返回的 const char* 指针指向已释放的内存,Preferences 读取时造成 IllegalInstruction 崩溃。
解决方案:
改用静态 char 缓冲区 + snprintf 固定写入键名,返回静态存储区的指针,生命周期与程序相同,不会悬空:
cpp
static char nvsKeyBuf[8];
static const char* nvsCardKey(int i) {
snprintf(nvsKeyBuf, sizeof(nvsKeyBuf), "c%d", i);
return nvsKeyBuf;
}
难点三:JavaScript 模板字符串转义
Web 管理界面的 HTML/JS 代码由 C++ 的 F() 宏包裹的 String 拼接而成。中文引号或 JS 模板字符串中的单引号容易与 C++ 字符串定界符冲突,导致编译错误或 JS 语法错误。例如原版 onclick 中的 '+'c.uid+'' 被当成了字符串字面量而非变量。
解决方案:
采用 data-* 属性 + querySelectorAll 事件委托模式替代内联 onclick 中的模板字符串拼接,避免多层引号嵌套的转义问题。同时移除 F() 字符串中所有的 JS 行注释 //,因为 F() 拼接不保留换行,会导致后续代码被注释掉。
目录
[1.1 硬件清单](#1.1 硬件清单)
[1.2 接线方案](#1.2 接线方案)
[1.3 硬件连接图](#1.3 硬件连接图)
[1.4 实物连接图](#1.4 实物连接图)
[2.1 系统初始化 (setup)](#2.1 系统初始化 (setup))
[2.2 主循环逻辑 (loop)](#2.2 主循环逻辑 (loop))
[3.1 数据结构设计](#3.1 数据结构设计)
[3.2 NVS 持久化](#3.2 NVS 持久化)
[3.3 考勤处理逻辑](#3.3 考勤处理逻辑)
[3.4 Web 管理界面](#3.4 Web 管理界面)
[3.5 屏幕显示与蜂鸣器](#3.5 屏幕显示与蜂鸣器)
[4.1 操作步骤](#4.1 操作步骤)
[4.2 演示视频](#4.2 演示视频)
[5.1 工作原理](#5.1 工作原理)
[5.2 工作模式配置](#5.2 工作模式配置)
[Q1: RC522 读取寄存器始终返回 0x00](#Q1: RC522 读取寄存器始终返回 0x00)
[Q2: 烧录后出现 IllegalInstruction 或 Guru Meditation 错误](#Q2: 烧录后出现 IllegalInstruction 或 Guru Meditation 错误)
[Q3: Web 管理页面无法访问](#Q3: Web 管理页面无法访问)
[Q4: Web 接口返回 401 Unauthorized](#Q4: Web 接口返回 401 Unauthorized)
[Q5: 打卡记录丢失](#Q5: 打卡记录丢失)
[Q6: 删除卡片时提示 "Not found"](#Q6: 删除卡片时提示 "Not found")
一、硬件系统部分
1.1 硬件清单
构建本系统需要以下硬件组件:
|------------|-----------------|------------|--------------------------|
| 序号 | 组件 | 数量 | 备注 |
| 1 | 零知ESP32 开发板 | 1 | 写入代码 |
| 2 | 零知ESP32扩展板 | 1 | 方便接线 |
| 3 | RC522 RFID 读卡模块 | 1 | MFRC522 芯片,13.56MHz |
| 4 | ST7789 TFT 显示屏 | 1 | 240×240 分辨率,SPI 接口 |
| 5 | 无源蜂鸣器 | 1 | 5V 有源或无源均可(无源音色更丰富) |
| 6 | NFC 标签/IC卡 | 若干 | Mifare Classic 1K S50 兼容 |
| 7 | 杜邦线 | 若干 | 母对母、公对母各若干 |
| 8 | Micro USB 数据线 | 1 | 供电和程序烧录 |
1.2 接线方案
以下是各模块与 ESP32 的详细接线对照表:
RC522 读卡模块接线
|------------------|--------------------|----------------|
| RC522 引脚 | ESP32 GPIO | 说明 |
| SDA (SS) | GPIO5 | SPI 片选 |
| SCK | GPIO18 | SPI 时钟线(与屏幕共用) |
| MOSI | GPIO23 | SPI 数据线(与屏幕共用) |
| MISO | GPIO19 | SPI 数据线(主机输入) |
| RST | GPIO27 | RC522 复位 |
| VCC | 3.3V | 供电(不可接 5V) |
| GND | GND | 共地 |
蜂鸣器接线
|---------------|--------------------|
| 蜂鸣器引脚 | ESP32 GPIO |
| 正极 (+) | GPIO25 |
| 负极 (-) | GND |
1.3 硬件连接图

1.4 实物连接图

二、软件架构设计
系统软件遵循 初始化 (setup) → 主循环 (loop) 的经典嵌入式架构。整体代码分为七大模块:初始化模块、SPI 总线管理模块、NFC 读卡模块、显示模块、Web 服务器模块、NVS 持久化模块以及蜂鸣器模块。
2.1 系统初始化 (setup)
系统启动时,setup() 函数按照严格的先后顺序执行以下初始化步骤:
- 蜂鸣器初始化:调用 setupBuzzer(),根据 ESP32版本兼容性配置 LEDC PWM 通道
- 屏幕初始化:调用 tft.init() + setRotation(3),显示启动动画
- SPI 总线初始化:调用 SPI.begin() 启用 SPI,拉高 RC522 片选引脚避免误选中
- SPI 复位(关键修复):先 SPI.end() 再 SPI.begin(),清除 TFT_eSPI 设置的高速 SPI 配置
- RC522 初始化:调用 rfid.PCD_Init(),读取版本寄存器验证通信是否正常
- NVS 数据加载:调用 loadCardsFromNVS() 和 loadRecordsFromNVS() 读取持久化的卡片和记录
- Wi-Fi 连接:调用 connectWiFi(),最多尝试 20 秒
- NTP 校时:如果 Wi-Fi 连接成功,调用 syncNTP() 同步网络时间
- Web 服务器:调用 setupWebServer() 注册 HTTP 路由并启动
- 显示就绪界面:showReadyScreen() + playStartupSound()
2.2 主循环逻辑 (loop)
loop() 函数是系统的主循环,每秒执行数十次,主要处理以下任务:
- Web 请求处理:server.handleClient() 处理 HTTP 请求
- 时钟刷新:updateReadyClock() 每秒局部刷新屏幕上的时间显示
- Wi-Fi 状态检测:每 30 秒检查一次 Wi-Fi 连接状态,断开则自动重连
- 异步音效播放:管理模式触发的提示音由主循环异步播放,避免阻塞 Web 处理线程
- 管理模式超时检测:30 秒无操作自动退出管理模式
- 刷卡检测:MFRC522 库的 PICC_IsNewCardPresent() + PICC_ReadCardSerial() 检测 NFC 卡片
- 防抖处理:3 秒内重复刷卡被忽略
- 考勤处理:已注册卡执行签到/签退逻辑;未注册卡在管理模式下自动注册,否则显示未知卡
三、代码拆分讲解
以下按照功能模块逐一讲解核心代码的设计思路和关键实现。
3.1 数据结构设计
系统定义了两种核心数据结构:
cpp
struct CardInfo {
char uid[CARD_UID_LEN]; // 卡片 UID,如 "C942C66D"
char name[32]; // 持卡人姓名
bool registered; // 是否已注册
};
struct AttendanceRecord {
char uid[CARD_UID_LEN]; // 卡片 UID
char name[32]; // 持卡人姓名
char timeStr[32]; // 打卡时间 "YYYY-MM-DD HH:MM:SS"
bool checkedIn; // true=签到, false=签退
};
固定数组存储避免了动态内存分配带来的碎片问题,MAX_CARDS=50 和 MAX_RECORDS=100 满足中小规模考勤需求。
3.2 NVS 持久化
Preferences 库将数据存储在 ESP32 的 NVS 分区中。卡片和记录分别以 "c0"-"c49" 和 "r0"-"r99" 为键,值为 "UID|NAME|1" 格式的管道分隔字符串。总条数分别存储于 "ccount" 和 "rcount" 键中。
首次启动时(NVS 无数据),loadCardsFromNVS() 会写入三张默认演示卡片(Zhang San, Li Si, Wang Wu)以便快速验证系统功能。
3.3 考勤处理逻辑
handleAttendance() 函数实现了完整的考勤处理流程:
- 获取当前时间字符串和日期字符串(NTP 同步的北京时间)
- 遍历所有记录,判断本卡今日是否已签到/已签退
- 规则:未签到则签到,已签到则签退;若已签到又试图签到则判为重复打卡
- 记录存储采用环形覆盖策略:达 MAX_RECORDS 上限时整体前移,新记录追加到末尾
- 每次考勤操作后自动保存到 NVS,确保断电不丢失
3.4 Web 管理界面
generateDashboardHTML() 函数动态生成完整的 Web 管理页面,包含卡片管理、考勤记录、今日出勤状态三大功能模块。前端通过 fetch API 与后端 REST 接口通信:
- GET /api/cards - 获取已注册卡片列表
- GET /api/records - 获取考勤记录
- POST /api/cards - 添加新卡片(需鉴权)
- POST /api/cards/delete - 删除卡片(需鉴权)
- POST /api/cards/rename - 重命名卡片(需鉴权)
- GET /api/admin - 启用管理模式(需鉴权)
- GET /api/admin/newcard - 轮询新注册卡通知
3.5 屏幕显示与蜂鸣器
系统通过 ST7789 彩屏提供丰富的视觉反馈:就绪界面显示 Wi-Fi 状态、IP 地址和实时时钟;打卡成功显示绿色签到动画或橙色签退动画;错误时显示红色错误提示。蜂鸣器提供四种不同的提示音:开机音、成功音、错误音和重复打卡的三短音。
四、操作过程及数据展示
4.1 操作步骤
首次使用
步骤 1:按照接线方案连接硬件,确保所有模块供电正常。
步骤 2:修改 ESP32-NFC.ino 中的 Wi-Fi 凭据(WIFI_SSID 和 WIFI_PASSWORD)。
步骤 3:修改 AUTH_TOKEN 为一个随机字符串,用于保护 Web 写接口。
步骤 4:上传代码到 ESP32,打开串口监视器(115200bps)观察启动日志。
步骤 5:确认 "RC522 固件版本" 和 "Wi-Fi OK" 信息,系统就绪后屏幕显示 "Tap Your Card"。
刷卡打卡(签到/签退)
将已注册的 NFC 卡片贴近 RC522 天线区域。系统将自动识别:若今日未签到则执行签到(屏幕显示绿色 "CHECK IN");若今日已签到则执行签退(屏幕显示橙色 "CHECK OUT")。若当日已签到和签退完毕,再次刷卡显示 "Already Done" 提示。每次有效打卡均伴有蜂鸣器提示音。
注册新卡片(管理模式)
方法一(管理员模式刷卡注册):
在 Web 管理页面点击 "管理模式" 按钮,然后在 30 秒内将新卡片贴近 RC522,系统自动注册并弹出改名对话框。注册完成后自动退出管理模式。
方法二(Web 页面手动添加):
在 "注册卡片管理" 区域输入卡片 UID(如 A1B2C3D4)和持卡人姓名,点击 "添加" 按钮。卡片 UID 可通过串口监视器在刷卡时获得。
查看考勤数据
在浏览器中访问 ESP32 的 IP 地址(http://192.168.x.x)。管理界面包含四个统计卡片(注册卡片数、打卡记录数、今日签到数、今日签退数)、卡片管理表格、今日出勤状态(未签到/未签退列表)以及考勤记录表格。页面每 10 秒自动刷新打卡记录。
删除卡片
在 "注册卡片管理" 表格中找到目标卡片,点击 "删除" 按钮,确认后即可删除。删除采用 swap-and-decrement 策略,确保卡片容量不泄漏。
4.2 演示视频
零知ESP32--NFC考勤打卡系统
五、技术原理
5.1 工作原理
系统的工作流程可以概括为:读卡 → 识别 → 处理 → 反馈。当 NFC 卡片靠近 RC522 天线时,RC522 通过 13.56MHz 射频场与卡片通信,读取卡片 UID(唯一标识符)。ESP32 通过 SPI 总线读取该 UID,然后在已注册卡片列表中查找匹配项。如果找到匹配项,系统根据当前时间和历史打卡记录判定是签到还是签退,并记录打卡信息。如果未找到匹配项,系统根据是否处于管理模式决定是自动注册还是显示未知卡错误。
打卡记录包含卡号、持卡人姓名、打卡时间(格式 YYYY-MM-DD HH:MM:SS)和打卡类型(签到/签退)。所有记录保存至 NVS 持久化存储,并通过 Web 接口向用户展示。
5.2 工作模式配置
普通模式(默认)
上电即进入此模式。屏幕显示就绪界面(Wi-Fi 状态、IP 地址、实时时钟),底部显示 "Tap Your Card"。已注册的卡片刷卡可正常签到/签退,未注册的卡片将被忽略并显示 "Unknown Card" 错误。
管理模式
通过 Web 管理页面 "启用管理模式" 按钮(或直接调用 GET /api/admin)激活。屏幕显示紫色管理界面,顶部有 "A" 标志。在 30 秒内刷卡可将新卡片自动注册到系统,注册完成后自动退出管理模式返回就绪界面。如果 30 秒内无刷卡,自动超时退出。
自检模式(调试用)
取消代码头部的 // #define TEST_RC522_ONLY 注释并重新上传,固件将跳过屏幕/Web/NVS 等所有模块,只反复读取 RC522 版本号并尝试读卡。此模式用于单独排查 RC522 硬件接线问题,排除 SPI 总线共享冲突因素。
SPI 总线共享机制
ST7789 和 RC522 共用 SPI 总线是本次项目最大的技术挑战。两者的 SPI 协议差异如下:
|------------|-------------------------|-------------------------|
| 参数 | ST7789 (屏幕) | RC522 (读卡器) |
| SPI Mode | Mode 3 (CPOL=1, CPHA=1) | Mode 0 (CPOL=0, CPHA=0) |
| 最大频率 | 40 MHz | ~5 MHz |
| 数据长度 | 8/9/16/18 bit 可变 | 8 bit 固定 |
| 片选逻辑 | 低电平有效 (CS=GPIO15) | 低电平有效 (SS=GPIO5) |
MFRC522 库在每次 SPI 事务开始时调用 beginTransaction() 动态设置 RC522 所需的工作参数,事务结束后调用 endTransaction() 释放总线。这使得两个设备可以在同一总线上共存而互不干扰。核心修复在于初始化阶段执行 SPI.end() 再 SPI.begin() 复位总线,让 RC522 的 PCD_Init() 能在干净的 SPI 环境下配置自己的寄存器。
六、常见问题指引
Q1: RC522 读取寄存器始终返回 0x00
这是最常见的问题,通常由 SPI 总线冲突引起。排查步骤:
- 启用 TEST_RC522_ONLY 模式验证 RC522 硬件是否正常(排除屏幕干扰)
- 确认接线:SDA=GPIO5, SCK=GPIO18, MOSI=GPIO23, MISO=GPIO19, RST=GPIO27
- 确认 RC522 供电为 3.3V(不能接 5V)
- 检查 MISO 线是否漏接(漏接会始终读回 0x00)
- 检查所有 GND 是否共地
- 尝试更换杜邦线或 RC522 模块
Q2: 烧录后出现 IllegalInstruction 或 Guru Meditation 错误
此错误通常由以下原因导致:
- 更改分区表后未擦除旧数据:切换分区方案(如 huge_app → default)后,需使用 esptool erase-flash 擦除整个 flash,然后重新烧录
- NVS 指针悬空:确保 nvsCardKey() 和 nvsRecordKey() 使用静态缓冲区返回 const char*,而非临时 String 对象的 c_str()
- 命令:esptool.py --port COMx erase_flash(Windows 下端口号请根据实际情况修改)
Q3: Web 管理页面无法访问
排查步骤:
- 确认 ESP32 已成功连接 Wi-Fi(屏幕显示 Wi-Fi: OK 和 IP 地址)
- 确认电脑/手机与 ESP32 在同一局域网
- 在浏览器中直接输入 IP 地址(不要加 https://)
- 检查串口日志中是否有 Web 服务器启动的相关信息
Q4: Web 接口返回 401 Unauthorized
写接口(添加/删除卡片、管理模式)需要鉴权。请确保请求头中包含 X-Auth-Token,其值等于代码中 AUTH_TOKEN 常量的值。浏览器管理页面已自动处理鉴权,不需要额外操作。如果使用 curl 或 Postman 等工具调用 API,请添加 -H "X-Auth-Token: change-this-token" 请求头。
******Q5:******打卡记录丢失
NVS 存储有容量限制(约 15KB)。系统最多存储 100 条打卡记录,超出时旧记录会被覆盖。如有长期归档需求,建议通过 Web API 定期导出数据到外部服务器。在代码中设置 SERVER_HOST 可启用自动上传功能。
Q6: 删除卡片时提示 "Not found"
删除请求的 UID 必须与存储的 UID 完全一致(包括大小写)。系统存储时自动将 UID 转换为大写,因此删除时也需传入大写 UID。Web 管理页面已处理此逻辑,手动调用 API 时需自行转换。