news 2026/10/20 8:45:12

基于C#的RFID卡识别与读写:从ISO14443A协议到M1卡数据块操作实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于C#的RFID卡识别与读写:从ISO14443A协议到M1卡数据块操作实战

简介:基于C#的RFID卡识别与读写是一份面向.NET开发者的射频识别入门实战资源,涵盖串口通信、事件驱动编程、数据解析及读写器交互等核心环节,适用于门禁系统、物流跟踪等场景。资源共29个文件,压缩包约66KB,以cs源码为主,配合sln工程、csproj配置、resx界面资源、exe可执行程序及pdb调试符号,构成完整的Windows Forms演示项目。已有4660人学习。包内工程结构清晰,含Form1主窗体、Program入口及Properties配置等模块,读者可据此快速搭建RFID实验环境,并通过阅读源码掌握SerialPort串口配置、标签事件监听与EPC数据解析等关键技巧。对于希望将C#与硬件打通的初学者而言,这份资源提供了可运行的参照实现,能有效缩短上手周期。

1. 基于C#的RFID卡识别和读写:先搞清楚这张卡到底藏了什么

很多开发者在 Windows 下第一次接触 RFID 读卡器时,都以为插上 USB 就能像 U 盘一样直接读到卡号,结果一调设备返回的却是错误帧、乱码,甚至毫无反应。RFID 模块不是即插即用设备,读卡只是一系列状态机的流转,尤其是基于 ISO14443A 协议的高频卡,一次完整操作涉及寻卡、防冲突、选卡、认证、读写块五个步骤,任何一个环节时序不对,数据就读不出来。这份基于 C# 的 RFID 卡识别和读写资源,适合做门禁、仓储盘点、会员卡系统的开发者,直接解决了从串口收发到 M1 卡数据块读写这一整条链路,而不只是给你一个“读卡号”的 Demo。

2. 硬件与协议层:高频卡不认识“读卡号”这种指令

2.1 读卡器不是扫描枪:为什么必须先理解 ISO14443A 时序

高频 RFID 读卡器虽然看起来像扫码枪,但它的工作方式完全不同。扫码枪主动成像,RFID 读卡器则是通过电磁场感应供电并交换数据。M1 卡(MF1ICS50)这类高频卡遵循 ISO14443A 协议,读卡器和卡片之间通过 13.56MHz 的载波通信,传输速率默认 106kbps,整个交互过程是半双工的:读卡器先发请求,卡片返回应答,一次完整的操作由多条 APDU 指令串联完成。

在 C# 开发时,如果你直接把厂商提供的“读卡号”函数当成黑盒调用,通常能拿到 UID(4 字节卡号),但一旦需要读写用户数据区,就绕不开底层时序。最常见的情况是调用读块函数返回超时,原因就是前一步没有完成选卡操作,卡片还停留在“防冲突状态”。正确顺序永远是:Request(请求)→ Anticollision(防冲突)→ Select(选卡)→ Authentication(认证)→ Read/Write(读写),跳过任何一环都会失败。

读卡器硬件层面还有一个关键参数:天线驱动电流。很多国产读卡器默认功率偏低,卡片贴近天线边缘时场强不稳定,会导致重传和 CRC 错误。这一点在后面的排错章节会专门展开,开发阶段我一般建议先固定卡片在读写区域中心再调试,排除物理位置干扰。

2.2 M1 卡的数据结构:十六个扇区的存储器映射

M1 卡内部有 1K 字节 EEPROM,分为 16 个扇区,每个扇区 4 个块(Block),每个块 16 字节。第 0 扇区的第 0 块是厂家固化的 UID 和校验字节,出厂只读,任何写入都会失败。每个扇区的第 3 块是控制块(Trailer),存放该扇区的 KeyA、Access Bits、KeyB,控制块的读写权限受 Access Bits 自己管理,改错了整个扇区就废了。

这个结构的直接后果是:你在写数据时,能自由使用的是扇区中第 0、1、2 块,每块 16 字节,共 48 字节。如果业务需要存更多数据,就得规划占用多个扇区。很多新人把卡当成无限存储用,写超了或者越界写入控制块,卡片直接报错,这是读写模块开发中最容易翻车的地方之一。

这张表是 M1 卡的存储布局,开发时我常打印出来贴在工位旁边:

扇区块号内容可用性
00UID(4字节)+ 校验 + 厂商数据只读
01-2用户数据可读写
03KeyA + Access Bits + KeyB受权限控制
1-15每个扇区第0-2块用户数据可读写
1-15每个扇区第3块控制块受权限控制

默认出厂时,所有扇区的 KeyA 和 KeyB 都是 FFFFFFFFFFFF,Access Bits 允许任何密钥认证后用任意密钥读写。如果你拿到批量卡后改了密钥,务必自己留存记录表,卡丢了还能找回数据,密钥忘了就只能当废卡处理。

3. 串口通信与数据帧:把 C# 和读卡器对接的底层约定

3.1 厂商协议帧格式:一条指令的头发、身体、尾巴

市面上大部分串口 RFID 读卡器采用类似的帧结构,我接触过的几家国产模块基本都遵循“帧头 + 设备地址 + 命令字 + 数据长度 + 数据 + 校验”的格式。以我常用的某厂家模块为例,帧头固定 2 字节 0xAA 0xBB,命令字 1 字节,长度 1 字节,数据区和校验字 1 字节异或校验。下面是一个完整的读取块数据请求帧示例:

AA BB 00 03 01 01 00 04

逐字节解释:

  • AA BB:固定帧头,不是每个厂牌都一样,但功能相同
  • 00:读卡器地址,单设备通常填 00,多设备级联时区分用
  • 03:命令字,0x03 定义为寻卡请求
  • 01:数据区长度,表示后面跟一个字节的寻卡模式
  • 01:寻卡模式,1 表示只寻未休眠的卡(对应 ISO14443A 的 Request All 指令)
  • 00 04:异或校验,把前面所有字节异或后得到的结果

异或校验的计算逻辑在 C# 里写出来就一行:

public static byte CalculateChecksum(byte[] data, int startIndex, int length) { byte checksum = 0x00; for (int i = startIndex; i < startIndex + length; i++) { checksum ^= data[i]; } return checksum; }

这段代码把帧头到数据区的每一个字节依次异或,结果作为最后一个字节发送。不要把整帧从帧头开始异或,校验区间是到数据区结束为止,发出去的帧末字节是校验位本身。

3.2 SerialPort 配置与超时策略:参数错了连不上

C# 端使用 System.IO.Ports.SerialPort 类连接读卡器,最常见的错误是认为读卡器像串口打印机一样即插即用。实际上,你需要先确认设备在系统里的 COM 口号,以及波特率、数据位、校验位和停止位,四个参数任何一项不匹配都会导致读到的是乱码帧。

我最常用的一套参数组合是波特率 9600,8 数据位,无校验,1 停止位。部分高速模块支持 115200,但仓库门禁这类场景我坚持用低波特率,抗干扰能力更强。SerialPort 初始化代码:

SerialPort sp = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One); sp.ReadTimeout = 500; sp.WriteTimeout = 500; sp.Open();

ReadTimeout 设置尤其重要。RFID 模块在寻卡时如果天线前没有卡片,会一直不返回数据,默认超时 -1 会无限阻塞线程。500 毫秒超时加上 try-catch 捕获 TimeoutException,是避免界面卡死的底线。

读卡器响应的帧结构里,第一个字节一般是 0xBB,表示帧头响应。很多模块会在响应的数据区返回卡片 UID,具体偏移位置每个厂牌不同,我建议你拿到模块后先用串口助手手动发送寻卡指令,把返回的十六进制数据抄下来再写解析代码,不要直接套网上的例子。曾经有位同事直接照搬另一家模块的偏移量来解析,UID 全是乱的,最后抓包发现响应数据区前两个字节是返回码和长度信息。

4. C# 读写实战:从寻卡到块数据的完整模块化封装

4.1 核心类设计:把设备指令封装成五个方法

拿到模块指令表后,我一般会写一个 RfidReader 类,把重复性高的指令封装成公开方法,业务层只需要调用ReadUid()、ReadBlock()、WriteBlock()就够了。类初始化的时候传入串口号,构造器里完成端口打开,释放时自动关闭,避免资源泄漏。

public class RfidReader : IDisposable { private SerialPort _serialPort; public RfidReader(string portName, int baudRate = 9600) { _serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); _serialPort.Open(); } public void Dispose() { if (_serialPort != null && _serialPort.IsOpen) { _serialPort.Close(); _serialPort.Dispose(); } } }

构造器直接在 new 实例时打开串口,如果串口被其他程序占用,这里就会抛异常,调用方可以根据异常类型给出友好提示。Dispose 模式保证即使业务代码内部出现异常,串口资源也会被释放,避免占用导致的后续重启失败。

4.2 防冲突与寻卡:同时出现两张卡怎么办

开门禁场景最常见的情况是:人员手上同时拿着门禁卡和饭卡,两张都是 M1 卡,同时进入天线场区。如果不做防冲突处理,读卡器会随机锁定一张卡,另外一张读不到。ISO14443A 协议的防冲突机制是通过 CT 级联和 UID 逐位比较来实现的,读卡器发送防冲突指令后,两张卡各自返回 UID 的冲突位,读卡器根据冲突位决定保留哪一张。

模块层面的表现为:发送寻卡指令后返回两段 UID 数据,或者返回防冲突错误。C# 代码里处理的方式就是循环执行寻卡,直到只锁定一张卡:

public byte[] RequestCard() { byte[] requestCmd = { 0xAA, 0xBB, 0x00, 0x03, 0x01, 0x01 }; byte checksum = CalculateChecksum(requestCmd, 0, requestCmd.Length); byte[] fullCmd = requestCmd.Concat(new byte[] { checksum }).ToArray(); _serialPort.Write(fullCmd, 0, fullCmd.Length); byte[] response = ReadResponse(12); if (response[1] == 0x00) // 返回码 0x00 表示成功 { byte[] uid = new byte[4]; Array.Copy(response, 5, uid, 0, 4); return uid; } return null; }

这段代码发送寻卡请求,然后读取 12 字节响应,返回码为 0x00 时提取第 5 到第 8 字节作为 UID。ReadResponse 方法内部处理超时和数据帧完整性校验,如果返回码为 0x02(表示检测到多卡冲突),就需要重新发送寻卡指令,可加一个循环重试,一般重试两三次就能稳定锁定单一卡片。

4.3 认证与写卡:密钥错了就报错 0xE9

读写 M1 卡的数据块,先要认证。认证指令需要一个 6 字节的密钥,常见默认密钥是FF FF FF FF FF FF。认证成功后,才能执行读块或写块命令。写块之前必须确认目标块不是控制块,否则在默认访问位下虽然能写进去,但可能导致控制位参数异常,卡片直接报废。

public bool WriteBlock(byte sector, byte block, byte[] data) { byte blockNumber = (byte)(sector * 4 + block); byte[] authCmd = { 0xAA, 0xBB, 0x00, 0x05, 0x07, blockNumber, 0x60, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; byte authChecksum = CalculateChecksum(authCmd, 0, authCmd.Length); byte[] authFull = authCmd.Concat(new byte[] { authChecksum }).ToArray(); _serialPort.Write(authFull, 0, authFull.Length); byte[] authResp = ReadResponse(8); if (authResp[1] != 0x00) { return false; } byte[] writeCmd = { 0xAA, 0xBB, 0x00, 0x04, 0x11, blockNumber }; writeCmd = writeCmd.Concat(data).ToArray(); byte writeChecksum = CalculateChecksum(writeCmd, 0, writeCmd.Length); byte[] writeFull = writeCmd.Concat(new byte[] { writeChecksum }).ToArray(); _serialPort.Write(writeFull, 0, writeFull.Length); byte[] writeResp = ReadResponse(8); return writeResp[1] == 0x00; }

这里要注意认证指令的数据区,第一个字节是块号,第二个字节 0x60 表示用 KeyA 认证。如果改成 0x61 就是用 KeyB 认证,业务上如果授信给不同部门,可以一个扇区用 KeyA,一个扇区用 KeyB,互不干扰。data 参数必须是 16 字节,调用方传入不足 16 字节时,要补零再发,否则帧长度和数据区对不上,模块会丢弃整帧。

写卡成功与否的判定,有些模块返回码是 0x00,有些是 0xFF,最好以自己手边模块的说明书为准。返回码为 0xE9 时表示认证失败,一半情况是密钥写错或扇区控制块损坏。

5. 避坑指南:串口通信与 M1 卡读写常见问题排查

5.1 现象:寻卡偶尔成功偶尔超时,重试后又稳定了

原因:最常见的是卡片处于天线场区边缘,场强不稳定导致卡片上电复位延迟,卡片和读卡器的通信握手失败。天线的有效读取范围其实很窄,尤其小型桌面模块,有效高度通常不超过 5 厘米。

解决:把卡片固定在天线中心区域,保持静止,不要悬空拿在手上。如果必须在移动场景用,比如产线过卡,就要调高模块发射功率,或者选用天线面积更大的模块,单纯靠代码加重试只会掩盖物理问题。另外注意,卡片和读卡器之间有金属物体时,涡流会吸收电磁能量,导致读卡距离骤降,调试时排除工作台上的金属工具。

5.2 现象:读块返回数据全是 0x00,写块返回成功但读出来不对

原因:没有先认证就直接读了块。很多模块对未认证的读操作不返回错误码,而是返回全零或者上一次缓存的数据,这个设计很坑,因为它不会直接告诉你失败。你的流程跳过了 authentication,或者认证时 KeyA 选错(应该用 KeyA 结果用了 KeyB)。

解决:严格按状态机顺序执行。封装方法时把认证和读写绑定在一个函数里,不要分开暴露给业务层单独调用。认证成功后应立刻进行读写操作,不要穿插其他指令,有些模块认证状态会在其他指令执行后被清除。

5.3 现象:C# 调用 SerialPort.Write 后立即 Read 会抛超时

原因:SerialPort 的 ReadTimeout 生效条件是“在指定时间内没有收到任何字节”。如果读卡器响应的帧被拆成了两段到达,第一段到了但第二段还没到,你按帧长度去读就会阻塞。这个情形在 USB 转串口线缆上特别常见,底层缓冲导致延迟。

解决:读取响应时不要一次性按帧长读,先读固定长度的帧头部分,解析出数据区长度,再根据长度读出剩余部分。就像前面代码里 ReadResponse(int length) 方法,内部建议用两个循环,第一次读 2 字节判断长度,第二次读剩余字节。经验值是把 SerialPort.ReadBufferSize 调大到 4096,减少底层缓冲抖动。

5.4 现象:多个程序同时打开同一 COM 口,直接报 Access Denied

原因:一个串口在同一时刻只允许一个进程独占访问。调试时开着串口助手再启动自己的程序,必然冲突。这不是代码问题,是资源竞争问题。

解决:调试阶段不要同时开多个串口工具,程序崩溃后要确认进程已完全退出。如果程序异常退出后串口没释放,可以在任务管理器里结束残留进程,或者把串口重新插拔一次。我在项目里加了一个硬性规则:程序启动时先尝试打开串口,失败就提示用户关闭其他占用程序,而不是静默重试。

5.5 现象:写块返回成功,但用读卡器再读时数据里多了几个字节 0x00

原因:C# 端发送的 data 数组长度超过 16 字节,或者发送帧长度和实际数据不一致。模块按帧头里的长度字段解析数据区,如果你把 16 字节数据后面还跟了回车换行符,会把换行符解析为下一条指令的帧头,直接造成指令错乱。

解决:发送前检查 data.Length == 16,同时把所有通过 SerialPort.Write 发送的字节统一经过一个指令构造器,在内部强制校验帧长度。这个构造器是我封装 RfidReader 类时最重要的一层,所有命令帧都从它流出,从源头避免手工拼帧的低级错误。

6. 进阶技巧:把 UID 读取和业务数据写入做成事务式操作

很多系统上线后才发现一个典型问题:发卡环节读到了 UID,但业务数据没写进去,导致卡作废重发。根本原因是读 UID 和写数据是两个独立的指令,中间任何失败都会造成数据不一致。我后来把所有发卡动作封装成一个事务式方法,核心思路是写卡之前先读一遍目标块,和期望值比较,写卡成功后再读一次回验,三次确认才返回成功。

public bool IssueCard(int sector, int block, byte[] userData, byte[] fallbackData) { byte targetBlock = (byte)(sector * 4 + block); byte[] oldData = ReadBlock(sector, block); bool hasOldData = oldData != null && oldData.Any(b => b != 0x00); if (hasOldData) { return false; } bool writeOk = WriteBlock(sector, block, userData); if (!writeOk) { return false; } byte[] verifyData = ReadBlock(sector, block); bool verifyOk = verifyData != null && verifyData.SequenceEqual(userData); if (!verifyOk && fallbackData != null) { WriteBlock(sector, block, fallbackData); return false; } return verifyOk; }

这段代码先读旧数据判断块是否已使用,已使用的卡拒绝重复发卡,避免覆盖存量数据。写入成功后立即回读比对,若回读数据和写入数据不一致,判断为硬件写失败,尝试写回 fallbackData 恢复现场。fallbackData 是调用方预留的出厂数据或者空白数据,相当于给写卡操作上了后悔药。这样设计后,发卡系统里再也没出现过写着写着数据丢了的情况。

这个事务式思路对门禁系统尤其重要。读到 UID 后写卡失败,如果直接入库,门禁卡就是一张空卡,发到员工手里根本开不了门。写成回验式之后,写入失败会直接抛异常给前端,提示重新放卡,不给半成品卡入库的机会。

从那以后我每接到一个 RFID 项目,不管是门禁还是仓储,都会强制要求写卡操作带一次回读验证,哪怕模块手册上说写卡成功率 99.9%,剩下那 0.1% 落到真实用户身上就是一次投诉。做开发调试时也习惯了每一步指令前后都打印十六进制帧,帧一乱先比对格式,再查时序,比对着硬件猜快得多。这套流程帮我在若干个模拟项目里少踩了不少坑,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 8:44:35

OpenSim与AnyBody人体肌骨建模全流程:从缩放、逆动力学到肌肉重建

先别急着把标题里的词挨个念一遍。这条标题概括的其实是一套完整的科研工作流&#xff0c;从数据采集到仿真计算再到论文输出&#xff0c;我拆开讲清楚&#xff0c;顺便把中间那些文档里不会写的坑也一起填了。处理运动生物力学数据这件事&#xff0c;近几年越来越多人绕不开Op…

作者头像 李华
网站建设 2026/10/20 8:44:32

EmbeddingGemma 2本地部署指南:多模态嵌入实现图文联合检索

做检索增强应用做久了&#xff0c;你会慢慢发现一个尴尬的阶段&#xff1a;文本检索做得再顺&#xff0c;遇到文档里的截图、产品图、带表格的报告页&#xff0c;基本就束手无策了。EmbeddingGemma 2 这个 740M 参数的多模态嵌入模型&#xff0c;是我最近在本地跑得比较顺手的一…

作者头像 李华
网站建设 2026/10/20 8:43:45

SpringBoot+Vue敬老院管理系统:从数据库设计到部署排错全解析

我最初接触敬老院管理系统这个项目&#xff0c;是帮一个熟人机构的信息科搭内部系统。需求一听并不复杂&#xff1a;老人档案、床位、护理记录、费用结算&#xff0c;再加点统计报表。但真正把“基于SpringBootVueMyBatisMySQL”这套组合从零跑到稳定上线&#xff0c;中间还是踩…

作者头像 李华
网站建设 2026/10/20 8:43:11

Matlab手写RNN与LSTM:从零实现时间序列预测与BPTT训练

简介&#xff1a;一套基于Matlab的RNN-LSTM卷积神经网络实现资料&#xff0c;面向深度学习初学者或需要在Matlab中快速搭建序列预测模型的开发者。资料围绕LSTM输入门、遗忘门、细胞状态与输出门等核心机制展开&#xff0c;配套数据预处理、训练主流程和权重更新三个MATLAB脚本…

作者头像 李华
网站建设 2026/10/20 8:40:55

cuteCOM:Linux下支持可靠文件传输的Qt串口调试工具

简介&#xff1a;这是一份面向Linux嵌入式开发工程师、串口通信学习者及C Qt初学者的开源串口工具源码资源&#xff0c;解决Linux环境下轻量级串口调试、文件传输与固件更新等实际问题。资源为cuteCOM 0.22.0版本完整源码包&#xff08;ZIP格式&#xff0c;14KB&#xff09;&am…

作者头像 李华
网站建设 2026/10/20 8:40:54

合同审查平台怎么选?从智能法律工具到法智研判的实战解析

上周有个创业公司的朋友问我&#xff1a;合同审查平台哪个好&#xff1f;他不是法务&#xff0c;是技术负责人&#xff0c;签合作协议、采购合同全靠自己硬着头皮看。我说你先别急着比参数&#xff0c;得先想明白一件事——你需要的到底是一个能把合同翻译成“人话”的助手&…

作者头像 李华