零知IDE——基于STM32F103RBT6与RFID-RC522的校园餐卡系统实现

✔零知IDE 是一个真正属于国人自己的开源软件平台,在开发效率上超越了Arduino平台并且更加容易上手,大大降低了开发难度。零知开源在软件方面提供了完整的学习教程和丰富示例代码,让不懂程序的工程师也能非常轻而易举的搭建电路来创作产品,测试产品。快来动手试试吧!

✔访问零知实验室,获取更多实战项目和教程资源吧!

www.lingzhilab.com

目录

一、硬件系统设计

[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 视频演示)

四、RFID-RC522射频IC卡识别模块详解

[4.1 工作原理](#4.1 工作原理)

[4.2 核心寄存器](#4.2 核心寄存器)

[4.3 软件SPI通信](#4.3 软件SPI通信)

五、常见问题指引(Q&A)

[Q1---RFID 无法读卡或认证失败?](#Q1—RFID 无法读卡或认证失败?)

Q2---系统超时不生效?

Q3---系统偶尔死机如何解决?


(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

中文库:U8g2_for_Adafruit_GFX

RFID-RC522数据手册:MFRC522-Data sheet

MIFARE Classic 1K数据手册:MF1S50-Data sheet

相关推荐
promising-w1 小时前
【stm32入门教程】GPIO输入之按键控制LED&光敏传感器控制蜂鸣器
stm32·单片机·嵌入式硬件
必胜的思想钢印2 小时前
修改主频&睡眠模式&停机模式&待机模式
笔记·stm32·单片机·嵌入式硬件·学习
飞睿科技3 小时前
【IoT开发选型】乐鑫ESP32-S3核心优势解析:为何它是AIoT应用的“全能王”?
科技·嵌入式硬件·物联网·智能家居
文亭湖畔程序猿5 小时前
开天斧 STC8H8K64U低功耗demo
单片机·嵌入式硬件
ACP广源盛139246256737 小时前
GSV5100A@ACP#2 进 2 出 HDMI2.0 中继器 / CAT 延长器,带有音频提取和插入功能
单片机·嵌入式硬件·音视频
d111111111d8 小时前
W25Q60简介--SPI通信(笔记)
笔记·stm32·单片机·嵌入式硬件·学习
Hai小易9 小时前
【STM32单片机】智能家居语音控制系统
stm32·单片机·智能家居·嵌入式软件开发·stm32项目设计·智能家居语音控制系统·c语言编程项目
国科安芯12 小时前
模拟DCDC电源芯片ASP3605S引脚功能及控制环路分析
单片机·嵌入式硬件
ACP广源盛1392462567314 小时前
GSV5100B@ACP#一种具有音频提取和插入功能的 2 进 2 出 HDMI2.0 中继器 / CAT 延长器
单片机·嵌入式硬件·音视频