跳转到内容

RFID标签UID读取

RFID标签UID读取

本节介绍从 RC522 模块读取和验证 RFID 标签 UID(唯一标识符)的相关内容,包括标签区分、UID 字符串格式化以及实现标签白名单逻辑。学习完本节后,您将能够:

  • 以十六进制字符串形式读取和解析 RFID 标签 UID
  • 通过 UID 区分多个标签
  • 使用白名单/黑名单逻辑实现标签验证
  • 处理常见的 UID 读取边缘情况

开始本节前,请确保您已完成:

  • RC522 模块已接线并验证(05-02、05-03)
  • MFRC522 库已安装
  • 至少两个不同的 RFID 标签/卡用于测试
  • 基本草稿模板已就绪

每个 RFID 标签在制造时都有一个唯一的标识符(UID)烧录在芯片中:

MIFARE Classic 1K 的 UID 结构:
┌─────────┬─────────┬─────────┬─────────┐
│ 字节 0 │ 字节 1 │ 字节 2 │ 字节 3 │
├─────────┼─────────┼─────────┼─────────┤
│ 0x04 │ 0xA3 │ 0xB2 │ 0xC1 │
└─────────┴─────────┴─────────┴─────────┘
→ 完整 UID 字符串:"04A3B2C1"(4 字节 UID,8 个十六进制字符)
→ 完整 UID 字符串:"045A3B2C1D"(5 字节 UID,10 个十六进制字符)

关于 UID 的重要事实

  • UID 不加密——它们是只读标识符
  • 大多数 MIFARE Classic 标签具有 4 字节 UID
  • 较新的标签(NTAG、MIFARE Ultralight)可能具有 7 字节 UID
  • UID 每个标签唯一,但存在克隆可能(安全性限制)
  • UID 可以印在标签本体上以便识别

RC522 支持多种标签/卡类型:

类型UID 大小典型用途成本
MIFARE Classic 1K(S50)4 字节门禁控制、交通约 $0.50
MIFARE Classic 4K(S70)4 字节复杂应用约 $1.00
MIFARE Ultralight7 字节一次性使用、活动票务约 $0.20
NTAG213/215/2167 字节NFC 标签、产品标签约 $0.30
MIFARE DESFire7 字节高安全性约 $3.00

步骤 1:以十六进制字符串读取 UID

Section titled “步骤 1:以十六进制字符串读取 UID”

创建一个读取标签 UID 并将其转换为十六进制字符串的函数:

#include <SPI.h>
#include <MFRC522.h>
#define RST_PIN 4
#define SS_PIN 5
MFRC522 rfid(SS_PIN, RST_PIN);
/**
* 读取 RFID 标签 UID 并返回十六进制字符串
* 如果未检测到标签则返回空字符串
*/
String readTagUID() {
// 检查是否有新卡出现
if (!rfid.PICC_IsNewCardPresent()) {
return "";
}
// 读取卡的序列号
if (!rfid.PICC_ReadCardSerial()) {
return "";
}
// 构建 UID 字符串
String uid = "";
for (byte i = 0; i < rfid.uid.size; i++) {
if (rfid.uid.uidByte[i] < 0x10) {
uid += "0"; // 单位数十六进制数字前面补零
}
uid += String(rfid.uid.uidByte[i], HEX);
}
uid.toUpperCase();
// 停止通信
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
return uid;
}
void setup() {
Serial.begin(115200);
SPI.begin();
rfid.PCD_Init();
Serial.println("RFID 标签读取器就绪");
Serial.println("将标签靠近读取器...");
Serial.println();
}
void loop() {
String uid = readTagUID();
if (uid.length() > 0) {
Serial.print("检测到标签!UID:");
Serial.print(uid);
Serial.print(" | 长度:");
Serial.print(uid.length());
Serial.println(" 个十六进制字符");
delay(2000); // 防止快速重复读取
}
}

预期输出

RFID 标签读取器就绪
将标签靠近读取器...
检测到标签!UID:04A3B2C1 | 长度:8 个十六进制字符
检测到标签!UID:E5F8123456 | 长度:10 个十六进制字符

对于资产追踪系统,只应接受已知/已注册的标签:

// 定义已知标签(白名单)
// 将这些替换为您的实际标签 UID
const String KNOWN_TAGS[] = {
"04A3B2C1", // 员工 A / 资产 1
"E5F8123456", // 员工 B / 资产 2
"1A2B3C4D" // 员工 C / 资产 3
};
const int TAG_COUNT = sizeof(KNOWN_TAGS) / sizeof(KNOWN_TAGS[0]);
// 标签所有者名称(用于识别)
const String TAG_NAMES[] = {
"ESP32_TEAM", // 员工 A
"BLANK_TAG", // 员工 B
"DEVICE_C" // 员工 C
};
bool isKnownTag(String uid) {
for (int i = 0; i < TAG_COUNT; i++) {
if (uid == KNOWN_TAGS[i]) {
return true;
}
}
return false;
}
String getTagName(String uid) {
for (int i = 0; i < TAG_COUNT; i++) {
if (uid == KNOWN_TAGS[i]) {
return TAG_NAMES[i];
}
}
return "未知";
}
void loop() {
String uid = readTagUID();
if (uid.length() > 0) {
if (isKnownTag(uid)) {
String name = getTagName(uid);
Serial.print("✅ 欢迎,");
Serial.print(name);
Serial.print("!(UID:");
Serial.print(uid);
Serial.println(")");
} else {
Serial.print("❌ 未知标签:");
Serial.println(uid);
}
delay(2000);
}
}

在首次设置时,您可能需要注册未知标签:

void loop() {
String uid = readTagUID();
if (uid.length() > 0) {
if (isKnownTag(uid)) {
String name = getTagName(uid);
Serial.print("已知用户:");
Serial.println(name);
} else {
// 注册新标签
Serial.print("检测到新标签!UID:");
Serial.println(uid);
Serial.println("输入 'register <name>' 注册此标签");
// 在实际系统中,这将发布到 MQTT
// 让 Node-RED 处理注册
}
delay(2000);
}
}

当系统使用多个标签时,以不同方式处理每个标签非常重要:

// 扩展标签处理,带角色
enum TagAction {
ACTION_CHECK_IN,
ACTION_CHECK_OUT,
ACTION_ACCESS_DENIED
};
struct TagInfo {
String uid;
String name;
bool isActive; // 当前是否已签到
};
TagInfo tags[] = {
{"04A3B2C1", "ESP32_TEAM", false},
{"E5F8123456", "BLANK_TAG", false}
};
const int TAG_INFO_COUNT = sizeof(tags) / sizeof(tags[0]);
TagAction processTag(String uid) {
for (int i = 0; i < TAG_INFO_COUNT; i++) {
if (uid == tags[i].uid) {
if (!tags[i].isActive) {
// 当前已签退 → 执行签到
tags[i].isActive = true;
Serial.print(tags[i].name);
Serial.println(" 已签到");
return ACTION_CHECK_IN;
} else {
// 当前已签到 → 执行签退
tags[i].isActive = false;
Serial.print(tags[i].name);
Serial.println(" 已签退");
return ACTION_CHECK_OUT;
}
}
}
return ACTION_ACCESS_DENIED;
}

测试 UID 读取功能:

// 测试脚本
// 1. 上传基础 UID 读取草稿
// 2. 以 115200 波特率打开串口监视器
// 3. 将标签 #1 靠近读取器
// 预期:
// 检测到标签!UID:04A3B2C1 | 长度:8 个十六进制字符
// ✅ 欢迎,ESP32_TEAM!(UID:04A3B2C1)
// 4. 将标签 #2 靠近读取器
// 预期:
// 检测到标签!UID:E5F8123456 | 长度:10 个十六进制字符
// ✅ 欢迎,BLANK_TAG!(UID:E5F8123456)
// 5. 放置一个未注册的标签
// 预期:
// 检测到标签!UID:9B8C7D6E | 长度:8 个十六进制字符
// ❌ 未知标签:9B8C7D6E

验证清单

  • 每个标签返回唯一的 UID
  • 已知标签被正确按名称识别
  • 未知标签被拒绝并显示错误消息
  • UID 字符串格式一致(大写,无空格)
  • 重复扫描时标签操作在签到和签退之间切换

症状:UID 字符串显示类似 FFFFFFA3B2C1 的值

原因:Arduino 中有符号字节解释导致的负字节值

解决方案

// 使用正确的十六进制格式化以避免符号扩展
String uid = "";
for (byte i = 0; i < rfid.uid.size; i++) {
char hex[3];
sprintf(hex, "%02X", rfid.uid.uidByte[i]); // 始终 2 个字符
uid += hex;
}

问题 2:同一标签显示不同的 UID

Section titled “问题 2:同一标签显示不同的 UID”

症状:同一标签有时返回不同的 UID 字符串

原因:读取错误或 CRC 不匹配

解决方案

  1. 检查天线连接
  2. 减少读取距离
  3. 添加读取验证:
bool readVerifiedUID(String &outUid) {
String firstRead = readTagUID();
delay(100);
String secondRead = readTagUID();
if (firstRead == secondRead && firstRead.length() > 0) {
outUid = firstRead;
return true;
}
return false;
}

症状:两个不同的标签被识别为同一个

原因:只使用了 7 字节 UID 的最后 4 个字节(截断)

解决方案

// 始终使用完整的 UID
String uid = "";
for (byte i = 0; i < rfid.uid.size; i++) {
uid += String(rfid.uid.uidByte[i], HEX);
}
// 不要截断为最后 4 个字节
  • 建议:将 UID 存储为大写十六进制字符串以确保一致性
  • 建议:使用白名单方法以确保安全(拒绝未知标签)
  • 建议:读取后始终停止卡片通信(PICC_HaltA()
  • 避免:将 UID 用于安全关键型认证(UID 可被克隆)
  • 避免:假设所有标签都有 4 字节 UID——处理 4 字节和 7 字节两种变体
  1. UID 读取:按顺序使用 PICC_IsNewCardPresent() + PICC_ReadCardSerial()
  2. UID 格式:4 字节(8 个十六进制字符)或 7 字节(14 个十六进制字符),始终使用完整 UID
  3. 标签验证:为已知标签实现白名单,拒绝未知标签
  4. 状态追踪:追踪每个标签的签到/签退状态以实现切换逻辑