简介这份C# IC卡读写实例源码面向具备一定C#基础的开发者与智能卡应用学习者聚焦通过PC/SC接口与读卡器硬件交互完成选卡、发送APDU命令、读取与写入卡片数据等核心操作可应用于门禁、员工卡、身份识别等场景。压缩包共49个文件约1023KB以13个cs源码文件为主体配合9个dll依赖库、5个resx与5个resources资源文件、3个exe可执行程序以及sln解决方案、csproj工程文件、mdb数据库和说明文档构成一套可直接编译运行的完整示例工程。目前已有737人学习下载。源码按初始化读卡器、建立连接、选卡、发送APDU、解析响应、断开连接等流程组织并包含异常处理与界面交互逻辑读者可据此理解ISO 7816协议在C#中的落地方式掌握PCSC-sharp库的调用技巧并在此基础上扩展加密通信与安全规范相关功能。1. 从一张员工卡说起这套 C# IC 卡读写源码到底能跑通什么上周产线那边拿来一张员工考勤卡说刷不开门禁让我帮忙看看是不是卡里数据写坏了。我第一反应不是拿读卡器去测而是先翻出这套CorporationEmployeeICCard源码——它就是一个用 C# 写的 IC 卡硬件读写实例专门针对公司员工卡这种场景。压缩包里是一个完整的 Visual Studio 解决方案.sln和.suo都在主工程CorporationEmployeeICCard.csproj下面挂着Form1到Form3三个窗体、AboutBox1、baseClass.cs还有一个db1.mdb的 Access 数据库和一张IC card.jpg示意图。它解决的不是“IC 卡是什么”这种概念问题而是“读卡器插上电脑之后C# 怎么把 APDU 命令发出去、怎么把返回的二进制解析成业务字段”这条完整链路。适合谁做门禁、考勤、会员卡这类桌面端上位机的开发者尤其是手里已经有 USB 读卡器、但卡在“命令发出去没反应”这一步的人。这套源码的价值在于它把 PC/SC 那套东西封装成了baseClass.cs里的方法你照着改改就能接自己的读卡器。2. 拆开压缩包工程结构、依赖与读卡器选型2.1 解决方案里每个文件在干什么先把包解开用 VS 打开CorporationEmployeeICCard.sln。整个工程是典型的 WinForms 结构没有用 WPF也没有上 .NET Core就是 .NET Framework 那一套。我按文件逐个说清楚免得你打开之后一脸懵。文件作用你要不要动CorporationEmployeeICCard.sln解决方案入口不用动CorporationEmployeeICCard.csproj工程文件记录引用和编译项换 .NET 版本时动Program.cs程序入口Application.Run(new Form1())基本不动Form1.cs主界面读卡、写卡按钮和日志框主要改这里Form2.cs员工信息录入/查询窗体按业务改Form3.cs辅助窗体通常是关于或设置按需改baseClass.cs核心读卡器连接、APDU 收发封装重点看这里db1.mdbAccess 数据库存员工卡号与信息映射换成你的库IC card.jpg示意图不是代码资源忽略baseClass.cs是整套源码的心脏。它把读卡器的打开、上电、发命令、下电、关闭这一串动作包成了几个方法Form1里点按钮就是调这些方法。你如果只关心“怎么把一条 APDU 发出去”直接跳到这个文件看发送函数就行。2.2 读卡器选型为什么这套代码默认走 PC/SCIC 卡读写绕不开一个选型问题你的读卡器是走串口COM还是走 PC/SC 标准接口。这套源码的摘要里提到过System.IO.Ports下的SerialPort但实际工程里更常见的是走 PC/SC因为 Windows 自带winscard.dll不用自己管串口波特率、校验位这些玄学参数。常见做法是如果你的读卡器是那种免驱的 USB 键盘口模拟器它其实走的是 HID读出来的是键盘码不适合做二进制读写真正能做 APDU 收发的要么是 PC/SC 读卡器比如 ACR38、ACR122 系列要么是厂商提供 DLL 的专用读卡器。我一般会先确认三件事读卡器型号、它支持哪套 API、卡片是接触式还是非接触式。这套源码的baseClass.cs里如果引用了winscard.dll的SCardEstablishContext、SCardConnect、SCardTransmit这几个函数那它就是 PC/SC 路线。你换读卡器时只要新读卡器也支持 PC/SC代码基本不用大改改一下读卡器名字匹配逻辑就行。2.3 环境准备与第一次编译动手之前把环境对齐不然编译报错能折腾半天。装 Visual Studio勾选“.NET 桌面开发”工作负载这套代码用的是 .NET Framework别只装 .NET Core。确认系统是 32 位还是 64 位winscard.dll在两个目录下都有但 P/Invoke 声明时平台要匹配否则会报“找不到入口点”。把读卡器驱动装好设备管理器里能看到智能卡读卡器设备。打开CorporationEmployeeICCard.sln先别急着运行右键解决方案点“重新生成”看有没有引用丢失。# 编译前先确认 .NET Framework 版本工程文件里 TargetFrameworkVersion 一般是 v4.x # 用命令行编译可以快速看报错比在 VS 里点更快 msbuild CorporationEmployeeICCard.sln /p:ConfigurationDebug /p:PlatformAny CPU这段命令的作用是走 MSBuild 编译整个解决方案/p:ConfigurationDebug指定调试配置/p:Platform指定平台。如果报winscard相关的 P/Invoke 错误八成是平台没对齐把工程属性里的目标平台改成和系统一致再试。编译过了先别插卡直接运行看主窗体能不能起来能起来说明依赖没问题再插读卡器。3. 核心链路从连接读卡器到收发 APDU 的完整代码3.1 建立上下文与连接读卡器PC/SC 的操作是有顺序的先建立上下文再列出读卡器再连接最后才能发命令。顺序错了就是一句“没有可用读卡器”甩你脸上。下面这段是baseClass.cs里连接逻辑的典型写法我按可复现的方式整理出来。// 引入 winscard.dll 的三个核心函数 [DllImport(winscard.dll)] public static extern int SCardEstablishContext(uint dwScope, IntPtr pvReserved1, IntPtr pvReserved2, out IntPtr phContext); [DllImport(winscard.dll)] public static extern int SCardListReaders(IntPtr hContext, string mszGroups, byte[] mszReaders, ref uint pcchReaders); [DllImport(winscard.dll)] public static extern int SCardConnect(IntPtr hContext, string szReader, uint dwShareMode, uint dwPreferredProtocols, out IntPtr phCard, out uint pdwActiveProtocol); // 建立上下文SCARD_SCOPE_USER 0 IntPtr hContext; int ret SCardEstablishContext(0, IntPtr.Zero, IntPtr.Zero, out hContext); if (ret ! 0) throw new Exception(建立上下文失败错误码 ret); // 先问长度再分配缓冲区这是 PC/SC 的固定套路 uint readerLen 0; SCardListReaders(hContext, null, null, ref readerLen); byte[] readers new byte[readerLen]; SCardListReaders(hContext, null, readers, ref readerLen); string readerName System.Text.Encoding.ASCII.GetString(readers).Trim(\0); // 连接卡片SCARD_SHARE_SHARED 2SCARD_PROTOCOL_T0 1 IntPtr hCard; uint activeProtocol; ret SCardConnect(hContext, readerName, 2, 1, out hCard, out activeProtocol); if (ret ! 0) throw new Exception(连接读卡器失败错误码 ret);逻辑说明SCardEstablishContext建立和智能卡资源管理器的会话SCardListReaders拿到读卡器名字注意它要调两次——第一次传null拿长度第二次才真正取数据这是 PC/SC 里最容易写错的地方。SCardConnect的第三个参数是共享模式2表示共享第四个参数是协议1表示 T0 协议。参数怎么改如果你读卡器只支持 T1把1改成2如果连接报错0x80100069说明读卡器里没插卡或者卡没放好。3.2 发送 APDUSELECT、READ BINARY、UPDATE BINARY连上之后就是发命令。APDU 分命令 APDU 和响应 APDU命令里最关键的是 CLA、INS、P1、P2、Lc、Data、Le 这几个字段。下面这段是发送函数和三条常用命令的构造。[DllImport(winscard.dll)] public static extern int SCardTransmit(IntPtr hCard, ref SCARD_IO_REQUEST pioSendPci, byte[] pbSendBuffer, uint cbSendLength, IntPtr pioRecvPci, byte[] pbRecvBuffer, ref uint pcbRecvLength); // 发送 APDU 的通用封装 public byte[] SendApdu(IntPtr hCard, byte[] command) { SCARD_IO_REQUEST ioRequest new SCARD_IO_REQUEST(); ioRequest.dwProtocol 1; // T0 ioRequest.cbPciLength 8; byte[] response new byte[258]; // 响应最大 256 数据 2 状态字 uint responseLen (uint)response.Length; int ret SCardTransmit(hCard, ref ioRequest, command, (uint)command.Length, IntPtr.Zero, response, ref responseLen); if (ret ! 0) throw new Exception(发送 APDU 失败错误码 ret); byte[] result new byte[responseLen]; Array.Copy(response, result, responseLen); return result; } // SELECT 命令选择卡片上的主文件P10x04 表示按 DF 名称选择 byte[] selectCmd new byte[] { 0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x86, 0x01, 0x00 }; // READ BINARY从偏移 0 读 16 字节 byte[] readCmd new byte[] { 0x00, 0xB0, 0x00, 0x00, 0x10 }; // UPDATE BINARY往偏移 0 写 16 字节数据 byte[] writeCmd new byte[] { 0x00, 0xD6, 0x00, 0x00, 0x10, /* 16 字节数据 */ };逻辑说明SendApdu把SCardTransmit包了一层返回的字节数组最后两个字节是状态字 SW1 SW20x90 0x00表示成功其他值都是错误。参数说明SELECT 命令里0xA4是 INS0x04是 P1后面跟的是文件标识符READ BINARY 里0xB0是 INS0x10是 Le表示期望读 16 字节UPDATE BINARY 里0xD6是 INS0x10是 Lc表示后面跟 16 字节数据。你改数据长度时Lc 和 Le 要跟着改不然卡片会返回0x6700长度错误。3.3 解析响应与业务字段映射拿到响应之后不能直接用得先判状态字再把数据段映射成业务字段。这套源码里Form1的读卡按钮后面就是这套逻辑。byte[] resp SendApdu(hCard, readCmd); byte sw1 resp[resp.Length - 2]; byte sw2 resp[resp.Length - 1]; if (sw1 0x90 sw2 0x00) { // 数据段是去掉最后两个状态字的部分 byte[] data new byte[resp.Length - 2]; Array.Copy(resp, data, data.Length); // 假设前 4 字节是卡号后面是员工编号 string cardNo BitConverter.ToString(data, 0, 4).Replace(-, ); string empNo System.Text.Encoding.ASCII.GetString(data, 4, data.Length - 4); // 写入日志框或数据库 } else { // 常见错误0x6A82 文件未找到0x6982 安全状态不满足 throw new Exception(string.Format(读卡失败SW{0:X2}{1:X2}, sw1, sw2)); }逻辑说明状态字判断是第一步0x9000才继续解析。数据段的偏移和长度取决于你卡片里文件的实际结构这套源码假设前 4 字节卡号、后面员工编号你换成自己的卡时得先读一遍原始数据看看布局。参数说明BitConverter.ToString把字节转成十六进制字符串Replace去掉分隔符Encoding.ASCII.GetString按 ASCII 解码如果卡里存的是 GBK 或 UTF-8这里要换编码否则中文会乱码。4. 避坑与排查读卡失败时先看这五条4.1 现象SCardEstablishContext 返回 0x8010001D原因智能卡服务SCardSvr没启动或者当前用户没权限访问。解决services.msc里把“Smart Card”服务启动类型设为自动如果是远程桌面环境还要确认“Smart Card Device Enumeration Service”也起来了。4.2 现象SCardListReaders 返回空列表原因读卡器驱动没装好或者读卡器被其他程序独占了。解决设备管理器里看有没有黄色感叹号换一个 USB 口重插如果装了厂商自带的测试工具先关掉它PC/SC 同一时间只允许一个程序连接。4.3 现象SCardTransmit 返回 0x8010000C原因没有插卡或者卡片没放到位。解决把卡重新插拔一次接触式卡片注意芯片朝向非接触式卡片要贴到感应区距离太远会超时。4.4 现象APDU 返回 0x6A82原因SELECT 命令里的文件标识符不对卡片上没这个文件。解决先用厂商工具或者通用读卡软件把卡片文件结构读出来确认 DF 和 EF 的标识符再改 SELECT 命令里的字节。4.5 现象写卡返回 0x6982原因安全状态不满足卡片要求先验证密钥才能写。解决在 UPDATE BINARY 之前先发 VERIFY 命令或者按卡片规范走外部认证流程。这套源码里如果没有实现认证写操作就会一直失败别以为是代码写错了。5. 进阶把读写封装成可复用类并做数据校验这套源码跑通之后下一步就是把它变成你自己项目里能直接用的东西。我一般会把baseClass.cs里的连接和收发逻辑抽成一个ICCardReader类构造函数里做连接Dispose里做断开业务层只调ReadBlock和WriteBlock。这样窗体代码里就不会到处是IntPtr和错误码。public class ICCardReader : IDisposable { private IntPtr hContext; private IntPtr hCard; private bool connected; public ICCardReader() { // 构造函数里建立上下文并连接失败直接抛异常 Connect(); } public byte[] ReadBlock(byte offset, byte length) { // 读之前先 SELECT 到目标文件这里省略文件选择步骤 byte[] cmd new byte[] { 0x00, 0xB0, 0x00, offset, length }; byte[] resp SendApdu(cmd); CheckStatus(resp); return ExtractData(resp); } public void WriteBlock(byte offset, byte[] data) { // 写之前必须确认安全状态否则返回 0x6982 byte[] cmd new byte[5 data.Length]; cmd[0] 0x00; cmd[1] 0xD6; cmd[2] 0x00; cmd[3] offset; cmd[4] (byte)data.Length; Array.Copy(data, 0, cmd, 5, data.Length); byte[] resp SendApdu(cmd); CheckStatus(resp); } private void CheckStatus(byte[] resp) { byte sw1 resp[resp.Length - 2]; byte sw2 resp[resp.Length - 1]; if (sw1 ! 0x90 || sw2 ! 0x00) throw new Exception(string.Format(卡片返回错误 SW{0:X2}{1:X2}, sw1, sw2)); } public void Dispose() { if (connected) { // 断开连接并释放上下文顺序不能反 SCardDisconnect(hCard, 0); SCardReleaseContext(hContext); connected false; } } }逻辑说明这个类把连接、收发、状态检查、释放都收在一起业务代码只需要using (var reader new ICCardReader()) { reader.WriteBlock(0, data); }。参数说明ReadBlock的offset是文件内偏移length是读取长度WriteBlock的data长度不能超过卡片单次写入上限一般不超过 255 字节超了要分块写。Dispose里先SCardDisconnect再SCardReleaseContext顺序反了会泄漏上下文。数据校验这块我踩过坑有一次写员工卡卡号写进去了但校验位没算结果门禁读出来卡号对但校验失败照样刷不开。后来我在WriteBlock之前加了一步校验把卡号按规则算出校验位一起写进去。常见做法是卡号用 BCD 码存储校验位用异或或者 CRC具体规则看你的卡片规范。另外db1.mdb那个 Access 库如果并发写会锁库生产环境建议换成 SQLite 或者 SQL ServerAccess 只适合单机演示。从那以后我每次接新读卡器都强制先跑一遍“连接→SELECT→READ→断开”的最小闭环确认状态字是0x9000再往下写业务逻辑。这套源码的价值就在这它把最小闭环给你搭好了你改改读卡器名字和文件标识符就能用。希望帮到你。本文还有配套的精品资源点击获取