ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

VC++实现非接触式IC卡读写:从APDU到PC/SC实战

VC++实现非接触式IC卡读写:从APDU到PC/SC实战 简介面向VC开发者的非接触式射频IC卡读写程序源码包基于dcic32.dll动态库实现完整读写流程适合需要集成RFID卡操作的Windows桌面应用开发者与中高级C程序员参考。压缩包共64个文件约2.05MB涵盖头文件、C源文件、对话框资源、图标、编译生成的动态库与可执行文件并附有配置文件、调试信息和工程文件可直接打开工程编译运行。已有238人学习下载。代码不只演示射频卡读写核心逻辑还包含界面交互如列表控件、打印机模块及字符串处理等实用工具目录组织清晰便于按模块查阅与二次开发可作为VC硬件编程的入门参考或项目基础。 上个月朋友搬来一台老式门禁一体机说是配套的发卡软件只认Windows XP换到新电脑上不是缺驱动就是读不到卡折腾几天后干脆让我用VC写一个独立的小工具核心功能就一句话读写非接触式射频IC卡。我起初觉得这不难市面上现成的DLL和Demo一抓一大把可真上手才发现从射频协议的理解、读卡器选型到APDU指令的拼装每一步都有文档之外的门道。这篇文章就把我从零到跑通全流程的经验整理出来包括硬件方案怎么选、VC代码怎么组织、实际调试会碰到哪些坑给正准备做同类读写工具的朋友一个完整参照。1. 非接触式IC卡读写的核心先搞清楚你到底在读什么1.1 卡片和读卡器之间靠什么“隔空”交换数据很多人第一次接触非接触式IC卡容易把它想象成一张带无线模块的卡片实际上大部分卡里面连电池都没有。读卡器先产生一个13.56MHz的射频电磁场卡片进入这个场后利用线圈感应到的能量给芯片供电然后再通过负载调制的方式把数据传回读卡器。也就是说读写过程中的能量供给和数据传输都靠这一副天线同时完成这也是“非接触式”和“射频”这两个词的本质来源。从ISO 14443协议往下拆数据链路还能分为物理层、防冲突层和命令层。物理层处理射频调制解调防冲突层解决多张卡同时出现在天线范围内时如何选一张卡的问题命令层才是我们写程序时最常接触的部分。对应到VC开发里真正要对付的其实是命令层读卡器负责把命令帧通过射频发送给卡片我们只负责把命令拼好、发出去、解析返回结果。这里的“命令”在PC/SC体系下有一个统一格式叫APDU读卡器驱动会用SCardTransmit这类接口帮你完成底层传输。理解这一点很关键。因为网上大量Demo会把底层射频参数也暴露给你新手容易在卡协议上钻牛角尖其实如果你的硬件方案走的是标准PC/SC读卡器射频部分完全不用碰。程序员的关注点应该是这张卡内部的数据怎么组织、认证需要什么密钥、命令返回的状态字代表什么含义。1.2 Mifare 1K的扇区地图写代码前必须背下来的结构现在市面上非接触式IC卡虽然种类不少但绝大多数门禁、考勤、校园一卡通用的都是Mifare Classic 1K也就是常说的M1卡。M1卡的存储结构非常规整总共1KB的EEPROM被分成16个扇区每个扇区又分成4个块每个块16个字节。扇区中的块0、块1、块2是数据块块3是扇区尾块用来存放密钥和访问控制位。具体来说块3的结构是6字节Key A 4字节访问控制位 6字节Key B。读数据时只要对上Key A或Key B中的一个就能认证通过但写数据时不一定访问控制位决定了两把密钥各自能做什么操作。很多新接触M1卡的人以为“有密码就能随便读、随便写”实际上访问控制位设置得严格时Key A可能只能读不能写Key B可能既能读又能写这套权限矩阵是必须查表确认的。另一个容易忽略的点是第0扇区的块0。这个块保存的是厂商出厂写入的UID唯一标识和厂商数据正常情况下不允许修改。很多接手项目的人上来就把整卡64个块一股脑遍历写入结果在块0上报错还怀疑是自己程序写得不对。实际经验是读写程序要把第0块排除在常规写操作之外只做读取用于识别卡片身份。2. 硬件选型PC/SC读卡器和串口方案怎么选2.1 PC/SC标准读卡器Windows上最省心的接入路径做VC程序首选PC/SC标准读卡器典型代表是ACR122U这种USB接口设备。它在Windows下要么是系统自带驱动要么装上厂商驱动后就能通过标准的winscard.dll接口访问。好处非常明显代码不绑定具体厂家今天拿ACR122U调试明天换个支持PC/SC的其他牌子程序基本不用改。PC/SC这套接口对VC开发者来说非常友好核心就几个函数SCardEstablishContext建立上下文、SCardListReaders枚举读卡器、SCardConnect连接读卡器、SCardTransmit发送APDU指令、SCardDisconnect断开连接。整个流程有点像一个串口程序打开句柄、写数据、读数据、关句柄。这里有个容易被忽略的细节SCardConnect时你可以指定共享模式。SCARD_SHARE_EXCLUSIVE是独占模式适合我们这种单纯读写卡片的工具但如果程序同时要配合刷卡监控最好用SCARD_SHARE_SHARED共享模式。我曾经在给别人写工具时贪方便一律用独占模式结果用户机器上本来常驻着一个刷卡监控程序两边一冲突读卡器直接被锁死重启程序都救不回来。后来改成共享模式问题才消失。2.2 串口直连方案适合自研硬件和整机集成的场景不是所有场景都有现成PC/SC读卡器可用。我见过不少设备是主板上直接焊了一个射频读写芯片比如MFRC522这类通过UART或者SPI接到MCU再走串口和电脑通信。这种情况下PC/SC这套标准基本用不上需要根据芯片厂家提供的通信协议自己定制命令帧。串口方案的优势是成本低、硬件集成度高、响应速度可以直接掌控缺点也很明显协议要自己定义防冲突和密钥管理都要底层实现。本身上位机在Windows上通过VC操作串口很容易一个大OpenFile或者CreateFile打开COM口WriteFile发命令、ReadFile读响应但没有现成的SCardTransmit那么标准。我的建议是纯粹做PC端工具果断上PC/SC读卡器把精力放到卡片业务逻辑上如果是给嵌入式或整机项目做配套上位机再考虑串口直连。选错方案会带来大量无谓的工作量这一点在行业里吃过亏的人不在少数。3. VC读写程序实现从枚举设备到扇区数据写入3.1 建立上下文、枚举读卡器、连接卡片不管用什么IDEVC程序基本绕不开Windows平台SDK。第一步需要引入winscard头文件和库#include winscard.h #pragma comment(lib, winscard.lib)建立上下文和执行枚举的代码非常直白SCARDCONTEXT ctx NULL; LONG ret SCardEstablishContext(SCARD_SCOPE_USER, NULL, NULL, ctx); if (ret ! ERROR_SUCCESS) { // 处理错误常见原因Smart Card服务没有启动 return -1; } DWORD len 0; // 第一次调用只获取缓冲区长度 ret SCardListReaders(ctx, NULL, NULL, len); char* buffer new char[len]; ret SCardListReaders(ctx, NULL, buffer, len); // buffer里是多字符串格式依次列出读卡器名称以空字符串结尾这一步容易踩的坑是SCardListReaders第一次调用时如果系统里没有任何读卡器返回值是SCARD_E_NO_READERS_AVAILABLE而不是返回0。很多入门代码没有判断这个情况直接拿着返回的len去分配内存程序就崩了。稳妥的做法是先判断返回值再根据len分配缓冲区。拿到读卡器名称后下一步是连接SCARDHANDLE hCard NULL; DWORD activeProtocol 0; ret SCardConnect(ctx, buffer, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, hCard, activeProtocol);SCardConnect的最后一个参数会返回读卡器和卡最终协商出来的协议T0或T1。大多数非接触式IC卡走的是TCL链路但这个细节驱动已经帮你处理了代码里只要把两种协议都允许就行。3.2 认证 读块 写块三个APDU搞定核心操作连接成功之后就能发送APDU了。PC/SC读卡器通常用厂商定义的一套“伪APDU”第一个字节固定为0xFF表示这条命令是发给读卡器硬件本身的而不是直接透传给卡片。整段指令由读卡器解析后再转成射频命令发到卡上。先看认证命令// FF 86 00 00 05 01 00 块号 密钥类型 // 密钥类型0x60表示用Key A0x61表示用Key B0xFF表示使用读卡器内置密钥 BYTE authCmd[] { 0xFF, 0x86, 0x00, 0x00, 0x05, 0x01, 0x00, 0x04, 0x60 }; BYTE resp[16] {0}; DWORD respLen sizeof(resp); ret SCardTransmit(hCard, SCARD_PCI_T1, authCmd, sizeof(authCmd), NULL, resp, respLen); // 正常返回时resp[respLen-2]0x90, resp[respLen-1]0x00注意这里认证命令的第8字节是块号第9字节是密钥类型。我写0x04意思是认证第1扇区的块4扇区1的第一块。这个块号必须落在你想访问的扇区里认证的作用范围是整个扇区认证通过后就能对该扇区的所有块执行读写了。读一个块BYTE readCmd[] { 0xFF, 0xB0, 0x00, 0x04, 0x10 }; // 读块4长度0x10即16字节 respLen sizeof(resp); ret SCardTransmit(hCard, SCARD_PCI_T1, readCmd, sizeof(readCmd), NULL, resp, respLen); // 成功后resp前16字节是块数据后2字节是状态字0x90 0x00写一个块BYTE data[16] { H, e, l, l, o, , M, 1, 0,0,0,0,0,0,0,0 }; BYTE writeCmd[22] {0}; writeCmd[0] 0xFF; writeCmd[1] 0xD6; writeCmd[2] 0x00; writeCmd[3] 0x04; writeCmd[4] 0x10; // 写块4长度16 memcpy(writeCmd 5, data, 16); respLen sizeof(resp); ret SCardTransmit(hCard, SCARD_PCI_T1, writeCmd, sizeof(writeCmd), NULL, resp, respLen);这三条指令基本就是读卡程序的全部核心。认证的方式可能随读卡器品牌不同略有差别有的读卡器支持直接装载密钥的指令有的需要提前用FF 82指令把密钥存进读卡器内部再在认证指令里指定用哪个密钥号。ACR122U两种都支持直接用0xFF密钥类型通常代表“用读卡器当前默认密钥”。3.3 封装一个可复用的扇区读写工具函数实际项目里不会只读写一个块而是需要按业务需求把一个扇区甚至多个扇区组织起来读写。所以我强烈建议把上面这些逻辑封装成函数至少提供三个接口认证扇区、读取块、写入块。我之前写的封装大概长这样bool MifareAuth(SCARDHANDLE hCard, BYTE blockNo, BYTE keyType, BYTE* keyData); bool MifareReadBlock(SCARDHANDLE hCard, BYTE blockNo, BYTE* outData); bool MifareWriteBlock(SCARDHANDLE hCard, BYTE blockNo, BYTE* inData);封装时有两个细节必须注意。第一认证失败不代表读卡器坏了程序要能识别0x6300状态字并给出“密钥错误”的提示而不是显示一个笼统的“操作失败”。第二写块操作尽量在写入后立刻读回来校验因为射频干扰、卡片老化都可能导致写入不完整读回校验是成本最低的可靠性保证。封装完成后调用方只需要关心扇区号和字节偏移。我会额外做一个扇区读取函数把扇区里0到2块读出来拼成一个48字节的结构体上层业务直接在这48字节里解析字段。这样程序的可维护性比到处散落的SCardTransmit要强得多。4. 实际调试里的三座大山认证失败、块区被锁、驱动占用4.1 认证总是报0x6300问题到底出在哪个环节0x6300是Mifare认证失败最常见的状态字网上几乎每个相关论坛都有人在问。按我的排查经验这个返回值背后可能藏着完全不同的几种原因一是密钥确实不对。出厂默认密钥是6个字节的0xFF如果这张卡以前被重新配过密钥默认密钥当然过不了认证。这种情况在接手别人发过的卡时太常见了解决办法就是找原始发卡软件或者发卡记录确认密钥。二是卡片不在天线范围。别笑这种问题真的高频出现。有的读卡器天线面积小卡片稍微偏几毫米就读不到但读卡器已经上电程序也正常连接了于是执行认证的时候卡片压根没回应返回超时或异常状态。排查的时候把卡片贴近天线正中心排除物理位置因素再谈代码问题。三是读卡器状态异常。如果你之前执行过一条错误指令导致读卡器内部状态机混乱后续认证也容易报0x6300。最简单的处理办法是断开连接重新SCardConnect或者直接对读卡器发一个复位指令。我在调试阶段就养成了一个习惯连接之后先发一条FF 00 00 00 00复位读卡器确保硬件处于干净状态。4.2 块0和Access Bits一张卡是怎么被“写废”的块0不能写这个前面已经说过但实际调试中还有更隐蔽的坑——访问控制位Access Bits。这4个字节在扇区尾块里每个字节的每一位都参与控制三组块的读写权限。它们的值不是随意填的必须按照Mifare官方定义的结构来组织。很多人拿到新卡就想自己写一个自定义密钥结果修改块3时只把Key A和Key B改了Access Bits却用了网上找的一组十六进制值没仔细核对含义。写进去之后发现扇区数据全部变成不可读连重新认证都过不去因为权限位已经被改成了一个将Key A和Key B都设为“不可读不可写”的组合。这种卡基本等于废了。我的建议是项目初期不要动Access Bits保持出厂值0xFF 0x07 0x80 0x69。这组默认值的含义是Key A可读、Key B可读写、数据块读写权限都由Key A和Key B共同决定绝大多数业务场景完全够用。真需要自定义权限时一定先画权限矩阵表把三组块分别对应的访问位算出来再转成字节填充。宁可多花十分钟做位运算也不要拍脑袋填数据。4.3 PC/SC句柄冲突程序莫名报错先查读卡器占用最后一个坑不在射频层也不在卡片层而在Windows系统本身。PC/SC读卡器是一个共享资源同一时间多个进程都可以访问但访问模式不一样时会互相排斥。比如程序A用SCARD_SHARE_EXCLUSIVE打开了读卡器程序B再去连接就会得到SCARD_E_SHARING_VIOLATION看起来像是读卡器坏了。此外SCardDisconnect如果忘了调用句柄会一直占着程序退出后系统可能在短时间内不释放资源重新打开时异常。长期跑的工具类程序尤其要注意每次操作结束一定要在finally块或者RAII析构里做断开操作。还有一个小经验读卡器驱动和系统自带Smart Card服务偶尔会出状态不同步的问题表现为设备管理器里读卡器正常但winscard接口怎么也连不上。这种情况先重启Smart Card服务再重新插拔USB读卡器80%能解决。别急着重装驱动先把软服务复位一遍成本最低。整个项目做下来我的体会是VC读写非接触式射频IC卡真正的难点从来不是VC也不是API调用而是你对卡片数据模型和对读卡器工作机制的理解。把扇区结构、APDU指令和PC/SC生命周期这三件事吃透后面写出来的代码会非常干净。最后再分享一个我坚持到现在的习惯每次读写卡工具交付时附一份简单的串口日志或文件日志把每一次认证、读、写的原始指令和状态字记录下来用户现场出问题时靠这份日志几分钟就能定位到是卡的问题还是程序的问题。本文还有配套的精品资源点击获取
返回列表