简介:这份资源面向具备一定C#基础、希望入门RFID硬件交互的开发者,围绕射频识别卡的读取与写入展开,可用于门禁、物流跟踪、商品防伪等场景的原型搭建与学习。压缩包共29个文件,约66KB,以cs源码文件为核心,配合sln解决方案、csproj项目配置、resx资源文件及exe可执行程序,另有缓存与日志类文件,构成一个可直接打开运行的完整工程。已有4662人学习下载,说明其在同类入门资料中具备一定参考价值。代码覆盖串口连接配置、事件驱动监听、标签数据解析、读写命令下发与错误处理等关键环节,并演示了将硬件交互封装为独立类库、再由界面层调用的分层思路,便于读者理解C#与RFID阅读器通信的完整链路,也方便日后替换硬件时减少业务代码改动。
1. 从一张门禁卡说起:这套 C# RFID 读写代码到底能干什么
手里有一张 13.56MHz 的 M1 卡,想用 C# 读出它的 UID、扇区数据,甚至改写块内容,很多人第一反应是去搜“C# RFID 读写源码”,结果下载下来一堆跑不起来的半成品。这套基于 C# 的 RFID 卡识别与读写资源,解决的就是这个落地问题:它把串口通信、寻卡、防冲突、密钥认证、扇区读写这几步串成一条可运行的链路,适合做门禁原型、考勤终端、资产标签管理的开发者直接拆用。读者对象很明确——会用 Visual Studio、懂一点串口但没系统做过 RFID 的 .NET 从业者。下面按“它是什么、怎么跑通、参数怎么调、哪里会翻车”的顺序拆,能照着复现。
2. 拆开代码包看通信链路:串口帧、寻卡与密钥认证
RFID 读写不是调一个 API 就出结果,底层是读卡器模块和上位机之间的一问一答。这套代码的核心价值在于把协议帧的拼装和解析做成了可复用的类,而不是把命令散落在窗体事件里。先把这个链路讲清楚,后面改参数、排故障才有依据。
2.1 串口帧结构:帧头、长度、命令字与校验
常见做法是读卡器走 UART,波特率 9600 或 115200,8 数据位、1 停止位、无校验。一帧典型结构是:帧头(如 0xAA 0xBB)、长度、命令字、数据域、校验和。校验多为累加和取低字节,少数模块用 CRC16。代码里一般会封装一个BuildFrame(byte cmd, byte[] data)和一个ParseFrame(byte[] buffer),收发都走这两个入口。
// 帧拼装:帧头 + 长度 + 命令 + 数据 + 累加和校验 private byte[] BuildFrame(byte cmd, byte[] data) { var len = (byte)(data.Length + 1); // 命令字 + 数据长度 var frame = new byte[4 + data.Length + 1]; frame[0] = 0xAA; // 帧头高字节 frame[1] = 0xBB; // 帧头低字节 frame[2] = len; // 长度域 frame[3] = cmd; // 命令字,如 0x20 寻卡 Array.Copy(data, 0, frame, 4, data.Length); byte sum = 0; for (int i = 2; i < frame.Length - 1; i++) sum += frame[i]; frame[frame.Length - 1] = sum; // 累加和,只算长度到数据域 return frame; }这段代码的关键在长度域和校验范围:长度从命令字算起,校验从长度域算到数据域末尾,帧头不参与。不同模块这两处定义经常不一样,拿到新模块第一件事就是翻手册核对,别直接套。串口打开时把ReadTimeout设成 200ms 左右,避免读不到数据时线程卡死。
2.2 寻卡与防冲突:一次只认一张还是多张
寻卡命令(常见 0x20 或 0x52)返回卡片的 UID 和类型(M1、CPU 卡等)。多卡同时进场时要做防冲突,代码里通常用Request加Anticollision两步:先请求所有卡应答,再逐位比较序列号筛出唯一一张。单卡场景可以跳过防冲突直接读 UID,速度快但多卡会乱。
// 寻卡并取 UID,单卡模式 public string ReadUid() { var resp = SendAndReceive(BuildFrame(0x20, new byte[] { 0x01 })); if (resp == null || resp.Length < 8) return null; // 假设返回:帧头2 + 长度1 + 命令1 + 状态1 + UID4 + 校验1 if (resp[4] != 0x00) return null; // 状态非 0 表示无卡或失败 return BitConverter.ToString(resp, 5, 4).Replace("-", ""); }SendAndReceive里要做超时重试,一般重试 2 到 3 次,间隔 50ms。状态字节非 0 时不要当异常抛,直接返回空让上层轮询,门禁场景里“没卡”是常态不是错误。UID 拿到后建议做大小端转换确认,有些模块返回的是反序,直接拿去比对会一直不匹配。
2.3 密钥认证与扇区读写:M1 卡的 16 个扇区怎么动
M1 卡分 16 个扇区,每扇区 4 块,每块 16 字节,最后一块是控制块存密钥和权限。读写前必须先用密钥(Key A 或 Key B)认证对应扇区,认证命令常见 0x60(Key A)/0x61(Key B),后面跟块号和 6 字节密钥。
// 认证扇区 1(块 4),Key A 默认全 FF public bool AuthSector(byte block, byte[] keyA) { var data = new byte[7]; data[0] = 0x60; // 0x60=KeyA, 0x61=KeyB data[1] = block; Array.Copy(keyA, 0, data, 2, 6); var resp = SendAndReceive(BuildFrame(0x40, data)); return resp != null && resp[4] == 0x00; }认证通过后才能读块(0x30)或写块(0xA0)。写块前务必确认目标块不是控制块,误写控制块会把扇区锁死,这张卡基本就废了。默认密钥全 FF 只适合测试,正式项目要在发卡环节改成自定义密钥并记录,否则等于门禁形同虚设。读到的 16 字节按需解析,做门禁一般只取 UID 或某一块的卡号字段,不必整卡 dump。
3. 从零跑通一次读写:环境、连接与第一个 UID
代码能编译不等于能读到卡,中间隔着驱动、端口、模块供电几道坎。这一章按实际动手顺序走一遍,目标是让读者在自己的机器上看到第一串 UID。
3.1 环境准备与串口枚举
先确认 .NET 版本和串口库。老项目多用System.IO.Ports,.NET Core 之后需要单独引System.IO.PortsNuGet 包。读卡器插上后设备管理器里会出现 COM 口,代码里不要写死COM3,用SerialPort.GetPortNames()枚举让用户选。
using System.IO.Ports; var ports = SerialPort.GetPortNames(); foreach (var p in ports) Console.WriteLine($"可用串口: {p}"); var port = new SerialPort("COM3", 115200, Parity.None, 8, StopBits.One) { ReadTimeout = 200, WriteTimeout = 200 }; port.Open();波特率必须和模块一致,9600 与 115200 混用是最常见的“打开了但没反应”。如果枚举不到端口,先查 USB 转串口驱动,再看模块是否独立供电——很多模块靠 USB 供电带不动射频部分,表现为能发命令但寻不到卡。
3.2 连接、寻卡、读块的完整调用顺序
一次完整读卡流程是:打开串口 → 寻卡取 UID → 选卡 → 认证扇区 → 读块 → 关串口。选卡(Select)这步容易被省,但部分模块不选卡直接认证会返回失败。下面把主流程串起来。
public string ReadBlock(byte block, byte[] keyA) { if (!AuthSector(block, keyA)) return null; // 先认证 var resp = SendAndReceive(BuildFrame(0x30, new byte[] { block })); if (resp == null || resp[4] != 0x00) return null; return BitConverter.ToString(resp, 5, 16).Replace("-", ""); } // 主流程 port.Open(); var uid = ReadUid(); if (uid != null) { Console.WriteLine($"UID: {uid}"); var block4 = ReadBlock(4, new byte[] { 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF }); Console.WriteLine($"块4: {block4}"); } port.Close();调用顺序不能乱:认证是对某个块所在扇区生效的,换扇区要重新认证。读块返回的 16 字节里,做卡号通常取前 4 字节做转换,具体偏移看发卡规则。调试阶段建议每步打日志,把发出的帧和收到的帧都按十六进制打出来,出问题时一眼能看出是发错还是收错。
3.3 参数怎么改:波特率、超时与重试
三个参数决定稳定性:波特率、读超时、重试次数。波特率跟模块手册走,改完两边必须同步;读超时 200ms 适合近距离单卡,远距离或弱场可放到 500ms;重试 2 到 3 次足够,再多会拖慢轮询节奏。
| 参数 | 常用值 | 调整依据 | 改错的典型现象 |
|---|---|---|---|
| 波特率 | 9600 / 115200 | 模块手册 | 打开成功但无任何返回 |
| 读超时 | 200~500ms | 读卡距离、卡片类型 | 偶发读不到、线程卡顿 |
| 重试次数 | 2~3 | 现场干扰程度 | 响应变慢、重复触发 |
| 认证密钥 | 测试全 FF | 发卡规则 | 认证失败、读块返回错误 |
轮询场景下别在主线程里死循环读,放到后台线程或定时器里,间隔 100 到 200ms,避免 UI 卡死。多读卡器并存时每个实例独立持有自己的SerialPort,不要共享。
4. 避坑与排查:那些让读卡器“时好时坏”的细节
RFID 现场问题八成不是代码逻辑错,而是电气、协议细节和卡片状态。下面几条是实际拆项目时反复遇到的,按现象、原因、解决写清楚。
4.1 现象:能寻到卡但认证一直失败
原因多半是密钥或块号对不上。M1 卡每个扇区独立密钥,测试卡常是全 FF,但有些出厂卡已改过密钥;另外认证传的是块号不是扇区号,传成扇区号会认证到错误位置。解决:先用全 FF 试,失败就换 Key B,再不行用已知密钥的空白卡对照;确认块号落在目标扇区内(扇区 n 的块号是 4n 到 4n+3)。
4.2 现象:读到的 UID 每次顺序不一样
原因是大小端或字节序没统一。不同模块返回 UID 的字节顺序可能相反,代码里直接BitConverter转字符串,换模块就对不上。解决:在ReadUid里统一做一次字节反转开关,用配置项控制,入库前固定成一种顺序,别在业务层到处转。
4.3 现象:连续读几分钟后串口假死
原因是接收缓冲区没清干净或异常没释放端口。上一帧残留数据会让下一帧解析错位,久而久之缓冲区溢出。解决:每次SendAndReceive前DiscardInBuffer(),异常路径用try/finally保证Close();长时间运行加一个看门狗,连续 N 次失败就重开端口。
4.4 现象:写块成功但卡行为异常
原因是误写了控制块或写了非法权限位。控制块(每扇区第 4 块)存密钥和访问位,写错会把扇区设成只读甚至锁死。解决:写之前判断block % 4 != 3,控制块操作单独走高风险流程并二次确认;正式发卡前先在空白卡上验证权限配置。
提示:调试期把每帧收发都按十六进制落日志,比单步调试快得多,现场排障也靠它。
5. 进阶:把读写封装成可复用服务并做稳定性验证
代码跑通只是起点,真正上线要把寻卡、认证、读写收成一个服务类,对外只暴露TryReadCard(out CardInfo)这类方法,内部处理重试、端口重连和日志。我一般会加一个简单的压测:连续寻卡 1000 次,统计成功率和平均耗时,成功率低于 98% 就回头查供电和天线位置。验证密钥时用两张卡对照——一张全 FF 测试卡、一张已改密钥的正式卡,确保认证分支都走到。还有个习惯:每次换读卡器模块,先只跑寻卡和读 UID,确认帧格式对上了再动扇区读写,别一上来就整卡操作。这套流程走下来,后面接门禁业务逻辑基本不会再被底层通信拖住。希望帮到你。
本文还有配套的精品资源,点击获取