第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)的读取
USB NFC读卡器开发知识体系 应用层:身份证/门禁卡读取 协议栈 ISO 14443 Type A/B PC/SC 标准 CCID 协议 Android USB Host API USB NFC 读卡器硬件

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设备通信。

实现步骤大致如下:

  1. 声明USB Host权限:在AndroidManifest.xml中添加<uses-feature android:name="android.hardware.usb.host" />
  2. 获取USB设备:通过UsbManager获取连接的设备列表
  3. 请求权限:调用requestPermission()让用户授权
  4. 打开设备:使用UsbDeviceConnection打开设备
  5. 通信:通过批量端点发送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