
简介这份资源是一套基于C#开发的RFID读写器自动读卡程序面向具备一定C#基础、希望学习串口通信与RFID协议解析的开发者可用于IC卡数据读取、展示与管理的实践场景。压缩包共37个文件约169KB以cs源码、exe可执行程序、config配置、pdb调试符号、dll依赖库及resx资源文件为主另含sln解决方案与csproj工程文件结构完整便于直接打开编译与调试。项目采用Windows Forms构建可视化界面通过SerialPort类完成串口通信并涉及MIFARE Classic、UID等RFID协议的数据解析同时包含SqlHelper等数据库操作辅助类覆盖界面设计、设备通信、协议处理与数据存储多个环节。目前已有628人学习下载适合作为C#与RFID技术综合练手的参考案例帮助读者理解读卡器通信流程与工程组织方式。1. 自动读卡版 C# RFID 读写器从串口收包到数据库落盘的完整拆解手上拿到一个叫「自动读卡版」的 C# RFID 读写器源码包第一反应不是急着双击 sln而是先看它到底把哪几件事串起来了。这个项目用 Windows Forms 做界面核心链路是串口收 RFID 读卡器上报的原始字节流按协议解析出 UID 或扇区数据再通过 SqlHelper 落到本地数据集里。它解决的不是「怎么造读卡器」而是「读卡器已经在吐数据了上位机怎么稳定接住、解析、存下来、还能查」。适合两类人一类是做 C# 上位机、考勤、门禁、资产管理这类需要对接 RFID 硬件的开发者另一类是想找一个能跑通的串口加数据库综合案例来练手的人。包里带了 RFIDLabDataSet.xsd 和 SqlHelper.cs说明作者已经把数据访问层和强类型数据集搭好了不是那种只贴个 SerialPort 示例的半成品。2. 串口通信层SerialPort 参数配置与 DataReceived 收包2.1 为什么用 SerialPort 而不是厂商 SDKRFID 读卡器常见的对接方式有两种厂商提供 DLL 或 SDK或者直接走串口。这个项目走的是串口路线原因很实际——串口是通用接口换一个品牌的读卡器只要通信协议对得上上位机代码基本不用大改。厂商 SDK 的问题是绑定硬件型号换设备就得换库维护成本高。用 System.IO.Ports 下的 SerialPort 类波特率、数据位、停止位、校验位这些参数在代码里显式配置调试时能直接看到原始字节出问题好定位。常见做法是读卡器出厂默认波特率 9600 或 115200数据位 8停止位 1无校验。但不同厂家会改所以参数不能写死最好做成配置文件或界面可调。这个项目里 app.config 存在说明作者留了配置入口。2.2 串口初始化的关键参数// 串口初始化参数必须和读卡器实际配置一致否则收到的全是乱码 private SerialPort _serialPort; private void InitSerialPort(string portName) { _serialPort new SerialPort(); _serialPort.PortName portName; // 如 COM3从配置或下拉框读取 _serialPort.BaudRate 9600; // 常见 9600 / 115200必须与读卡器一致 _serialPort.DataBits 8; // 数据位绝大多数读卡器为 8 _serialPort.StopBits StopBits.One; // 停止位1 最常见 _serialPort.Parity Parity.None; // 校验位None 最常见 _serialPort.ReadTimeout 500; // 读超时避免阻塞线程 _serialPort.WriteTimeout 500; // 写超时 _serialPort.DataReceived new SerialDataReceivedEventHandler(DataReceivedHandler); _serialPort.Open(); }这段代码里每个参数都有实际意义。BaudRate 不匹配是最常见的翻车点表现为收到的字节全是 0x00 或乱码。ReadTimeout 设 500ms 是为了防止在读操作上无限等待导致界面卡死。DataReceived 是事件驱动串口有数据进来时自动触发不需要开线程轮询。2.3 DataReceived 里不能直接操作 UI// DataReceived 在非 UI 线程触发直接更新控件会抛跨线程异常 private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { try { int bytesToRead _serialPort.BytesToRead; byte[] buffer new byte[bytesToRead]; _serialPort.Read(buffer, 0, bytesToRead); // 通过 Invoke 切回 UI 线程再更新界面 this.Invoke(new Action(() { string hex BitConverter.ToString(buffer).Replace(-, ); txtRawData.AppendText(hex Environment.NewLine); ParseRfidFrame(buffer); // 解析逻辑单独抽出来 })); } catch (Exception ex) { // 串口异常要记录不能吞掉 LogHelper.Write(串口接收异常: ex.Message); } }这里有个血泪经验DataReceived 回调运行在 ThreadPool 线程上直接碰 TextBox 会报「线程间操作无效」。用 this.Invoke 切回 UI 线程是标准做法。另外 BytesToRead 返回的是当前缓冲区里的字节数读的时候要按这个长度读不能固定读一个大数组否则会读到脏数据。解析逻辑抽成 ParseRfidFrame 单独方法方便后面按协议改。3. RFID 协议解析从原始字节到 UID 和扇区数据3.1 帧结构决定解析方式RFID 读卡器上报的数据不是裸 UID通常带帧头、长度、命令字、数据和校验。不同厂家帧格式不同但结构类似。常见的一种是帧头如 0x02 长度 命令 数据 校验 帧尾如 0x03。解析第一步是找到帧头然后按长度字段截取完整帧再校验最后提取数据段。如果项目里读的是 MIFARE Classic 卡UID 是 4 字节或 7 字节扇区数据是 16 字节一块。解析时要区分「读 UID」和「读扇区」两种命令的返回格式。这个项目叫「自动读卡版」说明它可能是循环读 UID 模式读到卡就上报不需要上位机主动发指令。3.2 一个可复用的帧解析方法// 按帧头帧尾截取完整帧再解析 UID private void ParseRfidFrame(byte[] raw) { // 假设帧格式0x02 LEN CMD DATA... CHECK 0x03 const byte FRAME_HEAD 0x02; const byte FRAME_TAIL 0x03; int headIndex Array.IndexOf(raw, FRAME_HEAD); if (headIndex 0) return; // 没找到帧头丢弃 int tailIndex Array.IndexOf(raw, FRAME_TAIL, headIndex); if (tailIndex 0) return; // 帧不完整等下次数据 int frameLen tailIndex - headIndex 1; byte[] frame new byte[frameLen]; Array.Copy(raw, headIndex, frame, 0, frameLen); // 校验常见为异或校验从 LEN 到 DATA 逐字节异或 byte check 0; for (int i 1; i frameLen - 2; i) check ^ frame[i]; if (check ! frame[frameLen - 2]) { LogHelper.Write(校验失败丢弃帧); return; } // 提取 UID假设 DATA 段前 4 字节为 UID byte[] uid new byte[4]; Array.Copy(frame, 3, uid, 0, 4); string uidStr BitConverter.ToString(uid).Replace(-, ); // 落盘到数据集 SaveCardRecord(uidStr); }这段代码的关键点先找帧头再找帧尾确保拿到完整帧校验不通过直接丢弃不往数据库写脏数据UID 提取位置按实际协议调整不同读卡器 DATA 段偏移不一样。参数上FRAME_HEAD 和 FRAME_TAIL 要根据读卡器手册改校验算法也可能是累加和或 CRC16这里用异或只是常见做法之一。3.3 自动读卡模式下的去重自动读卡意味着同一张卡放在感应区会连续上报。如果每报一次就写一条数据库记录表会迅速膨胀。常见做法是加一个时间窗口去重同一 UID 在 2 秒内只记一次。// 简单去重同一 UID 在 2 秒内不重复入库 private Dictionarystring, DateTime _lastSeen new Dictionarystring, DateTime(); private bool ShouldRecord(string uid) { DateTime now DateTime.Now; if (_lastSeen.ContainsKey(uid)) { if ((now - _lastSeen[uid]).TotalSeconds 2) return false; // 2 秒内重复跳过 } _lastSeen[uid] now; return true; }这个字典会随卡片数量增长长期运行要考虑清理过期项否则内存会慢慢涨。简单做法是定时清理超过 10 分钟没出现的 UID。4. 数据落盘SqlHelper 与强类型数据集的配合4.1 为什么项目里同时有 SqlHelper 和 DataSet打开源码包能看到 SqlHelper.cs 和 RFIDLabDataSet.xsd 并存。SqlHelper 是典型的 ADO.NET 封装提供 ExecuteNonQuery、ExecuteScalar 这类方法适合做增删改查。RFIDLabDataSet 是强类型数据集配合 .xsc 和 .xss 文件说明作者用设计器定义了表结构界面上可能用 DataGridView 直接绑定。两者配合的方式通常是SqlHelper 负责写库DataSet 负责界面展示和缓存。这种组合在 WinForms 项目里很常见好处是界面绑定快缺点是数据集和数据库之间需要手动同步。如果读卡频率高建议直接用 SqlHelper 写库然后刷新 DataGridView而不是往 DataSet 里塞。4.2 插入读卡记录的 SqlHelper 调用// 插入一条读卡记录参数化查询防注入 public void SaveCardRecord(string uid) { string sql INSERT INTO CardRecord (Uid, ReadTime) VALUES (Uid, ReadTime); SqlParameter[] paras new SqlParameter[] { new SqlParameter(Uid, uid), new SqlParameter(ReadTime, DateTime.Now) }; SqlHelper.ExecuteNonQuery(connStr, CommandType.Text, sql, paras); }参数说明connStr 从 app.config 的 connectionStrings 读不要硬编码在代码里。Uid 和 ReadTime 用参数化传递避免拼接字符串。如果数据库是 SQL Server表结构里 Uid 建议加唯一索引或至少加普通索引因为后面查询「某张卡最近一次读卡时间」会频繁用到。4.3 数据集与数据库的同步边界RFIDLabDataSet 里的表如果和数据库表同名同结构可以用 TableAdapter 做 Fill 和 Update。但自动读卡场景下数据是持续写入的用 TableAdapter 的 Update 反而容易冲突。我一般会写库走 SqlHelper界面展示走 DataGridView 绑定 DataTable每次插入后重新查询最近 N 条刷新。这样逻辑清晰不会出现数据集和数据库不一致的玄学问题。5. 避坑与排查串口读卡项目最容易翻车的五个点5.1 现象串口打开成功但收不到任何数据原因波特率、数据位、停止位、校验位中至少一项和读卡器不一致或者串口线是只供电不传数据的劣质线或者读卡器处于被动模式需要上位机先发指令才上报。解决先用串口调试助手单独测读卡器确认参数和主动/被动模式。如果调试助手能收到再对比代码里的参数。线材问题换一根带屏蔽的 USB 转串口线。5.2 现象收到的数据偶尔多几个字节或少几个字节原因DataReceived 触发时数据还没收完BytesToRead 只反映了当前缓冲区里的字节数一帧可能被拆成两次事件。解决不要假设一次事件就是一帧。维护一个接收缓冲区每次把新数据追加进去然后循环查找完整帧。找到完整帧就解析并从缓冲区移除剩余不完整的留着等下次数据。5.3 现象界面卡死点按钮没反应原因在 UI 线程里做了同步串口读或数据库操作或者 DataReceived 里直接更新控件导致跨线程异常后线程挂起。解决串口读用事件驱动数据库写用短连接快速执行耗时操作放 BackgroundWorker 或 Task。所有 UI 更新走 Invoke。5.4 现象数据库里出现重复 UID同一张卡刷一次记了十几条原因自动读卡模式下读卡器连续上报代码没有去重。解决加时间窗口去重如 5.3 节所示。更严谨的做法是读卡器端配置「只上报一次」模式但很多读卡器不支持只能上位机做。5.5 现象换了一台电脑或换了一个 USB 口程序报串口不存在原因串口号写死在代码里换环境后 COM 号变了。解决串口号从 app.config 读或者界面上做下拉框枚举可用串口。枚举用 SerialPort.GetPortNames()启动时自动填充。6. 进阶技巧把读卡记录做成可查询的考勤流水6.1 从裸记录到考勤逻辑读卡记录表里只有 Uid 和 ReadTime这还不是考勤。要变成考勤流水需要一张人员表把 Uid 映射到人名再按天聚合出「最早一次读卡」作为上班时间、「最晚一次」作为下班时间。这个项目本身没带人员管理但 SqlHelper 和 DataSet 的结构留了扩展空间。我一般会加两张表PersonUid, Name, Dept和 AttendancePersonId, Date, FirstIn, LastOut。读卡记录插入后用 SQL 的 GROUP BY 按人按天聚合。-- 按人按天聚合出上下班时间 SELECT p.Name, CAST(r.ReadTime AS DATE) AS WorkDate, MIN(r.ReadTime) AS FirstIn, MAX(r.ReadTime) AS LastOut FROM CardRecord r JOIN Person p ON r.Uid p.Uid GROUP BY p.Name, CAST(r.ReadTime AS DATE) ORDER BY WorkDate DESC, p.Name;这个查询在数据量到几十万条时仍然很快前提是 CardRecord 的 ReadTime 和 Uid 上有索引。如果没索引全表扫描会明显变慢。6.2 验证解析是否正确的一个笨办法协议解析对不对不要靠猜。我习惯在解析方法里加一段临时日志把原始字节和解析结果同时写到一个文本文件里跑一天后拿几张已知卡号的卡去比对。如果原始字节里能看到卡号的 ASCII 或十六进制但解析结果对不上就是偏移量或字节序搞错了。字节序问题在 UID 解析里特别常见有的读卡器低字节在前有的高字节在前差一个 Array.Reverse 结果就完全不同。6.3 长期运行的内存与串口稳定性自动读卡程序可能一开就是几个月。两个地方要注意一是去重字典要定期清理二是串口在异常后要能自动重连。我一般会加一个定时器每 30 秒检查串口 IsOpen如果断了就尝试重新打开并记录日志。数据库连接用 using 包起来确保每次用完就释放不要保持长连接。从那以后我每次拿到串口类项目都强制先跑一遍「串口调试助手对照测试」确认硬件和参数没问题再动代码。这个习惯帮我省掉了至少一半的无效调试时间。希望帮到你。本文还有配套的精品资源点击获取