资讯动态

VC++基于PC/SC读写非接触式IC卡:ACR122U实战与Mifare卡数据操作

发布时间:2026/9/10 2:16:17 来源:尧图企业网站定制
简介这是一套基于VC开发环境编写的非接触式射频IC卡读写程序面向需要掌握IC卡底层通信与上位机联调的嵌入式或桌面应用开发者。代码以完整的MFC工程形式组织调用dcic32.dll动态库完成卡片寻卡、读取、写入等操作适合作为门禁、考勤等场景的二次开发基础。资源包共64个文件约2.05MB核心包括10个头文件、6个C源文件及对应的工程文件dsp/dsw、资源脚本rc并附带动态库、静态库、可执行程序及配置文件可直接编译参考或提取核心代码复用。目前已有238人学习下载。对于希望快速了解非接触IC卡上位机程序设计流程的开发者这套程序提供了清晰的对话框交互界面、完整读写接口示例以及打印输出、列表框优化等辅助模块既能直接编译运行观察效果也便于按模块拆解学习可大幅缩短前期调研和编码时间。 前阵子接了个现场发卡的活儿几百张空白会议卡要在指定扇区写入卡号和参会人编号后边还得随时抽查核对。厂家自带的串口工具能干活但一张一张手工点太慢我就用 VC 写了个小工具走 PC/SC 标准接口配 ACR122U 读卡器把非接触式射频 IC 卡的读取和写入流程整个跑通了。这篇就把我实际踩过的坑和能直接落地的代码整理出来给同样要做发卡器、门禁写卡、会员卡初始化的朋友参考。这个程序本质上是“PC 上位机 USB 读卡器 非接触式射频卡”的组合不需要懂太深的射频电路知识但最好有点 C 基础。我会从方案选型、卡结构原理、核心代码到问题排查一条线讲完代码部分直接可以抄。1. 项目定位与整体方案设计1.1 为什么选 VC 和 PC/SC 这套组合先明确一下应用场景Windows 系统下用 PC 连接一个 USB 接口的读卡器对放在读卡器感应区上的非接触式 IC 卡进行数据读取和写入。这种需求在门禁发卡、会议签到、会员卡初始化、停车场月卡延期这些项目里非常常见。PC 端跟读卡器通信有三条路可以走。第一种是直接用读卡器厂商提供的 SDK。ACR122U 这类主流读卡器官方会带整套动态库和示例封装得比较完整缺点是 SDK 版本和读卡器型号绑定换个牌子可能就要重新看文档改代码第二种是绕过 SDK直接用串口或 USB 直连 PN532 这类射频模块自己拼帧、自己处理天线场和卡片轮询灵活但工作量大通信协议里的坑也多第三种就是我现在推荐的 PC/SC 标准接口。PC/SC 是 Windows 内置的智能卡读卡器访问标准winscard.dll系统自带只要读卡器驱动装好同一个枚举和收发指令的代码换不同品牌的读卡器基本不用改。如果用 VCVisual C开发 Win32 或 MFC 程序调用 PC/SC 接口非常直接不需要额外引入第三方库编译出来一个小 exe 就能扔到现场机器上跑。而且开发环境选择很宽VS2015、VS2017、VS2019、VS2022 都可以代码基本通用。如果担心目标机器缺少运行库项目属性里把运行库改成“静态链接”/MT再把字符集设置跟代码匹配好分发时连安装 VC 运行库的步骤都省了。我实测下来一个用 MFC 写的发卡小工具Release 编译后体积也就几百 KB放在 U 盘里带着走非常方便。1.2 非接触式射频 IC 卡是怎么工作的很多人一听到“射频”就觉得很高深其实对写程序的人来说只需要理解一个大概读卡器天线上电后会产生一个 13.56MHz 的交变磁场卡片靠近时卡内的线圈通过电磁感应获得能量给卡内芯片供电卡片和读卡器之间通过“负载调制”的方式传输数据简单说就是卡片通过改变自身电路的阻抗让读卡器天线的电压发生微小变化从而把数据传回去。整个过程不需要电池也不需要物理接触这就是“非接触式”“射频”这两个词的来源。项目里最常见的非接触式射频 IC 卡是 Mifare Classic 系列典型型号是 S50也叫 Mifare Classic 1K。这类卡内部是一个 EEPROM 存储器存储容量 1KB按 16 个扇区划分每个扇区 4 个块每块 16 字节。数据掉电不丢失所以大量用在门禁、校园卡、会员卡这些场景。如果你接触过 W25Q64 这类 Flash 存储芯片会发现思路很像——都是通过某种总线读写一块非易失性存储区域只是 IC 卡的物理介质是射频数据包的格式也和 SPI 完全不同。有个概念要区分清楚卡片通信走的是 ISO/IEC 14443A 协议而上位机跟读卡器之间走的是 PC/SC 协议。我们写的 VC 程序只需要关心 PC/SC 指令怎么发、返回数据怎么解析射频匹配那一层由读卡器硬件处理。1.3 核心参数与存储结构速查开始写代码前建议先把 S50 卡的基本参数记在脑子里调试时能少翻很多次文档。参数项数值 / 说明工作频率13.56 MHz通信协议ISO/IEC 14443A典型芯片NXP Mifare Classic 1KS50存储空间1024 字节1KB按 16 扇区划分每扇区结构4 块每块 16 字节扇区尾部块每扇区第 3 块存放 KeyA、访问控制位、KeyB认证方式每扇区独立KeyA 或 KeyB各 6 字节默认密钥出厂通常为 FF FF FF FF FF FF卡片 UID4 字节部分卡 7 字节出厂固化通常不可修改S50 卡 1KB 的存储空间看着不大但实际项目里完全够用。比如门禁系统只用一个扇区存卡号和有效期会员卡用两个扇区存积分和消费记录剩余的扇区还能留给扩展。关键是清楚每个扇区里哪些块可以随便写、哪些块不能碰——每扇区的第 3 块是 Sector Trailer扇区尾部块用来放密钥和访问控制位写错了整个扇区的权限就乱了严重时这张卡在这个扇区就直接废掉。所以我写数据前一定会先读一下目标块确认它不是第 3 块再动手。2. 核心细节解析从射频通信到数据读写2.1 一次完整读写的背后发生了什么很多新手拿到读卡器就直接发读写指令结果返回一堆莫名其妙的错误码。实际上 PC/SC 方式下一次完整的数据操作要经过几个固定阶段。第一步是寻卡。读卡器周期性发出 REQA 命令检测感应区内有没有卡片第二步是防冲突。如果同时有多张卡在感应区内读卡器要跟每一张卡依次通信避免数据撞车这个过程就像食堂打饭窗口前挤了很多人窗口工作人员必须先喊一句“排队一个一个来”第三步是选卡。读卡器通过防冲突流程拿到卡片的 UID选中特定的一张卡第四步是认证。Mifare 卡的每个扇区在读写前都必须通过密码认证用 KeyA 或者 KeyB 都可以认证成功后后续读写指令才能在这个扇区执行。认证指令发出后卡片内部会执行一个“三次认证”的流程简单说就是读卡器和卡片互相验证密码并生成随机会话密钥避免密码直接在射频链路上明文传输。这一步不是我们程序里能直接控制的PC/SC 读卡器会把整个认证过程封装成一条命令我们只要保证命令里带的密码正确就行。理解了这几个阶段就明白为什么连接读卡器成功不等于能马上读写。程序里必须先完成寻卡和认证再发读写指令否则卡片会返回 6A 82块号不存在或指令不支持或者 63 00认证失败这样的状态字。调试的时候每次只放一张卡在感应区可以避免防冲突过程带来的结果不确定性。2.2 扇区、块号和数据布局怎么规划S50 卡的块号计算不复杂绝对块号 扇区号 × 4 块内索引。也就是说第 0 扇区的 4 个块的绝对块号是 0、1、2、3第 1 扇区是 4、5、6、7依此类推。其中第 0 扇区的第 0 块是厂商数据块里面存放 UID、UID 校验字节和厂商信息出厂时已经写死普通用户不能修改也不能当普通数据块用。我一般会在功能设计文档里画一张简单的块分配表比如这样扇区绝对块号用途00厂商块UID不可写01~2项目信息卡号、版本号03扇区尾部KeyA/KeyB/访问控制位14~6业务数据有效期、部门编号等17扇区尾部2~158~63预留或按项目需求分配每块 16 字节的数据区我习惯的排布是前 4 字节存卡号这类固定编号中间 8 字节存时间戳或者业务数据最后留 2 个字节做简单校验再留 2 个字节备用。写数据时建议都从扇区 1 开始因为第 0 扇区的数据块紧挨着厂商块有些读卡器对第 0 扇区的操作会有额外限制。2.3 字节序、数据长度和校验的坑这一节全是实操中容易踩的坑建议收藏。第一UID 的字节序。PC/SC 接口返回的 UID 通常是低位在前直接按顺序打印出来会发现和卡片上印的数字是反的。比如卡片上印着 “04 A1 B2 C3”代码里收到的数组可能是 C3 B2 A1 04。打印或写入数据库之前要做一次反转很多“读出来的卡号对不上”的问题都是出在这里。第二写块必须一次提供完整的 16 字节。Mifare 卡的写块指令要求数据长度固定为 16 字节不足的部分要补 0x00。我见过有人只填了 4 字节的数据就直接发写指令读卡器返回 6B 00参数错误卡上原来的数据还被清零了非常被动。第三读出来的数据如果是满屏 0xFF不要惊慌。IC 卡出厂时存储区域默认是全 1 状态0xFF 表示这块区域在出厂之后没有被写过或者被擦除过。这时候如果误把 0xFF 当成有效数据处理就会得到一堆“错误卡号”。第四写数据前先读回。这是成本最低的保护措施。程序里每次写块之前先发一条读块指令把目标块的当前内容拉回来确认确认它是普通数据块而不是扇区尾部控制块再组织数据写入。写完之后再读一次比对关键字段确认写入成功。3. 实操过程VC 程序从零实现3.1 开发环境与工程配置我用的是 VS2019但前面说了VS2015 到 VS2022 都行。新建工程时可以选择 Win32 控制台程序快速验证逻辑也可以直接建 MFC 对话框工程做界面。小工具类项目我推荐 MFC 对话框界面调试效率高后面要加读写按钮、日志显示都很方便。工程属性里有几个关键点。链接器需要额外依赖winscard.lib这个是 Windows SDK 自带的也可以通过代码里的#pragma comment(lib, winscard.lib)引入字符集我习惯用“多字节字符集”配合宽字符版的SCardListReadersW也能跑但简单项目里用 A 结尾的版本函数更直接如果程序要拷贝到没有安装 VC 运行库的机器上把 C/C 代码生成里的运行库改为“多线程 (/MT)”这样编译出来的 exe 不再依赖动态运行库。顺便说一句网上很多人问 “VC 2015-2022 redistributable 怎么装”其实就是让目标机器装运行库但在项目里用静态链接能直接绕开这个问题。硬件方面把 ACR122U 插上电脑装好官方驱动在设备管理器里能看到“智能卡读卡器”设备。如果看不到多半是驱动没装好或者 USB 供电不足换一个 USB 口再试。3.2 初始化并连接读卡器PC/SC 编程的第一步是建立资源上下文然后枚举读卡器最后建立连接。下面这段代码是最小可用的连接流程。#include winscard.h #pragma comment(lib, winscard.lib) SCARDCONTEXT ctx 0; SCARDHANDLE hCard 0; DWORD dwActiveProtocol 0; LONG lRet 0; // 1. 建立 PC/SC 资源上下文 lRet SCardEstablishContext(SCARD_SCOPE_USER, NULL, NULL, ctx); if (lRet ! SCARD_S_SUCCESS) { // 错误处理常见的错误是 SCARD_E_NO_SERVICE服务未启动 return; } // 2. 枚举系统中可用的读卡器 char szReaders[1024] {0}; DWORD dwReadersLen sizeof(szReaders); lRet SCardListReadersA(ctx, NULL, szReaders, dwReadersLen); if (lRet ! SCARD_S_SUCCESS) { // 如果没有读到读卡器返回 SCARD_E_NO_READERS_AVAILABLE return; } // szReaders 是多个以 \0 分隔的字符串最后以两个 \0 结束 char* pReader szReaders; // pReader 就是读卡器名类似 ACS ACR122U PICC Interface 0 // 3. 连接读卡器共享模式T0/T1 协议都允许 lRet SCardConnectA(ctx, pReader, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, hCard, dwActiveProtocol); if (lRet ! SCARD_S_SUCCESS) { return; } // 连接成功后hCard 是后续所有收发指令的操作句柄连接成功后建议把pReader读卡器名保存下来界面上的下拉框可以展示给用户选择。如果系统里有多个读卡器SCardListReadersA返回的是多字符串数组需要逐个取出来我再加一个简单循环把名字放进下拉框。这一步没什么难度但逻辑要写对否则用户插了两个读卡器时只会连到第一个。3.3 APDU 指令封装认证、读块、写块PC/SC 下所有对卡片的操作最终都是通过SCardTransmit发送 APDU 指令完成的。ACR122U 这类读卡器有自己的一套扩展 APDU 指令指令以 0xFF 开头非常直观。先看认证指令。Mifare 卡认证的标准用法是给读卡器发送如下格式的数据// 认证扇区FF 82 00 00 06 6字节密码 块号 BYTE authCmd[16] { 0xFF, 0x82, 0x00, 0x00, 0x06, // 认证指令头和长度 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // KeyA默认 6 个 FF (BYTE)blockNo // 目标块号用来定位扇区 }; BYTE authResp[16] {0}; DWORD authRespLen sizeof(authResp); SCardTransmit(hCard, SCARD_PCI_T1, authCmd, 11, NULL, authResp, authRespLen); // 若 authResp[0] 0x90 authResp[1] 0x00认证成功需要注意两点一是认证指令里的块号是绝对块号比如要操作扇区 1就填绝对块号 4二是不同读卡器的扩展 APDU 格式可能有差异ACR122U 的格式是我上面写的这个如果换了其他品牌参数位置可能不同以读卡器官方 PC/SC 文档为准。读块指令就更短了。一次性读出 16 字节数据BYTE readCmd[8] {0xFF, 0xB0, 0x00, blockNo, 0x10}; BYTE readResp[280] {0}; DWORD readRespLen sizeof(readResp); SCardTransmit(hCard, SCARD_PCI_T1, readCmd, 5, NULL, readResp, readRespLen); // readResp 前 16 字节是块数据最后两个字节是状态字 0x90 0x00写块指令稍微长一些因为要把 16 字节数据带上BYTE writeCmd[32] {0}; writeCmd[0] 0xFF; writeCmd[1] 0xD6; writeCmd[2] 0x00; writeCmd[3] blockNo; writeCmd[4] 0x10; // 后面跟 16 字节 memcpy(writeCmd 5, blockData, 16); // blockData 是长度必须为 16 的数组 BYTE writeResp[16] {0}; DWORD writeRespLen sizeof(writeResp); SCardTransmit(hCard, SCARD_PCI_T1, writeCmd, 21, NULL, writeResp, writeRespLen);读块指令返回的数据里前 16 字节才是卡片数据后面两个字节是状态字。状态字只要是0x90 0x00就表示成功。我调试时会把每次发送的明文指令和返回的完整响应都打印到日志框里这样一旦出错看日志比瞎猜快得多。3.4 界面设计与日志输出界面不需要复杂一个竖排的对话框就够顶部一个“读卡器”下拉框和“连接”按钮中间是“块号”编辑框、“密钥”编辑框默认填 FFFFFFFFFFFF以及“读块”“写块”两个按钮下面是一个多行编辑框当日志显示区。日志区一定要显示原始 HEX 收发记录。我第一次做这个工具时只显示“成功/失败”出了问题完全不知道卡在哪一步后来改成把指令字节流和响应字节流都_stprintf到日志框里很多问题一眼就能判断出来。按键响应函数里把SCardTransmit前后打印日志代码逻辑不用改动排查效率提升巨大。要注意的是SCardTransmit在读写过程中可能阻塞几十毫秒到几百毫秒如果直接把逻辑写在按钮的点击事件里界面会短暂无响应。简单项目里可以接受但如果批量发卡时卡连续操作建议放到工作线程里跑用PostMessage把日志回传给界面线程刷新。4. 常见问题与排查技巧实录4.1 常见错误码速查表写代码期间我反复遇到几类错误这里整理成速查表遇到问题先对照一下。返回内容含义处理方式0x8010000E找不到可用读卡器检查读卡器是否插好、驱动是否安装0x8010000C读卡器上没有卡把卡片放到感应区0x80100003句柄无效或上下文无效检查是否先建立上下文再连接0x63 00卡片返回的认证失败状态字检查密钥是否正确、扇区权限是否已修改0x6A 82块号不存在或指令不支持检查块号是否超出范围、读卡器是否支持该指令0x90 00操作成功无要区分两个层次的错误以 0x80 开头的返回值是 PC/SC 层的错误比如系统找不到读卡器、连接失败以 0x63 00、0x6A 82 结尾的状态字则是卡片返回的状态会出现在响应缓冲区最后两个字节里。看到 0x63 00 先怀疑密码不对看到 0x6A 82 先检查块号经验之谈。4.2 实战排查过程记录举几个我实际遇到过的问题。卡片放上去后读卡器毫无反应。我先用读卡器厂商自带的小工具测试确认卡片本身能被识别。如果自带工具能读那问题就在我的程序里重点检查连接和寻卡步骤如果自带工具也读不到那就是卡片类型不匹配或者卡片损坏。这个排查顺序能帮你快速分清是硬件问题还是软件问题。连接成功后认证一直失败。我第一反应是查密码字节序。ACR122U 的扩展认证指令里密码是明文 6 字节默认是全 FF这个不会错但我发现有些卡片的密码被发卡系统改过了不再是默认密钥。如果确认密钥确实正确那就是扇区的访问控制位被修改过导致即使知道 KeyA 也只剩下读权限没有写权限。这个问题在二手旧卡的测试中特别常见。写数据返回成功但读回来发现数据不对。我排查下来的原因基本都是一个写到了扇区第 3 块也就是把控制块覆盖了。控制块写错之后这个扇区的权限配置可能变得非常奇怪甚至出现“写的时候能成读的时候被拒”的情况。所以写之前先读回目标块确认它不是 0x03、0x07 这种块号再执行写操作。4.3 合规使用与数据安全提醒这部分必须多说几句。非接触式 IC 卡读写工具本身没有对错但使用场景一定要合规。我建议你只对自己拥有或获得明确授权的卡进行操作比如自己买的空白卡、单位发卡项目里客户授权处理的卡、测试用的废旧卡。绝对不要复制他人门禁卡、交通卡、会员卡也绝对不要尝试破解或绕过他人卡片的密钥体系这是法律和职业道德的底线。另外S50 卡的默认密钥是全公开的这意味着如果发卡系统没有修改默认密钥任何拿到读卡器的人都可以读取卡内数据甚至改数据。项目里如果要追求安全性应该在发卡阶段由系统生成随机密钥并正确配置访问控制位。但要再次强调修改密钥和访问控制位是一件需要充分理解权限模型的事情不要为了“更安全”而胡乱写控制块否则会造成整卡数据不可用。修改前务必完整备份原卡数据。这个小工具我前后改了三版第一版没有日志出问题全靠猜第二版加了 HEX 收发日志后很多问题一眼就定位了。所以我特别建议你从一开始就把收发日志做好。另外一个小技巧批量发卡时我会在数据块里预留一个字节做状态标记写成功并读回校验后把状态置为 0x01这样后续排查“哪张卡写了、哪张没写”特别方便。做这个东西本身不难难的是把每个环节的细节都照顾到。本文还有配套的精品资源点击获取

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

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

免费获取报价