第23章 USB NFC读卡器开发:从协议到实战
说实话,NFC读卡器开发这块,我当年刚接触时也是一头雾水。那时候公司要做门禁系统,让我用Android平板去读身份证。我心想,这不就是调个API的事吗?结果一上手才发现,坑比想象的多得多。
今天我们就来聊聊USB NFC读卡器的开发。我会把CCID协议、PC/SC标准、Android端的Host实现,以及ISO 14443 Type A/B卡的读写,全部串起来讲。最后还会带一个实战案例——用USB NFC读卡器读取身份证和门禁卡。
核心知识点一览:
- CCID协议:USB上的智能卡读卡器标准
- PC/SC:跨平台的智能卡访问接口
- Android USB Host:让手机变成读卡器主机
- ISO 14443 Type A/B:非接触式卡片的物理层协议
- 实战:身份证(Type B)和门禁卡(Type A)的读取
23.1 CCID协议:USB上的智能卡读卡器标准
CCID全称是Chip Card Interface Device。说白了,它就是定义了USB读卡器该怎么跟主机通信的一套标准。我最早接触CCID是在做银行U盾项目的时候,那时候才知道原来智能卡读卡器都遵循这个协议。
CCID协议的核心是定义了USB的端点(Endpoint)和传输的数据格式。一个标准的CCID设备通常有:
- 控制端点(Endpoint 0):用于设备枚举和配置
- 批量输入端点(Bulk IN):读卡器向主机发送数据
- 批量输出端点(Bulk OUT):主机向读卡器发送命令
- 中断端点(Interrupt IN):读卡器通知主机状态变化
CCID的数据包格式也很固定。每个命令或响应都有一个10字节的头部,后面跟着数据。头部包含消息类型、长度、槽位号等信息。嗯,这里要注意,不同的消息类型对应不同的操作,比如PC_to_RDR_XfrBlock就是传输APDU命令。
我的经验:刚开始调试CCID设备时,建议先用USB抓包工具(比如Wireshark配合usbmon)看看数据流。我曾经花了两天时间找bug,最后发现是端点地址配反了。这种低级错误,抓包一看就明白。
23.2 PC/SC标准:跨平台的智能卡访问接口
PC/SC是Personal Computer/Smart Card的缩写。它定义了上层应用怎么跟读卡器交互。你想想看,如果没有PC/SC,每个读卡器厂商都得自己写一套API,那开发人员不得疯掉?
PC/SC的核心概念包括:
- SCardEstablishContext:建立资源管理器上下文
- SCardListReaders:列出所有可用的读卡器
- SCardConnect:连接到一个读卡器
- SCardTransmit:发送APDU命令
- SCardDisconnect:断开连接
在Android上,我们通常用开源库来实现PC/SC兼容层。我个人习惯用libnfc或者自己封装一套简单的PC/SC接口。毕竟Android原生并没有内置PC/SC支持。
避坑指南:我曾经在某个项目里直接调用了厂商的私有API,结果换了个读卡器型号,代码全废了。后来我学乖了,一律用PC/SC标准接口。这样换读卡器只需要改底层驱动,上层代码纹丝不动。
23.3 Android端USB NFC Host实现
Android从3.1(API 12)开始支持USB Host模式。这意味着你的手机可以当主机,直接连接USB设备。实现USB NFC读卡器,本质上就是让Android跟CCID设备通信。
实现步骤大致如下:
- 声明USB Host权限:在AndroidManifest.xml中添加
<uses-feature android:name="android.hardware.usb.host" /> - 获取USB设备:通过
UsbManager获取连接的设备列表 - 请求权限:调用
requestPermission()让用户授权 - 打开设备:使用
UsbDeviceConnection打开设备 - 通信:通过批量端点发送CCID命令,接收响应
下面是一个简单的代码片段,展示如何发送APDU命令:
// 发送APDU命令到USB NFC读卡器
public byte[] sendAPDU(UsbDeviceConnection connection,
UsbEndpoint outEndpoint,
UsbEndpoint inEndpoint,
byte[] apdu) {
// 构造CCID命令头
byte[] ccidCommand = new byte[10 + apdu.length];
ccidCommand[0] = 0x6F; // PC_to_RDR_XfrBlock
ccidCommand[1] = (byte)(apdu.length & 0xFF);
ccidCommand[2] = (byte)((apdu.length >> 8) & 0xFF);
ccidCommand[3] = 0x00; // slot 0
ccidCommand[4] = 0x00; // sequence number
// ... 填充剩余头部
// 复制APDU数据
System.arraycopy(apdu, 0, ccidCommand, 10, apdu.length);
// 发送
connection.bulkTransfer(outEndpoint, ccidCommand, ccidCommand.length, 1000);
// 接收响应
byte[] buffer = new byte[1024];
int len = connection.bulkTransfer(inEndpoint, buffer, buffer.length, 1000);
// 解析CCID响应头,提取数据
byte[] response = new byte[len - 10];
System.arraycopy(buffer, 10, response, 0, len - 10);
return response;
}
小技巧:调试时可以用adb shell dumpsys usb查看USB设备信息。我经常用这个命令确认设备是否被正确识别,端点信息对不对。
23.4 ISO 14443 Type A/B卡读写
ISO 14443是非接触式智能卡的国际标准。它分为Type A和Type B两种。说白了,就是两种不同的通信方式。
| 特性 | Type A | Type B |
|---|---|---|
| 调制方式 | ASK 100% | ASK 10% |
| 编码方式 | Miller编码 | NRZ编码 |
| 典型应用 | MIFARE卡、门禁卡 | 身份证、护照 |
| 防冲突机制 | 基于UID的二进制搜索 | 基于槽位的轮询 |
Type A和Type B的APDU命令格式是一样的,但初始化流程不同。Type A需要先发送REQA(请求类型A),然后进行防冲突和选择。Type B则发送REQB,然后通过槽位号来寻卡。
我记得有一次做门禁项目,客户说卡读不出来。我排查了半天,发现是读卡器默认配置成了Type B模式,而门禁卡是Type A的。改一下配置就好了。这种问题,说白了就是协议不匹配。
23.5 实战:通过USB NFC读卡器读取身份证/门禁卡
好了,理论讲完了,我们来点实际的。下面是一个完整的实战流程,读取身份证(Type B)和门禁卡(Type A)。
读取身份证(Type B)
中国的第二代身份证使用的是ISO 14443 Type B协议。读取身份证需要先进行安全认证,这里我们只演示读取UID(唯一标识符)的过程。
// 读取身份证UID(Type B)
public byte[] readIDCardUID(UsbDeviceConnection connection,
UsbEndpoint out, UsbEndpoint in) {
// 1. 发送REQB命令
byte[] reqb = new byte[] {
0x05, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00
};
byte[] response = sendAPDU(connection, out, in, reqb);
// 2. 解析响应,获取UID
if (response[0] == 0x00) {
byte[] uid = new byte[4];
System.arraycopy(response, 1, uid, 0, 4);
return uid;
}
return null;
}
读取门禁卡(Type A)
大多数门禁卡是MIFARE Classic卡,属于Type A。读取它的UID更简单:
// 读取MIFARE卡UID(Type A)
public byte[] readMifareUID(UsbDeviceConnection connection,
UsbEndpoint out, UsbEndpoint in) {
// 1. 发送REQA命令
byte[] reqa = new byte[] { 0x26 };
byte[] response = sendAPDU(connection, out, in, reqa);
// 2. 发送防冲突命令
byte[] anticoll = new byte[] { 0x93, 0x20 };
response = sendAPDU(connection, out, in, anticoll);
// 3. 解析UID(4字节或7字节)
if (response[0] == 0x00) {
int uidLen = response[1] & 0xFF;
byte[] uid = new byte[uidLen];
System.arraycopy(response, 2, uid, 0, uidLen);
return uid;
}
return null;
}
重要提醒:读取身份证涉及个人隐私,请确保你有合法授权。我曾经有个客户想批量读取小区住户的身份证信息,被我拒绝了。技术本身没有好坏,但用的人要有底线。
23.6 常见问题与调试技巧
做USB NFC开发,你肯定会遇到各种问题。我列几个常见的:
- 设备无法识别:检查USB线是否支持数据通信,有些充电线只能充电不能传数据
- 权限问题:Android 6.0以上需要动态申请USB权限
- 超时错误:NFC通信有严格的时间要求,超时时间设置太短会导致失败
- 数据乱码:检查字节序,CCID协议用的是小端序
调试时,我建议先用PC端的工具(比如pcsc_scan)验证读卡器是否正常工作。如果PC上能读卡,Android上不行,那就是Android端的问题。如果PC上也不行,那可能是读卡器或者卡片的问题。
总结一下:
USB NFC读卡器开发,核心就是理解CCID协议和PC/SC标准。Android端通过USB Host API与读卡器通信,发送APDU命令来操作卡片。Type A和Type B的区别在于物理层,但上层APDU命令是通用的。实战中,读取身份证和门禁卡的流程大同小异,关键是要选对协议类型。
好了,这一章的内容就到这里。如果你在实际开发中遇到问题,欢迎在公众号留言交流。
公众号:蓝海资料掘金营,微信deep3321