资讯动态

C# RFID开发实战:从UID读取到扇区读写防坑指南

发布时间:2026/10/17 11:09:27 来源:尧图企业网站定制
简介这是一份面向C#开发者的RFID卡识别与读写实战资源适合需要完成门禁系统、物流跟踪、商品防伪等场景的软硬件集成开发者也适合具备C#基础、想学习串口设备编程的进阶学习者。压缩包共29个文件约66KB核心内容以C#源码为主涵盖项目工程文件、窗体界面代码、资源配置文件以及编译生成的exe和调试记录便于直接打开工程查看完整实现并运行验证。资源围绕RFID读写核心链路展开涉及SerialPort串口通信参数配置、标签事件监听、EPC Gen2数据解析、读写命令构造与异常处理并提供将硬件交互封装为独立类库或服务的思路帮助读者从设备连接、数据解析到底层协议调试形成完整认识。已有4660人学习下载对有C#基础、希望快速上手RFID硬件编程的开发者有较高参考价值。1. 为什么“C# RFID”听起来简单一动手就翻车先回答能不能用1.1 认准一个目标RFID 卡读写的核心是“扇区”和“块”很多开发者拿到 RFID 读卡器第一反应是“把卡号读出来”然后拿着这个卡号去数据库比对。但实际上卡号UID只是这张卡唯一值的一部分你要做的业务数据通常存在卡的存储区里。以最常见的 M1 卡Mifare Classic为例一张卡有 16 个扇区每个扇区有 4 个块每个块能存放 16 字节数据。你要做的读卡本质是“找到扇区、通过密码认证、对指定块读写”你要做的写卡本质是“认证后把字节数组写进某个块”。这个认知不建立起来后面每一步都会踩坑。1.2 把命令封装成对象程序如何“听懂”读卡器说的话C# 程序本身不直接接触射频信号它只是通过串口或 USB 与读卡器模块通信。读卡器模块内部有天线和射频芯片负责向卡片发命令、收卡片响应。所以你的程序要做的事是“按照读卡器厂商定义的协议把命令字节数组发过去再解析返回的字节数组”。常见的做法有两种一是引入厂商提供的 SDK直接调用ReadCard()之类的方法二是自己拼帧通过串口发送十六进制命令再解析响应。绝大多数自制系统走的都是第二种因为 SDK 一般绑定具体硬件而自定义帧协议可以换硬件不换上位机逻辑。2. 原理与选型先搞懂 HF 读卡器、ISO 14443A 和 C# 里的数据通道### 2.1 一张卡片内部的结构怎么理解块、扇区和访问位高频 RFID 卡13.56MHz通常遵循 ISO 14443A 通信协议打交道最多的卡片类型是 M1 和 M1 兼容卡。M1 卡有 1K 字节 EEPROM分为 16 个扇区Sector 0 到 15每个扇区有 4 个块Block 0 到 3。每个块 16 字节其中 Sector 0 的 Block 0 存放 UID 和厂商数据出厂时只读每个扇区的 Block 3 是扇区尾部存放 KeyA、Access Bits、KeyB。实际业务数据一般写在每个扇区的 Block 0、1、2 里因为 Block 3 被密钥和访问控制位占住了。一个字节块能存 16 字节一块能存一个短文本、一个工号加一个时间戳或者半张二维码内容如果数据超过 16 字节就要连续占用多个块。常见误用是把一张 1K 卡当成无限大存储往 Block 15 的尾部写长字符串结果发现写不进去甚至把访问位改坏。容量极限和安全性是选型时必须权衡的取舍点。如果项目要存 512 字节以上的结构化数据建议直接换更大容量的卡或者用 NFC 的 NDEF 格式不要在 M1 卡上硬塞。选型建议见下表。卡类型容量与结构C# 开发注意点M1 Classic 1K16 扇区每扇区 4 块每块 16B认证用 KeyA/KeyB注意扇区尾部访问位M1 Classic 4K40 扇区前 32 扇区同 M1后 8 扇区块数不同块地址映射要重算Ultralight/NTAG64B 到 144B 不等无扇区概念直接用页读写部分型号有 OTP 区注意只读锁定问题CPU 卡 / 金融卡内部有 COS支持钱包圈存通常走 APDU 指令和普通 M1 的读块写块命令完全不同2.2 C# 对接读卡器的两条路线厂商 SDK 和串口帧协议厂商 SDK 的好处是封装了寻卡、防冲突、认证、读写全套流程你只需要调用方法并处理返回结果坏处是 SDK 一般绑定特定厂家的驱动和 DLL换一台读卡器可能就要换一个 SDK而且 SDK 的文档质量参差不齐。自研帧协议的好处是一次实现后续换兼容模块只要改端口号和命令码坏处是要自己搞定通信时序、校验和以及异常重发。我的建议是原型阶段用 SDK 快速跑通产品化阶段再替换成串口帧协议。帧协议的一般构成是“长度字节 命令码 参数 数据 校验”。比如某个市面常见的 13.56MHz 读卡器模块寻卡命令可能长这样以十六进制示意AA 00 03 01 81 00 01 00 匹配上面这串并不是某款具体模块的真实命令只是用来示意帧格式AA是帧头00是设备地址03是命令码01是防冲突级别81是寻卡模式后面的00 01 00是参数区最后的匹配是校验和。实际参数必须以你手上模块的数据手册为准因为每个厂家对命令码的定义都不完全一样。2.3 C# 里的串口通信波特率、数据位、校验位怎么设在 C# 里操作 RFID 读卡器最稳的通道是SerialPort。常见参数是波特率 9600、数据位 8、停止位 1、无校验。很多人会直接写死成 9600但部分读卡器模块出厂波特率是 115200或支持自动波特率。这里有个经验先用厂家附带的调试工具连接模块确认串口参数能正常读到卡再在 C# 里用同样的参数连接。不要一上来就写new SerialPort(COM3, 9600)然后发现收不到任何数据花半天排查读卡器其实只是波特率对不上。异步接收也值得注意DataReceived事件在子线程触发收到的不一定是一整帧可能是半帧、两帧粘连或延迟分包。一个可靠的做法是把收到的字节追加到Listbyte缓冲区然后循环尝试从缓冲区解析完整帧解析成功后移除已消费的数据。等到后面写批量识别时你会发现这个缓冲区设计比调用任何高级 API 都重要。3. 搭开发环境和跑通第一张卡从新建项目到读出 UID3.1 新建一个 C# 控制台项目并串口连接我用 Visual Studio 新建一个 .NET 控制台应用目标框架用 .NET 6 或 .NET 8Windows 下直接用System.IO.Ports。如果要在 Linux 上跑需要额外安装System.IO.PortsNuGet 包。下面是最小示例using System; using System.IO.Ports; class RfidDemo { static SerialPort _port; static void Main(string[] args) { string portName args.Length 0 ? args[0] : COM3; _port new SerialPort(portName, 9600, Parity.None, 8, StopBits.One); _port.DataReceived OnDataReceived; _port.Open(); Console.WriteLine($已打开 {portName}等待读卡……); Console.ReadLine(); } static void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { int n _port.BytesToRead; byte[] buffer new byte[n]; _port.Read(buffer, 0, n); Console.WriteLine(BitConverter.ToString(buffer)); } }这段代码只做一件事把读卡器上报的原始字节打印出来。好处是先用真实数据验证通信链路而不是直接写业务逻辑。你会看到卡片靠近天线时模块会主动上报一段字节卡片离开时又会收到另一段字节。这些字节就是后续程序解析的全部素材。参数说明波特率不一定是 9600先看模块手册DataReceived事件里不要做耗时操作只能把字节收进缓冲区后续解析放到另一个方法里做。3.2 读取 UID 的最小帧寻卡命令的构造与响应解析读到卡的第一步是让模块进入寻卡状态有些模块在卡片靠近时自动上报 UID有些则需要上位机主动发寻卡命令。以后者为例你需要周期性发送寻卡命令然后解析响应。一个示意性的命令帧如下AA 00 03 01 81 00 01 00 CS在 C# 里这样发送byte[] cmd { 0xAA, 0x00, 0x03, 0x01, 0x81, 0x00, 0x01, 0x00, 0xCS }; _port.Write(cmd, 0, cmd.Length);等待大约 100ms模块会返回一帧数据里面包含一个 4 字节的 UID。把这 4 字节转成 HEX 字符串就是最常见的输出格式。不要直接用Encoding.ASCII.GetString去转因为 UID 是原始字节不是文本。正确做法string uidHex BitConverter.ToString(uidBytes).Replace(-, );这条命令里的关键参数是寻卡模式0x81表示一次只找一张卡0x82表示连续寻卡后面的00 01 00是超时参数或防冲突级别。不同模块差异很大务必对照手册核对。初次调通后把收到的响应帧和手册提供的示例响应做一次逐字节对比这是最直接的排查方式。3.3 把读卡流程封装成方法重复请求、超时和返回值约定实际项目里你不可能只读一次卡。要把“请求寻卡 → 等待响应 → 解析 UID”封装成一个可复用方法。这里有个容易踩坑的地方模块响应是异步的方法等待时需要超时控制。常见做法是在收到完整帧时用一个事件或任务完成信号通知而不是Thread.Sleep后直接读缓冲区。static Taskbyte[] SendCommand(byte[] cmd, int expectLen, int timeoutMs) { return Task.Run(() { _port.Write(cmd, 0, cmd.Length); byte[] buffer new byte[64]; int offset 0; int deadline Environment.TickCount timeoutMs; while (Environment.TickCount deadline offset expectLen) { if (_port.BytesToRead 0) { int n _port.Read(buffer, offset, Math.Min(_port.BytesToRead, buffer.Length - offset)); offset n; } else { Thread.Sleep(20); } } if (offset expectLen) throw new TimeoutException($读卡器响应超时只收到 {offset} 字节); return buffer.Take(offset).ToArray(); }); }调用方式byte[] response await SendCommand(cmd, 9, 500);这个方法解决了串口通信里的三个常见问题半帧、粘帧和超时。timeoutMs建议设置 300 到 800ms太短会导致卡片偶尔响应慢时误判失败太长会拖慢批量识别。等到后面做一卡一档的排队读写时你会意识到超时设计直接影响整条产线的节拍。4. 从 UID 到数据扇区认证、读块写块与完整时序4.1 读块与写块块地址映射和 16 字节对齐M1 卡的块地址不是连续的扇区号而是按“扇区号 × 4 块内偏移”计算。比如 Sector 5 的 Block 0 地址是5 * 4 0 20Block 1 是 21Block 2 是 22Block 3 是 23。如果你用手册上的“扇区块”的描述去直接发命令命令码参数里填的却必须是绝对块地址这里极易出错。很多翻车现场就是“读第 5 扇区第 1 块”填成了5, 1读出来全 0还以为卡坏了。同时读写数据的长度是固定的 16 字节。不足 16 字节要补齐超过 16 字节要拆成多个块。举例说明要写一个 14 字节的字符串“Hello RFID!”长度为 12加上两个字节的版本号仍然不足 16必须补到 16 字节如果写入 32 字节内容建议直接分两块分别写进 Block 0 和 Block 1。4.2 扇区认证KeyA、KeyB 和访问控制位的关系在读任何一个数据块之前必须先认证该扇区。M1 卡每个扇区尾部存储 6 字节 KeyA4 字节 Access Bits6 字节 KeyB。认证时上位机把 KeyA 或 KeyB 发给模块模块和卡片做密钥交互认证通过后该扇区的数据块才允许读写。注意认证的粒度是扇区不是块。也就是说你认证通过 Sector 3只能对 Sector 3 的四个块做读写要操作 Sector 4 必须另外认证。常见的认证参数是密钥字节数组的长度为 6默认出厂密钥通常是FF FF FF FF FF FF或A0 A1 A2 A3 A4 A5。有些卡被改写后默认密钥失效这时必须知道卡的当前密钥。还有一个经验不要用00 00 00 00 00 00当密钥很多设备厂商出厂样例里会禁用全零密钥导致某些模块认证时报错。// 以一个常见SDK风格为例示意认证与读块代码 bool authOk reader.Authenticate(sector: 5, keyType: KeyType.KeyA, key: new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }); if (!authOk) { Console.WriteLine(扇区5认证失败); return; } byte[] blockData reader.ReadBlock(blockNumber: 20);参数说明sector和blockNumber不要混用读取的blockData固定是 16 字节认证失败的错误码在不同模块里含义完全不同有的是返回 0x01有的是返回 0x15要在前期把模块返回码整理成一张映射表。4.3 完整时序寻卡 → 选卡 → 认证 → 读/写 → 休眠工业场景下读写卡是一个完整时序稍有不慎就会把卡写坏或者写一半。正确流程是防冲突寻卡拿到 UID → 用 UID 执行选卡 → 认证指定扇区 → 对目标块读写 → 让卡片退场Halt。直接在寻卡后立刻读写某些模块也能工作但在卡片靠近多张、或有金属干扰的环境下会不稳定。我一般这样封装一个完整流程async Taskbool WriteCardAsync(string sectorData) { try { byte[] uid await FindCardAsync(); // 寻卡 byte[] data Encoding.UTF8.GetBytes(sectorData); Array.Resize(ref data, 16); // 补齐16字节 if (!AuthSector(5, KeyType.KeyA, DefaultKey)) return false; bool ok WriteBlock(20, data); // 写20块 HaltCard(); // 让卡片退出 return ok; } catch (TimeoutException ex) { Console.WriteLine($写卡超时: {ex.Message}); return false; } }这段代码的关键不在 API 长什么样而在时序顺序。HaltCard()容易被忽略但它是防止卡片反复被同一张卡触发的关键Array.Resize用来把不足 16 字节的字符串补齐避免把脏数据写进卡。5. RFID 开发避坑现象、原因和修复路径集合5.1 同一张卡读出的 UID 偶尔不同问题在防冲突命令现象一张 M1 卡放在读卡器上程序打出来的 UID 偶尔是对的偶尔是乱的比如最后一个字节变成FF或整体多出一个字节。原因模块处于连续寻卡模式防冲突命令没有选好卡导致两张卡同时在场参与轮询返回的 UID 是混合结果。也有一种情况是模块在某些固件版本下读到的是“卡序列号的一部分”而不是完整 UID。解决把连续寻卡模式改成单次寻卡模式确保每次操作前先 Halt让前一张卡彻底退场再去寻下一张卡。如果模块支持硬件防冲突优先打开该功能如果不支持就要在程序里限制同一时间只有一张卡贴近天线。5.2 能读不能写访问控制位把数据块锁死了现象认证成功读块数据正常但写块失败模块返回错误码。原因M1 卡的块 3 里的 Access Bits 定义了这个扇区KeyA/KeyB和三个数据块的读写权限。很多人用第三方软件修改过访问位把数据块设成了只读或者把写权限绑定在 KeyB 上而程序一直用 KeyA 认证。解决先用读卡器工具读扇区尾部把 Access Bits 解析出来确认当前块的实际权限矩阵。如果是绑定 KeyB就把认证密钥从 KeyA 换成 KeyB。不要试图在没备份的情况下乱改 Access Bits一旦写错整个扇区永久不可访问从而报废一张卡。5.3 读卡距离近到贴脸问题在天线参数而不是 C# 代码现象同样的模块在一台设备上能读 5cm在另一台设备上要贴到天线外壳才能读出来。原因RFID 模块的天线匹配电路和线圈尺寸直接影响读卡距离。程序里的读写命令几乎不会改变射频功率主要改动空间是模块寄存器里的发射功率档位。解决检查模块是否有多级功率配置如果模块外接天线确认天线的电感和电容匹配然后才是考虑 C# 端把寻卡超时调长。实践中天线位置只要偏移几毫米距离可能从 6cm 掉到 1cm这属于射频硬件问题和 C# 代码无关别在程序里浪费一整天。5.4 串口被多个逻辑抢占碰了不重启就卡死现象程序跑了一段时间后串口无法读写任务管理器显示端口被占用重启程序也不行。原因之前的进程没有关闭SerialPort或者DataReceived事件里抛了异常没有捕获导致串口对象处于半死状态。解决在程序退出时写清晰的处理方法protected override void Dispose(bool disposing) { if (disposing) { _port.DataReceived - OnDataReceived; if (_port.IsOpen) _port.Close(); _port.Dispose(); } base.Dispose(disposing); }同时把DataReceived里的所有代码包在try-catch里捕获任何异常并记录日志不要让事件处理函数带着异常返回。这个习惯能省掉很多“端口莫名消失”的玄学问题。5.5 卡片提示成功但数据是脏的重置缓冲区别偷懒现象写卡时提示成功再读回来发现前几个字节是对的后面字节是上一次的旧内容。原因写入数据没有补齐 16 字节模块把旧数据残留了或者卡片写块成功但缓存区里留了上一次读的数据没有清除。解决WriteBlock前总是构造一个长度固定为 16 的字节数组用 0 或空格填充读卡结果放进新数组而不是复用容器。后续再遇到数据“看着像新但带着旧尾巴”均先怀疑缓冲区复用问题。6. 让它真正能用异步与批量读写的落地技巧6.1 把串口读写封装成异步队列Rfid 项目一旦进入真实生产环境你会发现高频读卡模块最怕的不是读不到卡而是程序因为一次异常响应就卡死。把SendCommand改成异步加队列模式可以让每次寻卡读写都独立失败、独立重试。做法是维护一个待处理命令队列后台线程逐条发送每条命令单独设置超时主线程通过async/await拿结果超时或失败不影响下一次操作。这样单张卡操作失败的时候产线可以继续跑不会整条停下来等一张坏卡。我一般给每个模块单独开一个发送线程线程内循环处理队列避免多个线程同时写同一个SerialPort。注意SerialPort不是线程安全的同时写入会导致数据交沓造成模块误识别。6.2 缩短单卡操作时间的策略如果做批量识别比如一秒钟要读几十张卡需要把注意力放在“减少单卡操作次数”上。具体做法是读卡时只做防冲突和读 UID不要每次读 UID 都执行完整的选卡认证流程。等用户后续确认要写入时再进行扇区认证和数据读写。另一个有效技巧是把连续读多卡的循环放到一个后台线程里用信号量控制节拍而不是每读一张卡就await Task.Delay(200)。很多模块的实际响应在几十毫秒内就能完成程序里加延时往往是自己给自己设置的限制。6.3 一个可以立刻执行的验证方法当你跟着上面的流程写完基础代码可以用一张空白的 M1 卡做一个回环验证写入一批标志字节再读回来比对是否一致。比对通过说明链路完整。这里给出一个简单的验证思路表格验证步骤操作预期结果写前读读取目标块原始数据全 0 或旧数据写入数据写入 16 字节内容模块返回成功读后验证再次读取同一块字节完全一致断电验证断电 5 秒重新读数据持久存在如果第二步成功但第三步不一致优先检查是否读到不同扇区或块地址写错。如果第四步数据丢失基本可以确定是设备供电不稳或卡片质量有问题。这个回环验证是我调试某个模拟项目 X 时养成的一个习惯每次换个读卡器品牌我都先用这个方式确认数据链路再去写业务代码。今天的经验是RFID 读写的坑大多不在 C# 语言本身而在于帧时序、密钥权限和硬件环境。把每个环节先拆开测一遍再组装完整流程能帮你省下大量对接时的排查时间。希望这组笔记对你接下来的开发实践有帮助。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑