✔零知IDE 是一个真正属于国人自己的开源软件平台,在开发效率上超越了Arduino平台并且更加容易上手,大大降低了开发难度。零知开源在软件方面提供了完整的学习教程和丰富示例代码,让不懂程序的工程师也能非常轻而易举的搭建电路来创作产品,测试产品。快来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
目录
[1.1 硬件清单](#1.1 硬件清单)
[1.2 接线方案表](#1.2 接线方案表)
[1.3 系统接线图](#1.3 系统接线图)
[1.4 连接实物图](#1.4 连接实物图)
[2.1 硬件引脚定义](#2.1 硬件引脚定义)
[2.2 卡片数据读写流程](#2.2 卡片数据读写流程)
[2.3 按键处理人机交互](#2.3 按键处理人机交互)
[2.4 超时自动返回机制](#2.4 超时自动返回机制)
[2.5 完整代码架构](#2.5 完整代码架构)
[3.1 人机交互流程](#3.1 人机交互流程)
[3.2 视频演示](#3.2 视频演示)
[4.1 工作原理](#4.1 工作原理)
[4.2 核心寄存器](#4.2 核心寄存器)
[4.3 软件SPI通信](#4.3 软件SPI通信)
[Q1---RFID 无法读卡或认证失败?](#Q1—RFID 无法读卡或认证失败?)
(1)项目概述
本项目以 STM32F103RBT6 零知标准板 为主控,搭配 RFID-RC522 射频 IC 卡识别模块 核心器件,结合 2.0 寸 ST7789 彩色 LCD 屏和 4×4 矩阵按键,打造了一套功能完整、交互友好的校园餐卡系统。系统支持卡片初始化、金额充值、消费支付、余额查询、超时自动复位等核心功能,同时通过硬件 SPI 与软件 SPI 分离设计,解决了多设备通信冲突问题,保障了系统稳定性。
(2)项目难点及解决方案
*难点一:RFID 读卡稳定性差,易出现 "刷卡无响应""掉卡" 问题
&解决方案: 采用先设置金额后扫描卡片的流程设计,将金额输入与卡片操作分离。
cpp
// 金额输入确认后再进入等待卡片模式
void processAmountInput() {
// ... 金额验证逻辑
pendingAmount = amount;
pendingOperation = (currentMode == MODE_RECHARGE_INPUT) ? "充值" : "消费";
currentMode = MODE_WAITING_CARD; // 先设置金额,再等待放卡
displayWaitingCardScreen();
}
*难点二:共享硬件 SPI 会导致通信冲突和刷新率下降
**&解决方案:**使用 GPIO 引脚模拟 SPI 时序重写 RFID 库通信函数,ST7789 屏幕使用硬件 SPI屏幕解决刷新卡顿
*难点三:中文显示与 FLASH 资源占用平衡
&解决方案: 选用 U8g2 精简中文字体 u8g2_font_unifont_t_chinese3 ,占用 ROM<8KB
一、硬件系统设计
1.1 硬件清单
| 器件名称 | 型号规格 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | STM32F103RBT6(零知标准板) | 1 块 | 核心控制单元 |
| RFID 射频模块 | RC522(SPI 接口) | 1 块 | 非接触式 IC 卡读写 |
| TFT显示屏 | 2.0 寸 ST7789(240×320) | 1 块 | 人机交互显示 |
| 矩阵键盘 | 4×4 薄膜按键模块 | 1 块 | 功能选择与金额输入 |
| 非接触式 IC 卡 | M1 卡(13.56MHz) | 1-2 张 | 餐卡载体(支持初始化) |
| 杜邦线 | 公对母 / 公对公 | 20 根 + | 模块间接线 |
1.2 接线方案表
(1)ST7789 屏幕与 零知标准板接线
| ST7789 屏幕引脚 | 功能描述 | 零知标准板 引脚 | 备注 |
|---|---|---|---|
| CS | 片选(低有效) | 10 | 硬件 SPI 片选 |
| MOSI | 串行数据输入 | 11 | 硬件 SPI_MOSI(SPI1_MOSI) |
| SCK | 串行时钟 | 13 | 硬件 SPI_SCK(SPI1_SCK) |
| RST | 复位引脚 | 28 | 高电平有效,可接 VCC(默认) |
| DC | 数据 / 命令选择 | 26 | 高 = 数据,低 = 命令 |
(2)RFID-RC522 模块与 零知标准板接线(软件 SPI)
| RC522 引脚 | 功能描述 | 零知标准板 引脚 | 备注 |
|---|---|---|---|
| CS | 片选(NSS) | 9 | 软件 SPI 片选,低有效 |
| NRSTPD | 复位 / 掉电控制 | 5 | 高电平正常工作,低电平掉电 |
| SCK | 串行时钟 | 6 | 软件 SPI 时钟 |
| MOSI | 主机输出 / 从机输入 | 4 | 软件 SPI 数据发送 |
| MISO | 主机输入 / 从机输出 | 3 | 软件 SPI 数据接收 |
**注意:**ST7789屏幕与 RFID-RC522模块采用3.3V供电
(3)4×4 矩阵按键与 零知标准板接线
| 矩阵按键引脚 | 功能描述(行 / 列) | 零知标准板 引脚 | 备注 |
|---|---|---|---|
| R1 | 行 1 | 36 | 按键行引脚(下拉输入) |
| R2 | 行 2 | 35 | 按键行引脚(下拉输入) |
| R3 | 行 3 | 34 | 按键行引脚(下拉输入) |
| R4 | 行 4 | 33 | 按键行引脚(下拉输入) |
| C1 | 列 1 | 32 | 按键列引脚(上拉输出) |
| C2 | 列 2 | 31 | 按键列引脚(上拉输出) |
| C3 | 列 3 | 30 | 按键列引脚(上拉输出) |
| C4 | 列 4 | 29 | 按键列引脚(上拉输出) |
按照表格接线顺序依次将零知标准板引脚36-29从左到右接到4×4矩阵薄膜按键的引脚
1.3 系统接线图

RFID-RC522 模块、ST7789 显示屏和矩阵键盘通过杜邦线连接到零知标准板的指定 GPIO 引脚,注意电源和共地连接
1.4 连接实物图

二、代码解析部分
代码整体架构

2.1 硬件引脚定义
cpp
// ST7789屏幕配置(硬件SPI)
#define TFT_CS 10 // 片选引脚
#define TFT_MOSI 11 // 硬件SPI_MOSI(SPI1_MOSI)
#define TFT_SCK 13 // 硬件SPI_SCK(SPI1_SCK)
#define TFT_RST 28 // 复位引脚
#define TFT_DC 26 // 数据/命令选择
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
// RFID配置(软件SPI)
RFID rfid(9, 5, 6, 4, 3); // 参数顺序:CS, NRSTPD, SCK, MOSI, MISO
#define MEAL_CARD_SECTOR 1 // 餐卡数据存储扇区(M1卡共16个扇区)
#define MEAL_CARD_BLOCK 1 // 扇区内数据块(每个扇区4个块:0-3)
// 4×4矩阵按键配置
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
{'1','2','3','A'}, // A: 充值
{'4','5','6','B'}, // B: 消费
{'7','8','9','C'}, // C: 查询余额
{'*','0','#','D'} // D: 系统复位
};
byte rowPins[ROWS] = {36, 35, 34, 33}; // 行引脚
byte colPins[COLS] = {32, 31, 30, 29}; // 列引脚
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
M1 卡选择第 1 扇区第 1 块存储数据;RFID 的软件 SPI 引脚通过
RFID rfid(CS, NRSTPD, SCK, MOSI, MISO)自定义
2.2 卡片数据读写流程
充值 功能的**核心流程:**检测卡片→读卡号→选卡→密钥认证→读数据块→修改余额→写回数据块→停卡
cpp
void executeRecharge(unsigned long amount) {
systemReady = false;
// 1. 检测卡片是否存在(RC522发射射频场,卡片感应后返回响应)
if (!rfid.isCard()) {
displayErrorScreen("未检测到卡片");
delay(1500);
resetToIdle();
return;
}
// 2. 读取卡片序列号(UID)
if (!rfid.readCardSerial()) {
displayErrorScreen("读卡失败");
delay(1500);
resetToIdle();
return;
}
// 3. 选卡(通过UID选中卡片,建立通信)
if (rfid.selectTag(rfid.serNum) == 0) {
displayErrorScreen("选卡失败");
rfid.halt(); // 停卡,关闭射频场
delay(1500);
resetToIdle();
return;
}
// 4. 密钥认证(使用密钥A认证扇区1的块1,获取读写权限)
unsigned char blockAddr = MEAL_CARD_SECTOR * 4 + MEAL_CARD_BLOCK; // 块地址=1*4+1=5
if (rfid.auth(PICC_AUTHENT1A, blockAddr, defaultKeyA, rfid.serNum) != MI_OK) {
displayErrorScreen("密钥认证失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
// 5. 读取数据块(块1的16字节数据)
unsigned char cardData[16];
if (rfid.read(blockAddr, cardData) != MI_OK) {
displayErrorScreen("读数据失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
// 6. 验证是否为餐卡(检测前4字节是否为"MEAL")
if (!isMealCard(cardData)) {
// 非餐卡,初始化卡片(写入"MEAL"标识,余额设为0)
if (!initializeMealCard(blockAddr)) {
displayErrorScreen("卡片初始化失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
// 重新读取初始化后的数据
rfid.read(blockAddr, cardData);
}
// 7. 读取当前余额(卡片数据的第8-11字节存储余额,4字节无符号长整型)
unsigned long oldBalance = getBalanceFromCard(cardData);
unsigned long newBalance = oldBalance + amount; // 充值后余额
// 8. 写入新余额到数据块
setBalanceToCard(cardData, newBalance);
if (rfid.write(blockAddr, cardData) == MI_OK) {
// 写入成功,显示结果
tft.fillScreen(COLOR_BG);
u8g2_for_adafruit_gfx.setCursor(80, 80);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_SUCCESS);
u8g2_for_adafruit_gfx.print("充值成功");
u8g2_for_adafruit_gfx.setCursor(20, 140);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("新余额: ");
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_AMOUNT);
u8g2_for_adafruit_gfx.print(newBalance / 100.0, 2); // 转换为元(保留2位小数)
u8g2_for_adafruit_gfx.print("元");
delay(2000);
} else {
displayErrorScreen("充值失败");
delay(1500);
}
// 9. 停卡(关闭卡片通信,释放射频场)
rfid.halt();
// 10. 进入中转界面
resetToTransit("充值");
}
采用 4 字节无符号长整型(unsigned long)存储余额对应卡片数据的第 8-11 字节,(大端存储:高位字节存高地址);未初始化的卡片写入 "MEAL" 标识(前 4 字节),余额设为 0,实现 "空白卡即插即用"
2.3 按键处理人机交互
cpp
void handleKeyPress(char key) {
// 按D键:复位/返回
if (key == 'D') {
switch (currentMode) {
case MODE_TRANSIT:
case MODE_BALANCE_RESULT:
case MODE_IDLE:
resetSystem(); // 系统复位(重启启动画面)
break;
case MODE_RECHARGE_INPUT:
case MODE_CONSUME_INPUT:
resetToIdle(); // 返回就绪界面
break;
default:
break;
}
return;
}
// 根据当前模式处理其他按键
switch (currentMode) {
case MODE_IDLE: // 就绪界面:选择功能
switch (key) {
case 'A': enterRechargeMode(); break; // A=充值
case 'B': enterConsumeMode(); break; // B=消费
case 'C': checkBalance(); break; // C=查询余额
default: break;
}
break;
case MODE_RECHARGE_INPUT: // 充值/消费输入界面:处理金额
case MODE_CONSUME_INPUT:
switch (key) {
case '#': processAmountInput(); break; // #=确认金额
case '*': // *=删除最后一位
if (amountInput.length() > 0) {
amountInput = amountInput.substring(0, amountInput.length() - 1);
}
break;
default: // 数字键:输入金额
if (key >= '0' && key <= '9') {
amountInput += key;
}
break;
}
break;
default:
break;
}
}
支持数字输入、删除,按 #确认后校验金额有效性(非空、非零);
不同状态下同一按键功能不同(如 D 键在就绪界面是系统复位,在输入界面是返回)
2.4 超时自动返回机制
cpp
void loop() {
// 处理按键输入
char key = keypad.getKey();
if (key) {
handleKeyPress(key);
lastActivityTime = millis(); //重置活动时间
}
// 检查超时(在余额查询和等待卡片模式下)
if ((currentMode == MODE_BALANCE_CHECK || currentMode == MODE_WAITING_CARD)){
unsigned long elapsedTime = millis() - lastActivityTime;
// 显示倒计时
int remainingSeconds = (TIMEOUT_DURATION - elapsedTime) / 1000;
if (remainingSeconds >= 0) {
displayCountdown(remainingSeconds);
}
// 超时处理
if (elapsedTime > TIMEOUT_DURATION) {
displayErrorScreen("系统过时,自动复位");
delay(1500);
resetToIdle();
return;
}
}
// 省略按键扫描处理逻辑
}
void displayCountdown(int seconds) {
if (seconds == lastDisplayedCountdown) return;
// 清除倒计时区域
tft.fillRect(260, 10, 50, 25, COLOR_BG);
// 显示倒计时
u8g2_for_adafruit_gfx.setCursor(265, 25);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_COUNTDOWN);
if (seconds >= 10) {
u8g2_for_adafruit_gfx.print(String(seconds) + "s");
} else if (seconds >= 0) {
u8g2_for_adafruit_gfx.print("0" + String(seconds) + "s");
} else {
u8g2_for_adafruit_gfx.print("00s");
}
lastDisplayedCountdown = seconds;
}
系统在进入余额查询、等待充值/消费 界面会在右上角加载倒计时提醒用户,超过15s系统自动复位到"就绪页面"
⭐系统流程图

2.5 完整代码架构
cpp
/**************************************************************************************
* 文件: /CampusMealCard_V2.0/CampusMealCard_V2.0.ino
* 作者:零知实验室(深圳市在芯间科技有限公司)
* -^^- 零知开源,让电子制作变得更简单! -^^-
* 时间: 2025-11-21 18:15:21
* 说明: 基于零知标准板和RFID-RC522的校园餐卡管理系统
* 支持餐卡充值、消费、余额查询功能,配备2.0寸ST7789显示屏和4×4矩阵键盘
* 采用软件SPI驱动RFID模块,硬件SPI驱动显示屏,确保系统稳定高效运行
***************************************************************************************/
#include <SPI.h>
#include <RFID.h>
#include <Keypad.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <U8g2_for_Adafruit_GFX.h>
// ====================== TFT显示屏配置 ======================
#define TFT_CS 10
#define TFT_MOSI 11
#define TFT_SCK 13
#define TFT_RST 28
#define TFT_DC 26
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
// U8g2中文字体支持(关键优化:使用精简字体)
U8G2_FOR_ADAFRUIT_GFX u8g2_for_adafruit_gfx;
// 显示屏颜色定义(确保为ST77XX库支持的16位RGB565格式)
#define COLOR_BG ST77XX_BLACK // 黑色背景
#define COLOR_TITLE ST77XX_CYAN // 标题色(青色)
#define COLOR_SUBTITLE ST77XX_BLUE // 副标题色(蓝色)
#define COLOR_TEXT ST77XX_WHITE // 正文色(白色)
#define COLOR_AMOUNT ST77XX_YELLOW // 金额色(黄色)
#define COLOR_SUCCESS ST77XX_GREEN // 成功色(绿色)
#define COLOR_ERROR ST77XX_RED // 错误色(红色)
#define COLOR_HINT 0x8410 // 提示色(灰色,16位RGB565值)
#define COLOR_COUNTDOWN 0xFD20 // 倒计时颜色(橙色,16位RGB565值)
// 显示跟踪变量(避免重复绘制)
String lastDisplayedAmount = "";
unsigned long lastDisplayedBalance = 0;
bool isSplashShown = false;
// 静态界面初始化标志(解决局部刷新问题)
bool rechargeScreenInit = false;
bool consumeScreenInit = false;
// 超时相关变量
unsigned long lastActivityTime = 0;
const unsigned long TIMEOUT_DURATION = 15000; // 15秒超时
int lastDisplayedCountdown = -1; // 上次显示的倒计时秒数
// ====================== RFID配置 ======================
RFID rfid(9, 5, 6, 4, 3); // CS, NRSTPD, SCK, MOSI, MISO(软件SPI)
#define MEAL_CARD_SECTOR 1
#define MEAL_CARD_BLOCK 1
unsigned char defaultKeyA[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
// ====================== 4×4矩阵按键配置 ======================
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
{'1','2','3','A'}, // A: 充值
{'4','5','6','B'}, // B: 消费
{'7','8','9','C'}, // C: 查询余额
{'*','0','#','D'} // D: 系统复位
};
byte rowPins[ROWS] = {36, 35, 34, 33};
byte colPins[COLS] = {32, 31, 30, 29};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// ====================== 系统状态配置 ======================
enum SystemMode {
MODE_IDLE,
MODE_RECHARGE_INPUT,
MODE_CONSUME_INPUT,
MODE_WAITING_CARD,
MODE_BALANCE_CHECK,
MODE_BALANCE_RESULT, // 余额查询结果界面
MODE_TRANSIT // 中转提醒界面
};
SystemMode currentMode = MODE_IDLE;
// 全局状态变量
bool systemReady = true;
unsigned long currentBalance = 0;
String currentCardID = "";
String amountInput = "";
unsigned long pendingAmount = 0;
String pendingOperation = "";
// 卡片检测去抖动
unsigned long lastCardDetection = 0;
const unsigned long CARD_DEBOUNCE_DELAY = 500;
// ====================== 显示屏绘制函数(优化资源占用) ======================
// 启动画面(移除冗余动画,减少代码体积)
void displaySplashScreen() {
if (isSplashShown) return;
tft.fillScreen(COLOR_BG);
// 使用U8g2精简中文字体显示核心信息
u8g2_for_adafruit_gfx.setCursor(50, 100);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TITLE);
u8g2_for_adafruit_gfx.print("校园餐卡系统");
u8g2_for_adafruit_gfx.setCursor(85, 140);
u8g2_for_adafruit_gfx.print("RFID - RC522版");
// 版本信息(简化显示)
u8g2_for_adafruit_gfx.setCursor(70, 200);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("v1.0");
isSplashShown = true;
delay(1500); // 缩短显示时间,节省启动耗时
}
// 就绪界面(等待操作选择)
void displayIdleScreen() {
tft.fillScreen(COLOR_BG);
// 标题
u8g2_for_adafruit_gfx.setCursor(105, 50);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TITLE);
u8g2_for_adafruit_gfx.print("系统等待");
// 分隔线(简化绘制)
tft.drawFastHLine(20, 70, 280, COLOR_TITLE);
// 操作提示(精简排版,减少绘制次数)
u8g2_for_adafruit_gfx.setCursor(40, 110);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("A - 充值按键");
u8g2_for_adafruit_gfx.setCursor(40, 140);
u8g2_for_adafruit_gfx.print("B - 消费按键");
u8g2_for_adafruit_gfx.setCursor(40, 170);
u8g2_for_adafruit_gfx.print("C - 余额");
u8g2_for_adafruit_gfx.setCursor(40, 200);
u8g2_for_adafruit_gfx.print("D - 复位");
// 底部提示
u8g2_for_adafruit_gfx.setCursor(115, 230);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("请进行按键");
}
// 充值界面
void displayRechargeScreen() {
if (!rechargeScreenInit) {
tft.fillScreen(COLOR_BG);
// 标题
u8g2_for_adafruit_gfx.setCursor(120, 50);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TITLE);
u8g2_for_adafruit_gfx.print("充值模式");
// 分隔线
tft.drawFastHLine(20, 70, 280, COLOR_TITLE);
// 提示信息(精简文字,减少显示压力)
u8g2_for_adafruit_gfx.setCursor(20, 110);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("键入充值金额 (#确认)");
u8g2_for_adafruit_gfx.setCursor(20, 130);
u8g2_for_adafruit_gfx.print("* 取消充值");
u8g2_for_adafruit_gfx.setCursor(20, 150);
u8g2_for_adafruit_gfx.print("0开头=小数(如05=0.50)");
// 金额标签
u8g2_for_adafruit_gfx.setCursor(20, 190);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("金额: ");
rechargeScreenInit = true;
}
// 动态更新金额(仅刷新变化区域)
if (amountInput != lastDisplayedAmount) {
tft.fillRect(80, 170, 240, 45, COLOR_BG);
u8g2_for_adafruit_gfx.setCursor(80, 195);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_AMOUNT);
String displayAmount = amountInput;
if (displayAmount.startsWith("0") && displayAmount.length() > 1) {
displayAmount = "0." + displayAmount.substring(1);
while (displayAmount.length() < 4) displayAmount += "0";
}
u8g2_for_adafruit_gfx.print(displayAmount + "元");
lastDisplayedAmount = amountInput;
}
}
// 消费界面
void displayConsumeScreen() {
if (!consumeScreenInit) {
tft.fillScreen(COLOR_BG);
// 标题
u8g2_for_adafruit_gfx.setCursor(120, 50);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TITLE);
u8g2_for_adafruit_gfx.print("消费模式");
// 分隔线
tft.drawFastHLine(20, 70, 280, COLOR_TITLE);
// 提示信息
u8g2_for_adafruit_gfx.setCursor(20, 110);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("键入消费金额 (#确认)");
u8g2_for_adafruit_gfx.setCursor(20, 130);
u8g2_for_adafruit_gfx.print("* 清除金额");
u8g2_for_adafruit_gfx.setCursor(20, 150);
u8g2_for_adafruit_gfx.print("0开头=小数(如05=0.50)");
// 金额标签
u8g2_for_adafruit_gfx.setCursor(20, 190);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("金额: ");
consumeScreenInit = true;
}
// 动态更新金额
if (amountInput != lastDisplayedAmount) {
tft.fillRect(80, 170, 240, 45, COLOR_BG);
u8g2_for_adafruit_gfx.setCursor(80, 195);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_AMOUNT);
String displayAmount = amountInput;
if (displayAmount.startsWith("0") && displayAmount.length() > 1) {
displayAmount = "0." + displayAmount.substring(1);
while (displayAmount.length() < 4) displayAmount += "0";
}
u8g2_for_adafruit_gfx.print(displayAmount + "元");
lastDisplayedAmount = amountInput;
}
}
void displayCountdown(int seconds) {
if (seconds == lastDisplayedCountdown) return;
// 清除倒计时区域
tft.fillRect(260, 10, 50, 25, COLOR_BG);
// 显示倒计时
u8g2_for_adafruit_gfx.setCursor(265, 25);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_COUNTDOWN);
if (seconds >= 10) {
u8g2_for_adafruit_gfx.print(String(seconds) + "s");
} else if (seconds >= 0) {
u8g2_for_adafruit_gfx.print("0" + String(seconds) + "s");
} else {
u8g2_for_adafruit_gfx.print("00s");
}
lastDisplayedCountdown = seconds;
}
// 等待放卡界面(充值/消费确认后)
void displayWaitingCardScreen() {
tft.fillScreen(COLOR_BG);
// 标题
String title = pendingOperation == "充值" ? "正在充值..." : "正在消费...";
u8g2_for_adafruit_gfx.setCursor(65, 50);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TITLE);
u8g2_for_adafruit_gfx.print(title);
// 分隔线
tft.drawFastHLine(20, 70, 280, COLOR_TITLE);
// 确认金额
u8g2_for_adafruit_gfx.setCursor(20, 120);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("确认金额: ");
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_AMOUNT);
u8g2_for_adafruit_gfx.print(pendingAmount / 100.0, 2);
u8g2_for_adafruit_gfx.print("元");
// 提示信息
u8g2_for_adafruit_gfx.setCursor(60, 180);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("请放餐卡");
u8g2_for_adafruit_gfx.setCursor(60, 210);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("靠近读卡器!");
// 重置倒计时显示
lastDisplayedCountdown = -1;
}
// 余额查询界面(等待放卡)
void displayBalanceCheckScreen() {
tft.fillScreen(COLOR_BG);
// 标题
u8g2_for_adafruit_gfx.setCursor(120, 50);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TITLE);
u8g2_for_adafruit_gfx.print("查看余额");
// 分隔线
tft.drawFastHLine(20, 70, 280, COLOR_TITLE);
// 提示信息
u8g2_for_adafruit_gfx.setCursor(60, 150);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("请放餐卡");
u8g2_for_adafruit_gfx.setCursor(60, 180);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("靠近读卡器!");
// 重置倒计时显示
lastDisplayedCountdown = -1;
}
// 余额查询结果界面
void displayBalanceResultScreen(String cardID, unsigned long balance) {
tft.fillScreen(COLOR_BG);
// 标题
u8g2_for_adafruit_gfx.setCursor(60, 50);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TITLE);
u8g2_for_adafruit_gfx.print("查看余额信息");
// 分隔线
tft.drawFastHLine(20, 70, 280, COLOR_TITLE);
// 卡号(精简显示,避免换行)
u8g2_for_adafruit_gfx.setCursor(20, 120);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("卡号: ");
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_SUBTITLE);
u8g2_for_adafruit_gfx.print(cardID.substring(0, 14)); // 截取前14字符,避免超出屏幕
// 余额
if (balance != lastDisplayedBalance) {
tft.fillRect(20, 150, 280, 40, COLOR_BG);
u8g2_for_adafruit_gfx.setCursor(20, 170);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("当前余额: ");
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_AMOUNT);
u8g2_for_adafruit_gfx.print(balance / 100.0, 2);
u8g2_for_adafruit_gfx.print("元");
lastDisplayedBalance = balance;
}
// 底部提示
u8g2_for_adafruit_gfx.setCursor(60, 220);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("按D键返回");
// 清除倒计时显示
tft.fillRect(260, 10, 50, 25, COLOR_BG);
lastDisplayedCountdown = -1;
}
// 中转提醒界面(操作完成后)
void displayTransitScreen(String operation) {
tft.fillScreen(COLOR_BG);
// 操作结果
String resultTitle = operation + "完成";
u8g2_for_adafruit_gfx.setCursor(80, 80);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_SUCCESS);
u8g2_for_adafruit_gfx.print(resultTitle);
// 提示信息
u8g2_for_adafruit_gfx.setCursor(30, 120);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("已成功进行充值/消费");
u8g2_for_adafruit_gfx.setCursor(50, 200);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("按D键返回");
// 清除倒计时显示
tft.fillRect(260, 10, 50, 25, COLOR_BG);
lastDisplayedCountdown = -1;
}
// 错误提示界面
void displayErrorScreen(String errorMsg) {
tft.fillScreen(COLOR_BG);
// 错误标题
u8g2_for_adafruit_gfx.setCursor(110, 50);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_ERROR);
u8g2_for_adafruit_gfx.print("错误");
// 错误信息(精简显示,避免超出屏幕)
u8g2_for_adafruit_gfx.setCursor(20, 120);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print(errorMsg);
// 提示
u8g2_for_adafruit_gfx.setCursor(50, 200);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_HINT);
u8g2_for_adafruit_gfx.print("按D键复位");
// 清除倒计时显示
tft.fillRect(260, 10, 50, 25, COLOR_BG);
lastDisplayedCountdown = -1;
}
// ====================== 系统核心函数 ======================
void setup() {
Serial.begin(115200);
// 初始化显示屏
tft.init(240, 320);
tft.setRotation(1); // 320x240 横向显示
tft.fillScreen(COLOR_BG);
// 初始化U8g2中文字体(关键优化:使用精简字体)
u8g2_for_adafruit_gfx.begin(tft);
u8g2_for_adafruit_gfx.setFontMode(1); // 透明模式(节省绘制资源)
u8g2_for_adafruit_gfx.setFontDirection(0); // 从左到右显示
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
// 精简中文字体(仅含常用字,占用ROM<8KB)
u8g2_for_adafruit_gfx.setFont(u8g2_font_unifont_t_chinese3);
// 初始化RFID(软件SPI无需额外配置)
rfid.init();
// 显示启动画面
displaySplashScreen();
// 进入就绪界面
currentMode = MODE_IDLE;
lastActivityTime = millis(); // 初始化活动时间
displayIdleScreen();
}
void loop() {
// 处理按键输入
char key = keypad.getKey();
if (key) {
handleKeyPress(key);
lastActivityTime = millis(); //重置活动时间
}
// 检查超时(在余额查询和等待卡片模式下)
if ((currentMode == MODE_BALANCE_CHECK || currentMode == MODE_WAITING_CARD)){
unsigned long elapsedTime = millis() - lastActivityTime;
// 显示倒计时
int remainingSeconds = (TIMEOUT_DURATION - elapsedTime) / 1000;
if (remainingSeconds >= 0) {
displayCountdown(remainingSeconds);
}
// 检查是否超时
if (elapsedTime > TIMEOUT_DURATION) {
displayErrorScreen("系统过时,自动复位");
delay(1500);
resetToIdle();
return;
}
}
// 根据当前模式处理逻辑和显示
switch (currentMode) {
case MODE_IDLE:
break;
case MODE_RECHARGE_INPUT:
displayRechargeScreen();
break;
case MODE_CONSUME_INPUT:
displayConsumeScreen();
break;
case MODE_WAITING_CARD:
handleWaitingCardMode();
break;
case MODE_BALANCE_CHECK:
handleBalanceCheckMode();
break;
case MODE_BALANCE_RESULT:
case MODE_TRANSIT:
break;
}
delay(50);
}
// 按键处理
void handleKeyPress(char key) {
if (key == 'D') {
switch (currentMode) {
case MODE_TRANSIT:
case MODE_BALANCE_RESULT:
case MODE_IDLE:
resetSystem();
break;
case MODE_RECHARGE_INPUT:
case MODE_CONSUME_INPUT:
resetToIdle();
break;
default:
break;
}
return;
}
// 其他按键处理
switch (currentMode) {
case MODE_IDLE:
switch (key) {
case 'A': enterRechargeMode(); break;
case 'B': enterConsumeMode(); break;
case 'C': checkBalance(); break;
default: break;
}
break;
case MODE_RECHARGE_INPUT:
case MODE_CONSUME_INPUT:
switch (key) {
case '#': processAmountInput(); break;
case '*':
if (amountInput.length() > 0) {
amountInput = amountInput.substring(0, amountInput.length() - 1);
}
break;
default:
if (key >= '0' && key <= '9') {
amountInput += key;
}
break;
}
break;
default:
break;
}
}
// 进入充值模式
void enterRechargeMode() {
currentMode = MODE_RECHARGE_INPUT;
amountInput = "";
lastDisplayedAmount = "";
rechargeScreenInit = false;
lastActivityTime = millis(); // 重置活动时间
}
// 进入消费模式
void enterConsumeMode() {
currentMode = MODE_CONSUME_INPUT;
amountInput = "";
lastDisplayedAmount = "";
consumeScreenInit = false;
lastActivityTime = millis(); // 重置活动时间
}
// 处理金额输入确认
void processAmountInput() {
if (amountInput.length() == 0) {
displayErrorScreen("请键入正确金额");
delay(1500);
resetToIdle();
return;
}
unsigned long amount = 0;
if (amountInput.startsWith("0")) {
if (amountInput.length() > 1) {
String decimalPart = amountInput.substring(1);
if (decimalPart.length() > 2) decimalPart = decimalPart.substring(0, 2);
while (decimalPart.length() < 2) decimalPart += "0";
amount = decimalPart.toInt();
}
} else {
amount = amountInput.toInt() * 100;
}
if (amount == 0) {
displayErrorScreen("金额不能为零");
delay(1500);
resetToIdle();
return;
}
pendingAmount = amount;
pendingOperation = (currentMode == MODE_RECHARGE_INPUT) ? "充值" : "消费";
currentMode = MODE_WAITING_CARD;
displayWaitingCardScreen();
}
// 等待放卡处理(充值/消费)
void handleWaitingCardMode() {
// 检查退出按键
char key = keypad.getKey();
if (key == 'D') {
resetToIdle();
return;
}
if (millis() - lastCardDetection < CARD_DEBOUNCE_DELAY) return;
lastCardDetection = millis();
if (rfid.isCard()) {
lastActivityTime = millis(); // 检测到卡片,重置超时时间
if (pendingOperation == "充值") {
executeRecharge(pendingAmount);
} else if (pendingOperation == "消费") {
executeConsume(pendingAmount);
}
}
}
// 进入余额查询模式
void checkBalance() {
currentMode = MODE_BALANCE_CHECK;
displayBalanceCheckScreen();
lastActivityTime = millis(); // 重置活动时间
}
// 等待放卡处理(余额查询)
void handleBalanceCheckMode() {
// 检查按键(允许在等待卡片时退出)
char key = keypad.getKey();
if (key == 'D') {
resetToIdle();
return;
}
if (millis() - lastCardDetection < CARD_DEBOUNCE_DELAY) return;
lastCardDetection = millis();
if (rfid.isCard()) {
lastActivityTime = millis(); // 检测到卡片,重置超时计时
executeBalanceCheck();
}
}
// 执行充值
void executeRecharge(unsigned long amount) {
systemReady = false;
if (!rfid.readCardSerial()) {
displayErrorScreen("读卡失败");
delay(1500);
resetToIdle();
return;
}
String cardID = "";
for (int i = 0; i < 5; i++) {
if (rfid.serNum[i] < 0x10) cardID += "0";
cardID += String(rfid.serNum[i], HEX);
if (i < 4) cardID += " ";
}
currentCardID = cardID;
if (rfid.selectTag(rfid.serNum) == 0) {
displayErrorScreen("选卡失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
unsigned char blockAddr = MEAL_CARD_SECTOR * 4 + MEAL_CARD_BLOCK;
if (rfid.auth(PICC_AUTHENT1A, blockAddr, defaultKeyA, rfid.serNum) != MI_OK) {
displayErrorScreen("认证失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
unsigned char cardData[16];
if (rfid.read(blockAddr, cardData) != MI_OK) {
displayErrorScreen("读数据失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
if (!isMealCard(cardData)) {
if (!initializeMealCard(blockAddr)) {
displayErrorScreen("卡片初始化失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
if (rfid.read(blockAddr, cardData) != MI_OK) {
displayErrorScreen("初始化数据读取失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
}
unsigned long oldBalance = getBalanceFromCard(cardData);
unsigned long newBalance = oldBalance + amount;
setBalanceToCard(cardData, newBalance);
if (rfid.write(blockAddr, cardData) == MI_OK) {
currentBalance = newBalance;
tft.fillScreen(COLOR_BG);
u8g2_for_adafruit_gfx.setCursor(80, 80);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_SUCCESS);
u8g2_for_adafruit_gfx.print("充值成功");
u8g2_for_adafruit_gfx.setCursor(20, 140);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("新余额: ");
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_AMOUNT);
u8g2_for_adafruit_gfx.print(newBalance / 100.0, 2);
u8g2_for_adafruit_gfx.print("元");
delay(2000);
} else {
displayErrorScreen("充值失败");
delay(1500);
}
rfid.halt();
resetToTransit("充值");
}
// 执行消费
void executeConsume(unsigned long amount) {
systemReady = false;
if (!rfid.readCardSerial()) {
displayErrorScreen("读卡失败");
delay(1500);
resetToIdle();
return;
}
String cardID = "";
for (int i = 0; i < 5; i++) {
if (rfid.serNum[i] < 0x10) cardID += "0";
cardID += String(rfid.serNum[i], HEX);
if (i < 4) cardID += " ";
}
currentCardID = cardID;
if (rfid.selectTag(rfid.serNum) == 0) {
displayErrorScreen("选卡失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
unsigned char blockAddr = MEAL_CARD_SECTOR * 4 + MEAL_CARD_BLOCK;
if (rfid.auth(PICC_AUTHENT1A, blockAddr, defaultKeyA, rfid.serNum) != MI_OK) {
displayErrorScreen("认证失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
unsigned char cardData[16];
if (rfid.read(blockAddr, cardData) != MI_OK) {
displayErrorScreen("读数据失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
if (!isMealCard(cardData)) {
displayErrorScreen("无效的餐卡");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
unsigned long oldBalance = getBalanceFromCard(cardData);
if (oldBalance >= amount) {
unsigned long newBalance = oldBalance - amount;
setBalanceToCard(cardData, newBalance);
if (rfid.write(blockAddr, cardData) == MI_OK) {
currentBalance = newBalance;
tft.fillScreen(COLOR_BG);
u8g2_for_adafruit_gfx.setCursor(80, 80);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_SUCCESS);
u8g2_for_adafruit_gfx.print("消费成功");
u8g2_for_adafruit_gfx.setCursor(20, 140);
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_TEXT);
u8g2_for_adafruit_gfx.print("新余额: ");
u8g2_for_adafruit_gfx.setForegroundColor(COLOR_AMOUNT);
u8g2_for_adafruit_gfx.print(newBalance / 100.0, 2);
u8g2_for_adafruit_gfx.print("元");
delay(2000);
} else {
displayErrorScreen("消费失败");
delay(1500);
}
} else {
displayErrorScreen("余额不足");
delay(1500);
}
rfid.halt();
resetToTransit("消费");
}
// 执行余额查询
void executeBalanceCheck() {
systemReady = false;
if (!rfid.readCardSerial()) {
displayErrorScreen("读卡失败");
delay(1500);
resetToIdle();
return;
}
String cardID = "";
for (int i = 0; i < 5; i++) {
if (rfid.serNum[i] < 0x10) cardID += "0";
cardID += String(rfid.serNum[i], HEX);
if (i < 4) cardID += " ";
}
currentCardID = cardID;
if (rfid.selectTag(rfid.serNum) == 0) {
displayErrorScreen("选卡失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
unsigned char blockAddr = MEAL_CARD_SECTOR * 4 + MEAL_CARD_BLOCK;
if (rfid.auth(PICC_AUTHENT1A, blockAddr, defaultKeyA, rfid.serNum) != MI_OK) {
displayErrorScreen("认证失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
unsigned char cardData[16];
if (rfid.read(blockAddr, cardData) != MI_OK) {
displayErrorScreen("读数据失败");
rfid.halt();
delay(1500);
resetToIdle();
return;
}
if (isMealCard(cardData)) {
unsigned long balance = getBalanceFromCard(cardData);
currentBalance = balance;
currentMode = MODE_BALANCE_RESULT;
systemReady = true;
displayBalanceResultScreen(cardID, balance);
} else {
displayErrorScreen("无效的餐卡");
delay(1500);
resetToIdle();
}
rfid.halt();
}
// 重置到中转界面
void resetToTransit(String operation) {
currentMode = MODE_TRANSIT;
amountInput = "";
pendingAmount = 0;
pendingOperation = "";
lastDisplayedAmount = "";
lastDisplayedBalance = 0;
systemReady = true;
displayTransitScreen(operation);
}
// 重置到就绪界面
void resetToIdle() {
currentMode = MODE_IDLE;
amountInput = "";
pendingAmount = 0;
pendingOperation = "";
lastDisplayedAmount = "";
lastDisplayedBalance = 0;
rechargeScreenInit = false;
consumeScreenInit = false;
systemReady = true;
lastActivityTime = millis(); // 重置活动时间
displayIdleScreen();
}
// 系统复位
void resetSystem() {
currentMode = MODE_IDLE;
amountInput = "";
pendingAmount = 0;
pendingOperation = "";
systemReady = true;
currentBalance = 0;
currentCardID = "";
lastDisplayedAmount = "";
lastDisplayedBalance = 0;
rechargeScreenInit = false;
consumeScreenInit = false;
isSplashShown = false;
lastActivityTime = millis(); // 重置活动时间
rfid.init();
displaySplashScreen();
displayIdleScreen();
}
// ====================== 卡片数据处理函数 ======================
bool isMealCard(unsigned char* cardData) {
return (cardData[0] == 'M' && cardData[1] == 'E' && cardData[2] == 'A' && cardData[3] == 'L');
}
bool initializeMealCard(unsigned char blockAddr) {
unsigned char mealCardData[16] = {
'M', 'E', 'A', 'L', 'C', 'A', 'R', 'D',
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
if (rfid.write(blockAddr, mealCardData) == MI_OK) {
currentBalance = 0;
return true;
} else {
return false;
}
}
unsigned long getBalanceFromCard(unsigned char* cardData) {
return ((unsigned long)cardData[11] << 24) |
((unsigned long)cardData[10] << 16) |
((unsigned long)cardData[9] << 8) |
cardData[8];
}
void setBalanceToCard(unsigned char* cardData, unsigned long balance) {
cardData[8] = balance & 0xFF;
cardData[9] = (balance >> 8) & 0xFF;
cardData[10] = (balance >> 16) & 0xFF;
cardData[11] = (balance >> 24) & 0xFF;
}
软件 SPI 通信原理
RFID-RC522 通过软件模拟 SPI 协议与主控制器通信,关键通信函数包括:
cpp
/******************************************************************************
* 构造 RFID
* 输入参数:
* chipSelectPin: RFID模块CS引脚
* NRSTPD: RFID模块复位引脚
* sckPin: 软件SPI时钟引脚
* mosiPin: 软件SPI数据输出引脚
* misoPin: 软件SPI数据输入引脚
******************************************************************************/
RFID::RFID(int chipSelectPin, int NRSTPD, int sckPin, int mosiPin, int misoPin)
{
_chipSelectPin = chipSelectPin;
_NRSTPD = NRSTPD;
_sckPin = sckPin;
_mosiPin = mosiPin;
_misoPin = misoPin;
// 初始化引脚模式
pinMode(_chipSelectPin, OUTPUT);
pinMode(_NRSTPD, OUTPUT);
pinMode(_sckPin, OUTPUT);
pinMode(_mosiPin, OUTPUT);
pinMode(_misoPin, INPUT);
// 初始状态设置
digitalWrite(_chipSelectPin, HIGH); // 初始禁用RFID模块
digitalWrite(_NRSTPD, HIGH); // 禁用复位
digitalWrite(_sckPin, LOW); // SCK初始低电平
digitalWrite(_mosiPin, LOW); // MOSI初始低电平
}
/******************************************************************************
* 软件SPI核心函数:发送一个字节并接收返回字节(MSB先传)
******************************************************************************/
unsigned char RFID::spiTransfer(unsigned char data)
{
unsigned char received = 0;
for (int i = 7; i >= 0; i--) // 从最高位(bit7)到最低位(bit0)
{
// 1. 输出当前bit到MOSI
digitalWrite(_mosiPin, (data >> i) & 0x01);
// 2. 拉高SCK,让从机读取MOSI数据
digitalWrite(_sckPin, HIGH);
delayMicroseconds(1); // 时序匹配,可根据实际情况调整
// 3. 读取MISO数据
received |= (digitalRead(_misoPin) << i);
// 4. 拉低SCK,准备下一位数据
digitalWrite(_sckPin, LOW);
delayMicroseconds(1);
}
return received;
}
SCK:主机输出时钟,上升沿时 RC522 采样 MOSI 数据,下降沿时 RC522 输出 MISO 数据
核心寄存器
RFID-RC522 关键寄存器配置:0x2A的TModeReg值为0x8D进行定时器设置
cpp
void RFID::init()
{
digitalWrite(_NRSTPD, HIGH);
reset();
// 定时器配置:TPrescaler*TreloadVal/6.78MHz = 24ms
writeMFRC522(TModeReg, 0x8D); // Tauto=1; f(Timer) = 6.78MHz/TPreScaler
writeMFRC522(TPrescalerReg, 0x3E); // TModeReg[3..0] + TPrescalerReg
writeMFRC522(TReloadRegL, 30);
writeMFRC522(TReloadRegH, 0);
writeMFRC522(TxAutoReg, 0x40); // 100% ASK调制
writeMFRC522(ModeReg, 0x3D); // CRC初始值 0x6363
antennaOn(); // 打开天线
}
三、项目结果演示
3.1 人机交互流程
①启动阶段:显示"校园餐卡系统"启动画面,1.5秒后进入主界面
②功能选择:
A键进入充值模式,输入金额后按#确认;B键进入消费模式,输入金额后按#确认;C键进入余额查询,等待放卡;D键系统复位,返回初始状态

支持整数和小数输入(如05表示0.50元);*键删除错误输入;#键确认金额

③卡片操作:
确认输入金额;右上角显示15秒倒计时;检测到卡片自动执行相应操作;充值成功显示当前余额;按D键返回主界面

消费流程与充值一致,按 B 键进入消费模式,输入金额后刷卡,余额扣除;消费金额大于卡内余额时显示余额不足
④:余额查询:
等待放卡处理;执行余额查询(读卡/选卡),成功读取显示当前卡号和余额信息;15秒未操作执行超时复位

3.2 视频演示
基于STM32F103RBT6的RFID校园餐卡系统完整演示
展示完整的餐卡充值、消费、余额查询操作流程,倒计时功能的实际效果和超时自动返回机制,系统复位和金额异常处理
四、RFID-RC522射频IC卡识别模块详解
模块简介
RFID-RC522 是基于 NXP MFRC522 芯片的非接触式 RFID 读写模块,工作频率为 13.56MHz,支持 ISO 14443A 标准的 M1 卡(S50/S70)、MF1 IC 卡等,通信接口支持 SPI、I2C、UART(本项目使用软件 SPI),有效读写距离为 2-5cm,适用于门禁、支付、数据存储等场景
4.1 工作原理
①射频场产生:RC522 通过天线线圈产生 13.56MHz 的射频场,当 M1 卡进入射频场时,卡片内的 LC 谐振电路(13.56MHz)感应产生感应电流,为卡片内的芯片供电

②卡片激活:卡片供电后,内部芯片初始化,通过天线向 RC522 发送卡片序列号(UID)
③数据通信:RC522 接收 UID 后,通过 SPI 向 STM32 发送数据,STM32 发送认证命令(如密钥 A 认证),RC522 将命令转发给卡片,卡片认证通过后,RC522 与卡片建立双向通信,实现数据读写

④数据处理:STM32 接收 RC522 返回的数据(如余额),解析后通过屏幕显示,或修改数据(如充值)后通过 RC522 写回卡片
4.2 核心寄存器
①CommandReg寄存器
配置值为0x0F系统软复位

②ModeReg寄存器
配置值为0x3D发送接收模式

相关寄存器
| 寄存器地址 | 寄存器名称 | 功能描述 | 项目中用途 |
|---|---|---|---|
| 0x01 | CommandReg | 命令寄存器,存储待执行的命令 | 发送复位(PCD_RESETPHASE)、认证、读写命令 |
| 0x16 | ModeReg | 模式寄存器,配置 CRC 长度和通信模式 | 配置 CRC 为 16 位,SPI 通信模式(0x3D) |
| 0x09 | StatusReg | 状态寄存器,返回当前操作状态 | 判断认证、读写是否成功 |
| 0x0A | FIFODataReg | FIFO 数据寄存器,存储发送 / 接收的数据 | 传输密钥、读写的数据块内容 |
| 0x14 | TxControlReg | 发射控制寄存器,控制天线射频场强度 | 启动天线(0x40),增强射频场 |
| 0x15 | TxAutoReg | 自动发送寄存器,配置自动发送 CRC | 启用自动发送 CRC(0x40) |
4.3 软件SPI通信
数据由MISO端口到RC52寄存器,RC522寄存器到MOSI端口

软件SPI通过控制MISO引脚和MOSI端口引脚的高低电平来控制数据的进出,空闲时 SCK 为低电平,每次通信前拉低 CS(片选),通信结束后拉高 CS
五、常见问题指引(Q&A)
Q1---RFID 无法读卡或认证失败?
*A:进行以下排查:*确认 IC 卡为 M1 卡(13.56MHz),非其他协议卡片(如 ID 卡);检查defaultKeyA是否为0xFF FF FF FF FF FF(默认密钥),若卡片已修改密钥需同步更新;确保卡片靠近 RC522 的天线区域(中心位置)
Q2---系统超时不生效?
A: 检查
loop函数中的超时判断逻辑*:确保仅在MODE_BALANCE_CHECK和MODE_WAITING_CARD模式下启用;确认lastActivityTime在按键操作、检测到卡片时已重置(lastActivityTime = millis())*
Q3---系统偶尔死机如何解决?
A:启用看门狗定时器;增加异常处理机制;检查堆栈溢出问题;使用超时自动返回机制
项目资源整合
RFID 软件 SPI 驱动库:RFID-SoftSPI-Driver
RFID-RC522数据手册:MFRC522-Data sheet
MIFARE Classic 1K数据手册:MF1S50-Data sheet