ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ACR122U-A9 NFC读写开发实战:PC/SC与APDU驱动Mifare卡读写全解析

ACR122U-A9 NFC读写开发实战:PC/SC与APDU驱动Mifare卡读写全解析 简介ACR122U-A9 SDK 是意法半导体推出的一套 NFC 专业开发工具包面向需要开发移动支付、门禁系统、智能卡读取与信息分享应用的开发者也适合刚接触近场通信的初学者系统学习。NFC 是基于 RFID 的短距离无线通信技术支持非接触式点对点数据交换工作范围通常在 4 厘米内模块覆盖 13.56MHz 频段的 ISO/IEC 14443 A/B、FeliCa、ISO/IEC 18092 等标准可完成读卡、写卡和卡片模拟。压缩包约 95.68MB集中提供驱动程序、API 库、示例代码、详细文档与调试检测工具还包含读写解密软件可直接读取、写入 NFC 标签数据并关注加密安全问题全中文界面和文档降低了语言门槛适合国内开发者快速验证原型并定位问题。已有 1977 人学习下载整体资源将硬件操作、协议说明与软件工具有机整合可支撑围绕 ACR122U-A9 模块从基础通信实验到移动支付、智能门锁等项目的落地实践。1. ACR122U-A9一张SDK盘就能跑通的NFC读写开发上个月接小区车禁改造的单子物业旧系统里三千多张Mifare卡要重新发卡。客户丢来一个白色USB读卡器ACR122U-A9盒里附了张光盘写着SDK加全套软件。我原以为要啃一周协议文档结果从装驱动到用示例代码读出第一张卡UID只花了四十分钟。这套东西核心是一颗PN532芯片对外是标准PC/SC智能卡读卡器。做门禁、一卡通、会员储值这类NFC桌面应用或者只想给现有系统加个读卡功能这份SDK就是脚手架把驱动、指令集、示例代码一次给全。新手能在半小时内看到卡片数据熟手可以直接拿它的APDU指令集做二次封装。2. PC/SC与APDU读卡器在你电脑里到底是什么角色第一次拿到这套SDK的人最容易犯的错就是把光盘里的PDF当小说看。其实搞懂三件事就能干活这张卡是谁、电脑怎么找到读卡器、一条指令里每个字节干什么。2.1 硬件身份PN532、13.56MHz与ISO 14443系ACR122U-A9这个型号看着神秘拆开看就没有玄学了。板子正面是一颗NXP的PN532背面是USB转串口桥接芯片天线做在USB外壳里。A9只是渠道批次后缀核心方案和标准ACR122U一致都是PN532配合天线线圈。它的工作频率是13.56MHz对应的是ISO 14443 Type A/B、Mifare Classic和FeliCa这几类卡。为什么说这决定了选型如果你去搜PN532模块会发现市面上几十块钱的裸模块也能做到同样的事但ACR122U的价值在集成度外壳、天线、USB供电、射频认证一次做完。天线的阻抗匹配和调谐是NFC硬件里最容易翻车的地方自己画板子调天线运气不好要调一两天。用它就绕开了这一步插上USB就能当读卡器用。卡片靠近时读卡器以13.56MHz载波激活卡片卡片回传的数据经过PN532解调转成SPI/UART/I2C信号再被桥接芯片翻译成USB上的CCID协议。这一串过程对上层应用完全透明你不需要关心天线怎么调制只需要发APDU指令并回收响应。2.2 PC/SC协议栈应用层和驱动层的分工PC/SC是智能卡读卡器的标准协议栈在Windows上由系统自带的Smart Card服务实现在Linux上由pcscd实现。ACR122U把自己做成一个符合CCID规范的设备所以Windows绝大多数情况下插上就有反应不用装厂家专用驱动。SDK里所有示例代码不管用的是C、C#还是Delphi最终都在调同一组函数SCardEstablishContext、SCardListReaders、SCardConnect、SCardTransmit、SCardDisconnect。它们就是PC/SC API的骨架。你不需要知道USB怎么枚举、PN532怎么初始化只要往SCardTransmit里塞APDU指令再读回响应就行了。很多做嵌入式的人刚上手会不习惯总想去找PN532的寄存器说明文档觉得不碰底层心里不踏实。其实在这套方案里真不用。PC/SC已经帮你把底层封装掉了除非你要改天线阻抗或者射频功率否则寄存器完全碰不到。把它当黑匣子用反而开发效率最高。2.3 拆一条APDU指令每个字节在干什么APDU的全称是应用协议数据单元结构是CLA INS P1 P2 Lc Data Le。在ACR122U这套指令集里CLA固定为FF表示这是一条直接传输指令走读卡器私有命令通道。给一张Mifare Classic卡发卡整个过程无非三件事装载密钥、认证块、读写块。这三件事对应三条FF开头的指令我把最常用的几条列在这里后面写代码时全部会用到。指令用途关键参数说明FF CA 00 00 00获取卡UID无需认证任何卡靠近都能读出FF 82 00 00 06 6字节密钥装载密钥到读卡器密钥槽密钥长度固定6字节槽位由P2指定FF 86 00 00 05 01 00 块号 密钥类型 00认证指定数据块块号范围0~630x60A密钥0x61B密钥FF B0 00 块号 00读取16字节数据块必须先认证该块所在扇区FF D6 00 块号 10 16字节数据写入16字节数据块必须先认证Lc固定为0x10以读UID为例FF CA 00 00 00四个参数全部为零含义是按ISO 14443标准执行Get Data命令。Mifare Classic 1K返回4字节或7字节UID后面跟90 00表示成功。这里有个容易被忽略的边界读UID不需要认证所以任何兼容卡都能被读出来业务系统不能把UID当作唯一安全凭证这一点后面专门讲。3. 搭建SDK环境驱动安装、文件结构与第一个工具这套SDK加配套软件下载后先别急着跑示例先按目录把文件结构认清楚再动手装环境。很多报错就是因为驱动装了一半或者头文件没找到就开始编译。3.1 SDK包的文件结构先分清四类东西解压后典型结构是驱动、文档、示例代码和工具四个目录。注意这是成品SDK包不是网上常问的那种“SDK生成和打包的区别”里的生成物拿回去直接引用就能编译不需要自己从源码构建。目录内容作用DriverWindows下INF文件与驱动安装包让系统把读卡器识别为CCID智能卡读卡器DocAPI手册、ACR122U指令手册、应用笔记查APDU指令格式与参数边界SampleC#、C、Java、Delphi示例工程直接抄工程结构改几行就能用ToolPC/SC测试工具、Mifare读写工具验证读卡器是否正常工作排查问题SDK里那份指令手册是整包最值钱的文件比驱动还值钱。里面按指令码逐条列出了FF CA、FF 82、FF 86、FF B0、FF D6的格式、参数范围、返回状态字。我后来写封装时遇到不确定的参数全是从手册里翻的而不是去网上搜二手资料。3.2 Windows下安装与验证四步走Windows下的安装路径基本是固定的照着顺序来第一步把读卡器插进USB口系统会有发现新硬件的提示。第二步运行SDK里的驱动安装包或者手动指定.inf文件安装。第三步打开设备管理器展开“智能卡阅读器”分类确认下面出现了“ACS ACR122U PICC”之类名字。第四步用SDK自带的PC/SC测试工具列出读卡器名称。这一步最容易翻车的地方是设备管理器里读卡器显示成“人体学输入设备”后面避坑章节会单独讲。还有一个常见的坑是Windows的Smart Card服务被禁用了即使驱动装得再好也列不出读卡器。检查服务状态可以用PowerShell一条命令Get-Service -Name SCardSvr | Select-Object Status输出Status为Running就正常否则手动启动服务并设为自动。这一步是很多示例代码闪退的根源SCardListReaders返回空列表程序拿到空字符串数组去连接直接就抛异常了。3.3 Linux下怎么用不走原厂SDK的路原厂SDK里的驱动是为Windows准备的Linux下一般不用它。常见做法是装pcscd和pcsc-tools把ACR122U交给系统的PC/SC协议栈管理。开发时再装libpcsclite-dev拿头文件。sudo apt update sudo apt install -y pcscd pcsc-tools libpcsclite-dev sudo systemctl enable pcscd --now pcsc_scanpcsc_scan运行后把卡片放在读卡器上终端里会滚动显示卡片的ATR信息。看到ATR就说明整条链路通了。CtrlC退出扫描。这里注意libpcsclite-dev是编译PC/SC程序必需的开发头文件只装运行时的话后面编译示例代码会报找不到pcsclite.h很多人在这卡一下午。Linux下不需要关心原厂SDK的Windows驱动但SDK里的指令手册依然有效因为APDU指令是读卡器固件层面的东西和操作系统无关。实际项目里我在Linux下就用同一套APDU指令换个SCardTransmit的调用封装就接上了。4. 用SDK示例代码跑通读卡从读UID到写Mifare扇区这一章是核心直接写代码。我以C#为例因为SDK里C#工程改起来最快而且winscard.dll的P/Invoke方式在Windows上最直接。4.1 先跑通最简单的读UID先把PC/SC的四个核心API用DllImport引进来。注意winscard.dll在系统目录里不需要额外拷贝。[DllImport(winscard.dll, CharSet CharSet.Unicode)] public static extern int SCardEstablishContext( uint dwScope, IntPtr pvReserved1, IntPtr pvReserved2, out IntPtr phContext); [DllImport(winscard.dll, CharSet CharSet.Unicode)] public static extern int SCardConnect( IntPtr hContext, string szReader, uint dwShareMode, uint dwPreferredProtocols, out IntPtr phCard, out uint pdwActiveProtocol); [DllImport(winscard.dll, CharSet CharSet.Unicode)] public static extern int SCardTransmit( IntPtr hCard, IntPtr pioSendPci, byte[] pbSendBuffer, uint cbSendLength, IntPtr pioRecvPci, byte[] pbRecvBuffer, ref uint pcbRecvLength); [DllImport(winscard.dll, CharSet CharSet.Unicode)] public static extern int SCardDisconnect(IntPtr hCard, uint dwDisposition);dwScope传2表示用户范围ShareMode传2表示共享模式Protocol传2表示T1协议。T0和T1在ACR122U上都能用但T1对Mifare这类存储卡更稳SDK示例默认也是T1。接下来连接读卡器并发送读UID指令IntPtr ctx, card; uint protocol; int ret SCardEstablishContext(2, IntPtr.Zero, IntPtr.Zero, out ctx); if (ret ! 0) throw new Exception(SCardEstablishContext失败返回0x ret.ToString(X8)); // 示例代码直接写死读卡器名正式项目用SCardListReaders动态获取 ret SCardConnect(ctx, ACS ACR122U PICC, 2, 2, out card, out protocol); if (ret ! 0) throw new Exception(SCardConnect失败检查驱动和读卡器连接); byte[] cmdGetUID { 0xFF, 0xCA, 0x00, 0x00, 0x00 }; byte[] rsp new byte[64]; uint rspLen 64; ret SCardTransmit(card, IntPtr.Zero, cmdGetUID, (uint)cmdGetUID.Length, IntPtr.Zero, rsp, ref rspLen); if (ret 0 rspLen 2 rsp[rspLen - 2] 0x90 rsp[rspLen - 1] 0x00) { byte[] uid new byte[rspLen - 2]; Array.Copy(rsp, 0, uid, 0, uid.Length); Console.WriteLine(UID: BitConverter.ToString(uid).Replace(-, )); } else { Console.WriteLine(读UID失败SW rsp[rspLen - 2].ToString(X2) rsp[rspLen - 1].ToString(X2)); } SCardDisconnect(card, 0);这段代码的关键点在末尾的状态字判断。PC/SC返回成功时ret等于0但APDU执行成功与否要看响应数据的最后两个字节也就是SW1和SW2。90 00表示成功63 00说明认证失败或指令格式有问题。只判断ret等于0是不够的很多新手在这里踩坑以为SCardTransmit调用成功就万事大吉其实卡片的执行结果全在状态字里。UID的长度取决于卡片类型Mifare Classic 1K返回4字节部分国产兼容卡返回7字节。不要写死长度按实际返回值动态处理。4.2 密钥认证与数据块读写读UID只是热身真正发卡要读写扇区数据。Mifare Classic 1K有16个扇区每个扇区4个数据块每块16字节。访问前要先认证该块所在扇区。// 第一步装载默认密钥到读卡器密钥槽 byte[] cmdLoadKey { 0xFF, 0x82, 0x00, 0x00, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; SCardTransmit(card, IntPtr.Zero, cmdLoadKey, (uint)cmdLoadKey.Length, IntPtr.Zero, rsp, ref rspLen); // 90 00 表示密钥装载成功 // 第二步认证数据块120x60表示用A密钥0x01是密钥槽位 byte[] cmdAuth { 0xFF, 0x86, 0x00, 0x00, 0x05, 0x01, 0x00, 0x0C, 0x60, 0x00 }; SCardTransmit(card, IntPtr.Zero, cmdAuth, (uint)cmdAuth.Length, IntPtr.Zero, rsp, ref rspLen); if (rsp[rspLen - 2] 0x63) throw new Exception(认证失败密钥不对或卡片被锁); // 第三步读取块12的16字节数据 byte[] cmdRead { 0xFF, 0xB0, 0x00, 0x0C, 0x00 }; SCardTransmit(card, IntPtr.Zero, cmdRead, (uint)cmdRead.Length, IntPtr.Zero, rsp, ref rspLen); // 此时rspLen18前16字节是数据最后两字节是90 00认证指令里的0x0C是块号12的十六进制写法。块号除以4得到扇区号12除以4等于3所以块12属于扇区3。Mifare Classic的认证是扇区级的认证通过后只能对这个扇区内的4个块读写跨扇区必须重新认证。这条规则在后面踩坑章节会再次出现。写入数据块同理先认证再写。每条FF D6指令后面带16字节数据byte[] cmdWrite new byte[21]; cmdWrite[0] 0xFF; cmdWrite[1] 0xD6; cmdWrite[2] 0x00; cmdWrite[3] 0x0C; // 目标块号12 cmdWrite[4] 0x10; // 数据长度16字节 byte[] data new byte[16]; // 业务数据填这里 Array.Copy(data, 0, cmdWrite, 5, 16); SCardTransmit(card, IntPtr.Zero, cmdWrite, (uint)cmdWrite.Length, IntPtr.Zero, rsp, ref rspLen); if (rsp[rspLen - 2] 0x90 rsp[rspLen - 1] 0x00) Console.WriteLine(写入成功); else Console.WriteLine(写入失败SW rsp[rspLen - 2].ToString(X2) rsp[rspLen - 1].ToString(X2));每个扇区的最后一个块是控制块存的是密钥A、访问条件和密钥B。发卡时建议不要写这个块访问位一旦配错扇区可能被永久锁死而且这种锁死没有后悔药。4.3 把这套调用封装成可复用模块示例代码里的流程适合验证设备但直接往业务代码里塞这些SCardTransmit调用会把人逼疯。我会封装一个Acr122u类把连接、发送、认证、读写收拢成方法public class Acr122u { private IntPtr hContext; private IntPtr hCard; private byte[] rsp new byte[255]; public void Connect(string readerName) { SCardEstablishContext(2, IntPtr.Zero, IntPtr.Zero, out hContext); uint protocol; SCardConnect(hContext, readerName, 2, 2, out hCard, out protocol); } public byte[] Transmit(byte[] apdu) { uint rspLen 255; SCardTransmit(hCard, IntPtr.Zero, apdu, (uint)apdu.Length, IntPtr.Zero, rsp, ref rspLen); byte[] result new byte[rspLen]; Array.Copy(rsp, result, rspLen); return result; } public byte[] GetUID() { byte[] r Transmit(new byte[] { 0xFF, 0xCA, 0x00, 0x00, 0x00 }); if (r.Length 2 r[r.Length - 2] 0x90 r[r.Length - 1] 0x00) { byte[] uid new byte[r.Length - 2]; Array.Copy(r, uid, uid.Length); return uid; } return null; } public void LoadKey(byte[] key, byte slot 0) { byte[] cmd new byte[11]; cmd[0] 0xFF; cmd[1] 0x82; cmd[2] 0x00; cmd[3] slot; cmd[4] 0x06; Array.Copy(key, 0, cmd, 5, 6); Transmit(cmd); } public void Authenticate(byte block, byte keyType 0x60) { byte[] cmd { 0xFF, 0x86, 0x00, 0x00, 0x05, 0x01, 0x00, block, keyType, 0x00 }; Transmit(cmd); } }封装时的核心取舍是让每个方法只干一件事必要时抛异常而不是返回错误码。业务层拿到异常直接定位到“是密钥问题”还是“卡片未放置”比逐个解析状态字高效得多。5. 避坑ACR122U-A9 的五个常见翻车现场这套SDK上手快但不代表没有坑。下面五条全是实际跑过的血泪经验按现象、原因、解决写清楚每一条都对应一次真实排错。5.1 设备管理器里没有“智能卡阅读器”现象读卡器插上后灯亮但设备管理器里只有一个“人体学输入设备”找不到智能卡阅读器分类。原因ACR122U在USB层暴露了两个接口第一个是CCID智能卡接口第二个是HID人机接口。驱动没装成功时Windows只能识别出HID接口CCID接口被当成未知设备或者直接不显示。解决用SDK里的驱动安装包重新装一遍或者在设备管理器里右键这个人体学输入设备选择“更新驱动程序”手动指定到SDK解压后的Driver目录安装包含ACR122U.inf的那个文件夹。装完卸载设备再重新插拔一次系统会重新枚举这时候“智能卡阅读器”分类下就会出现ACS ACR122U PICC。5.2 SCardListReaders返回的读卡器列表为空现象程序跑起来SCardListReaders调用返回成功但读卡器名称为空后面SCardConnect直接报错。原因九成是Windows的Smart Card服务没启动。这个服务默认是自动但有些精简版系统或优化软件会把它禁掉。另一种可能性是读卡器被识别成了HID而不是CCID但那个情况在设备管理器里就能看出来。解决先确认设备管理器里有没有智能卡阅读器有的话再去services.msc里找到Smart Card服务启动并设为自动。改了服务状态后要把读卡器重新插拔一次让驱动重新绑定。我习惯把SCardListReaders返回空排成一个最高优先级的排查项因为它会让后面所有代码都跟着报错。5.3 读UID成功认证时却一直返回63 00现象读卡器正常UID能读出来但执行FF 86认证指令时响应的状态字是63 00。原因最常见的是没有先装载密钥就直接认证。FF 86指令里的密钥槽是空的认证自然失败。第二个常见原因是卡片的A密钥不是出厂默认的FF FF FF FF FF FF。很多门禁系统发卡时会把密钥改成厂商私有值你用默认密钥去认证肯定过不了。解决先执行FF 82装载密钥再执行FF 86认证顺序不能反。如果默认密钥不对可以自己写一个循环把常见的几只密钥挨个试一遍。业务系统里不要硬编码密钥放到配置文件里。另外注意认证指令里的块号是数据块编号不是扇区号块12等于扇区3别把扇区号填进去。5.4 写入第二个块时失败状态字63 00或6B 00现象连续写多个数据块时第一个块写入成功第二个块开始报错。原因Mifare Classic的认证是扇区级的认证了扇区1的块不代表扇区2的块也能直接写。换扇区必须重新发认证指令很多人写循环时只在最前面认证了一次后面跨扇区全部失败。解决把“认证加读写”打包成原子操作每个扇区处理前强制走一遍认证。我封装的代码里写块方法内部先判断目标块所属扇区如果和上次认证的扇区不同自动补一次认证。这个判断逻辑不复杂但能避免百分之八十的批量写卡翻车现场。5.5 Linux下pcscd和libnfc互相抢设备现象pcsc_scan能正常读到卡片但用libnfc工具比如nfc-list时却提示找不到设备或者反过来。原因pcscd和libnfc都会尝试独占USB读卡器两边同时跑的时候后启动的那一方拿不到设备句柄。ACR122U在这个情况下不支持并行访问只能归一边管。解决用哪边就开哪边。想用libnfc就先停掉pcscd执行sudo systemctl stop pcscd跑完再启动。反过来也一样。这个冲突在项目里很容易碰到因为调试时一会儿用pcsc_scan看ATR一会儿用libnfc做底层测试其实两边功能绝大部分重合固定用一边就够了。6. 进阶把读卡数据接进门禁与一卡通业务读卡器跑通只是第一步真正决定系统好不好用的是业务层怎么用这些数据。6.1 UID别做唯一安全凭证第一版代码里我犯过错直接拿UID当用户身份存数据库。后来发现UID只能证明卡的物理存在不能证明持卡人身份因为UID可以被伪装而且很多国产兼容卡的UID可以重复。正确做法是把UID当索引把用户ID、有效期、余额这些业务数据写进扇区数据块。验证时先读UID定位用户再读扇区数据校验有效性这样即使卡被复制业务层还能靠额外的动态数据止损。6.2 用Python做二次验证发卡程序用C#写完验证环节我喜欢用Python的pyscard库做交叉验证确保C#那边写进扇区的数据真实可靠from smartcard.System import readers conn readers()[0].createConnection() conn.connect() # 与C#版完全相同的APDU指令 data, sw1, sw2 conn.transmit([0xFF, 0xCA, 0x00, 0x00, 0x00]) if sw1 0x90 and sw2 0x00: print(UID: .join(f{b:02X} for b in data)) # 装载密钥并读取块12 conn.transmit([0xFF, 0x82, 0x00, 0x00, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) data, sw1, sw2 conn.transmit([0xFF, 0xB0, 0x00, 0x0C, 0x00]) if sw1 0x90 and sw2 0x00: print(Block12: .join(f{b:02X} for b in data[:16]))看到UID和块数据跟C#读出来的一致才敢把发卡流程交付出去。pyscard和C#走的是同一个PC/SC协议栈结果理应相同交叉验证更多是确认代码没写错。6.3 什么时候该放弃读卡器直接上PN532模块ACR122U的定位是桌面工具和发卡终端适合开发调试、批量发卡、后台管理系统。如果你做的是嵌入式门禁机现场要部署几百台设备那就不要用USB读卡器方案直接集成PN532裸模块或者买嵌入式NFC模组单台成本能省好几倍而且设备形态更好设计。反过来只是开发调试、做发卡工具、给现有系统加读卡功能USB读卡器明显更划算。项目后期我养成了一个习惯不管用哪个读卡器都强制走一遍环境健康检查设备树里找得到读卡器、PC/SC服务正常、能稳定读出同一张卡UID再开始写业务层。这套流程帮我避开了绝大多数低级报错也让我这个ACR122U-A9的开发经验沉淀成了一套可复用的调试模板。希望帮到你。本文还有配套的精品资源点击获取
返回列表