夜雨聆风学习资料网

ARTICLE · 1049003

CH585 NFC 示例说明

CH585 NFC 示例说明
CH585  NFC示例解析说明    ...... 矜辰所致

预告一下,NFC 部分文章,我们至少会有《NFC 基础知识汇总》 (上一篇)、《CH585 NFC 示例说明》本文、《NFC 卡片数据解析》下一篇、还有最关键的《NFC 硬件设计调试指南》 这些文章,大家敬请期待 (●'◡'●) !

前言

上一篇文章,我们了解了一下 NFC 基础知识,那么本文我们来看一下 CH585 上 NFC 的使用,我们通过官方的给的 NFC 例程来学习 。

本文我们来了解一下官方 NFC 示例框架。

相关博文:NFC 基础知识汇总

我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!

📑 本文目录前言一、📡 EVT 例程框架├─ 1.1 读卡器模式├─ 1.2 卡模式└─ 1.3 关于低功耗检卡二、🔧 代码解析(M1 读卡器)├─ 2.1 整体流程└─ 2.2 NFC 通信流程 ├─ 2.2.1 发送 REQA 命令获取 ATQA ├─ 2.2.2 防碰撞 ├─ 2.2.3 Select 获取 SAK ├─ 2.2.4 Crypto1 双向认证 ├─ 2.2.5 读 / 写数据块 └─ 2.2.6 休眠三、⚙️ 其他示例说明├─ 3.1 NTAG21x 示例├─ 3.2 PCD_SOFT 示例└─ 3.3 PICC 示例 ├─ 3.3.1 模拟 M1 卡 └─ 3.3.2 模拟 Type 2 标签结语

一、 EVT 例程框架

在官方 EVT 中,NFC 的示例都在一个 NFCA 文件夹下面,而且在目录文本文档 CH585_List 种有示例说明,如下图:

对于了解 NFC 的小伙伴来说,这个目录应该是很清晰了,如果不太清楚的可以查看博主的上一篇文章 NFC 基础知识汇总 。

1.1 读卡器模式

读卡器模式提供了 对 M1 卡片 和 NTAG21x 卡片的读取, 而且提供了硬件解码和软件解码两种方式 。

卡片说明:

M1 卡是目前国内使用最广泛的 NFC 卡,价格低廉(国产兼容芯片几毛钱一张),门禁卡、公交卡、校园卡大多都是 M1。虽然 M1 卡使用 NXP 私有的 Crypto1 加密算法,但该算法早年已被学术界完全破解,加上国产兼容芯片的普及,使得国内 M1 卡生态最为成熟。

NTAG21x 卡是 NFC Forum Type 2 标签标准的代表性产品,支持标准 NDEF 数据格式,手机碰一下就能直接识别,属于国际标准、全球通用。它特别适合 BLE+NFC 联动场景,比如手机碰一碰读 URL、电子名片、蓝牙快速配对。

CH585 目前的 SDK 只支持这两种卡,是因为它们都不需要 ISO 14443-4 (T=CL) 传输层。M1 走 Crypto1 私有认证,NTAG21x 走 Type 2 标签直读协议,都在 ISO 14443-3A 层之上直接工作。需要 T=CL 的卡(如 CPU 卡、DESFire、Type 4 标签),硬件解码不支持,需用软件解码自行实现 T=CL 状态机。

软解码&硬解码说明:

软件解码的本质是:接收侧用 Timer0 定时采样 + 软件解码算法,代替了 NFC 硬件控制器的接收解码单元。发送侧仍然使用 NFC 硬件调制器。

软件解码所有上层协议逻辑(帧格式、CRC、T=CL 等)都在软件里实现,因此灵活性更高——比如它可以读取硬件解码无法识别的 CPU 卡。

软件解码灵活性的代价:软件解码需要占用更大的 RAM (示例中硬件解码数据缓冲区 64字节,软件解码 8K,8192字节),并且还需要额外占用一个定时器,CPU 负担加重,程序开发复杂度也更高。

对比项
硬件解码
软件解码
发送侧 (TX)
NFC 硬件调制器
NFC 硬件调制器(相同)
接收侧 (RX)
NFC 硬件解调器 + 硬件解码
Timer0 采样 + 软件解码
占用资源
NFC 硬件中断
NFC 中断 + Timer0 + 8KB RAM
CPU 负担
低(硬件做完了)
高(中断内跑解码算法)
灵活性
低(只认 ISO 14443-3A 帧)
高(可自定义帧格式和协议)
支持卡类型
M1 + NTAG21x
M1 + NTAG21x + CPU卡(需写T=CL)
开发复杂度
低(库函数封装好了)
高(更多协议逻辑需自行处理)

1.2 卡模式

卡模式(PICC)即 CH585 主动模拟成一张 NFC 卡,被外部读卡器(或手机)读取。

CH585 提供两种卡模拟:PICC_M1(模拟 M1 卡)和 PICC_T2T(模拟 Type 2 标签)。

两种卡模式对比

对比项
PICC_M1
PICC_T2T
模拟对象
MIFARE Classic S50 (M1)
NFC Forum Type 2 标签
存储结构
16扇区 × 4块 × 16字节 = 1KB
可配置页 × 4字节 (默认135页)
UID 长度
4字节(单级防碰撞)
7字节(双级防碰撞)
加密方式
Crypto1 硬件加密
无加密
手机兼容性
差(手机不直接支持 M1 扇区读写)
好(手机 NFC 原生支持 NDEF 读取)
认证方式
双向 Crypto1 认证
ATQA / SAK
0x0004 / 0x08
0x0044 / 0x00
主要场景
门禁/一卡通(替代实体卡)
手机交互/NDEF/BLE配对
CH585 独特价值
手机做不到的 M1 卡模拟
BLE+NFC 联动的天然优势

模拟 Type2 卡说明

有些小伙伴可能会有疑问,我们之前学过 NTAG 213/215/216 和 MIFARE Ultralight(EV1 / C) 都是 Type 2 类型的卡,那PICC_T2T 示例是模仿的哪一种卡呢?

核心要明白:Type 2 是协议规范,不是某一款卡:

Type 2 NFC Forum 定义的一套通信协议和数据格式规范NTAG 213/215/216 NXP 按这个规范造的具体硬件产品,不同型号就是容量不同MIFARE Ultralight 也是按同一个规范造的产品

CH585 实现的是 Type 2 的协议行为:按 Type 2 规范响应读写命令,对外提供 NDEF 数据,内存大小由代码配置,手机去读取得到的是一张支持 NDEF 的 Type 2 标签(手机拿到 SAK=0x00 + ATQA=0x0044,就判定"这是 Type 2 标签",然后去读 CC 和 NDEF)。

至于是 NTAG 还是 Ultralight,取决于代码里配多大的内存映射、放什么 NDEF 内容。

理论参考表格如下:

模拟目标
PAGES_NUM
cc[2]
可用内存
Ultralight (64B)
16
0x06
56B
NTAG 213 (144B)
45
0x12
144B
NTAG 215 (504B)
135(默认)
0x3E(默认)
504B
NTAG 216 (888B)
231
0x6E
864B

仅供参考,具体实现有待验证其中 PAGES_NUM 为宏定义 WCH_NFCA_PICC_T2T_PAGES_NUM ,在示例工程 wch_nfca_picc_t2t.h 头文件中cc[2] 为卡片初始化,在示例工程wch_nfca_picc_t2t.c 文件中 g_nfca_picc_t2t_data.cc[2] 变量赋值

1.3 关于低功耗检卡

参考 EVT 里 NFCA 文件夹中 《CH585 NFC读卡器应用指南》说明:

低功耗检卡分为常规低功耗检卡和超低功耗检卡,常规低功耗检卡比超低功耗检卡功耗高,兼容性高。

常规低功耗检卡

常规低功耗检卡通过周期性的启动发送WUPA 命令来检测是否有卡片回复的方式实现低功耗检卡。

该方式分为如下4 个步骤:

  1. 1. 打开13.56M 信号发送;
  2. 2. 发送100us 左右后(以便于卡片获取能量可以正常工作),开始发送 WUPA 命令;
  3. 3. 发送完 WUPA 命令后,等待卡片回复(协议规定时间约为 92us)。
  4. 4. 如有回复,则进行后续操作,如果无回复,则关闭天线退出检卡。

常规低功耗检卡需要工作一段时间才可以判断有卡无卡 。

超低功耗检卡

在一些使用对功耗要求极高的场合,可以使用超低功耗建卡,但是它的注意事项比较多,也有一些严格的条件控制。

步骤如下:

  1. 1. 打开13.56M 信号发送;
  2. 2. 检测NFC 内部ADC 的值;
  3. 3. 通过阈值变化等判断来进行有卡无卡的检测,如果有卡则进行后续通讯,无卡则关闭天线。

对于超低功耗检卡,必须要注意的事项:

  • • 该方式一般与蓝牙睡眠同步使用,需要注意的是,在蓝牙例程中,使能HAL_SLEEP 后,初始化时不可将蓝牙代码中的关于NFC 的4 个引脚默认上拉,否则会影响超低功耗检卡的基线校准。
  • • 当与触摸一起使用时,需要注意在寄存器R8_TKEY_CFG 的 RB_TKEY_PWR_ON 位使能期间,R8_ADC_CHANNEL 不可以为 NFC 的检测通道,否则会将影响 NFC 检卡,需要一段时间才能恢复。
  • • 超低功耗检卡,在无卡时,分压电阻需要调整到NFCI 的峰峰值为2.9V 左右为佳。
  • • 该方式必须将天线匹配到13.56M 附件且回波损耗越大越好,不然天线信号的变化不明显,不能有效的检测到卡片靠近。

二、代码解析(M1 读卡器)

这里我们使用 MifareClassic 读取 M1 示例来说明

示例工程中,头文件    wch_nfca_mifare_classic.h     定义了 M1 卡的完整 API。

2.1 整体流程

工程整体流程:

  1. 1. pcd 读卡器初始化 nfca_pcd_init配置对应的 GPIO 引脚;分配内存缓冲区给 NFC 库;根据配置 初始化 pcd_lib 。
  2. 2. 低功耗检卡校准 nfca_pcd_lpcd_calibration :这个校准函数就是在无卡状态下测一个基线。天线在弱射频场下,ADC 采样天线信号幅度,作为无卡时的基线值。后续每次 nfca_pcd_lpcd_check() 时,采一次 ADC 和基线比,差得够多就说明有卡靠近了。检测到有卡才启动完整射频场做 REQA/防碰撞,以此来达到低功耗检卡的目的。

必须在无卡时候进行校准。工程应用中,每次开机,或者定期,或者出厂时候校准。

  1. 3. 进入循环检卡/通讯流程。开启 pcd 读卡器功能 nfca_pcd_ctr_on,射频场打开后会持续发射 13.56MHz 载波。

做 NFCI 引脚上的电压峰峰值测量的时候,在这个语句后面加一个 while(1) 死循环。进入通讯流程(流程我们下面再单独详细解释);停止 pcd 读卡器功能 nfca_pcd_stop

2.2 NFC 通信流程

我们之前理论学习过 NFC (ISO14443-A) 完整通信流程,现在我们可以通过示例代码去对照一下这个流程,就是上面的第三步 循环检卡/通讯流程,也就是示例中函数 nfca_pcd_test 里面 while(1) 循环实现的部分。

经过前面必要的 nfca_pcd_start() 和可选的低功耗检卡,如果发现了卡片,就进入我们的 NFC 通信流程:

2.2.1 发送 REQA 命令获取 ATQA

语句 res = PcdRequest(PICC_REQALL);

//PcdRequest 函数里面g_nfca_pcd_send_buf[0] = req_code;      // 0x52 (REQALL) 或 0x26 (REQIDL)nfca_pcd_communicate(7, NORMAL, 0);      // 只发7个bit!

根据返回结果的不同走不同的 if 分支 。

0x0004 → S50(1KB,16扇区×4块)→ 用 PcdAuthState + PcdRead0x0002 → S70(4KB,40扇区)→ 同样流程,但扇区遍历不同0x0044 → Ultralight/NTAG(Type 2 标签)→ 跳过认证,直接读取,示例只演示读取卡号

2.2.2 防碰撞

语句res = PcdAnticoll(PICC_ANTICOLL1);

每个分支里面都会有,防碰撞是必须的流程。

//PcdAnticoll 函数里面send[0] = cmd;      // 0x93 (ANTICOLL1)send[1] = 0x20;     // NVB = 0x20 表示"防碰撞开始,请给我完整UID"nfca_pcd_communicate(16, NORMAL, 0);   // 16 bit = 2字节

发送 2 字节命令,接收卡片 UID 。

2.2.3 Select 获取SAK

根据 UID 选择卡片,获取卡片的 SAK。

语句 res = PcdSelect(PICC_ANTICOLL1, picc_uid);

//PcdSelect 函数里面send[0] = 0x93;     // SELECT CL1send[1] = 0x70;     // NVB = 0x70 表示"防碰撞结束,选中这张卡"send[2..5] = UID;    // 4字节UIDsend[6] = BCC;       // 校验ISO14443AAppendCRCA(send, 7);  // 追加2字节CRC-A → 共9字节nfca_pcd_communicate(9*8, NORMAL, 0);  // 72 bit

接收 3*9=27 bit(SAK + 2字节CRC)。

2.2.4 Crypto1 双向认证

语句 res = PcdAuthState(PICC_AUTHENT1A, 0, default_key, picc_uid);

首先,读卡器发送认证请求:

// PcdAuthState 函数里面send[0] = auth_mode;   // 0x60=KeyA, 0x61=KeyBsend[1] = addr;         // 块地址ISO14443AAppendCRCA(send, 2);  // +CRC = 4字节// 此时 is_encrypted=0,所以用普通奇校验nfca_pcd_communicate(4*8, NORMAL, 0);  // 发送4字节// 接收4字节 = 卡的随机数挑战(tag_challenge)

然后计算密钥流:

// PcdAuthState 函数里面nfca_pcd_crypto1_setup(    &g_m1_crypto1_cipher,     // Crypto1状态机    pKey,                      // 6字节密钥    pUid,                      // 4字节UID    tag_clg,                   // 卡的挑战(4字节)    tag_clg_parity,    nfca_pcd_rand(),           // 读卡器随机数    reader_clg,                // → 生成8字节读卡器挑战    reader_clg_parity,    tag_rsp,                   // → 预期卡响应(4字节)    tag_rsp_parity);

最后验证:

// PcdAuthState 函数里面nfca_pcd_communicate(8*8, NORMAL, 0);  // 发送8字节加密挑战// 接收4字节加密响应if (tag_rsp[0..3] == recv_buf[0..3]   // 逐字节比对    && tag_rsp_parity[0..3] == parity[0..3]){    g_m1_crypto1_cipher.is_encrypted = 1;  // 认证成功!此后全部加密}

后续所有 PcdRead/PcdWrite 都会先调用 nfca_crypto1_encrypt 加密发送数据,再调用 nfca_crypto1_decrypt 解密接收数据。

2.2.5 读/写数据块

读写数据块都是加密的,示例中只展示了读取。

语句 res = PcdRead(i);

//PcdRead 函数中send[0] = 0x30;        // PICC_READsend[1] = addr;         // 块号ISO14443AAppendCRCA(send, 2);  // +CRC = 4字节nfca_crypto1_encrypt(&cipher, send, send, parity, 4*8);  // ← 加密!nfca_pcd_communicate(4*8, NORMAL, 0);// 接收 18*9=162 bit (16字节数据 + 2字节CRC + 各校验位)nfca_crypto1_decrypt(&cipher, recv, recv, parity, 18*8);  // ← 解密!

2.2.6 休眠

语句 PcdHalt() :

// PcdHalt 函数中send[0] = 0x50;  send[1] = 0x00;send[2] = 0x57;  send[3] = 0xcd;  // 固定CRCnfca_pcd_communicate(4*8, NORMAL, 0);// 不等待响应

整个流程示意图如下:

针对上图的说明:步骤 0-6(明文):走 ISO 14443-3A 标准,所有卡片通用 — 库函数 + 开源 ISO14443-3A.c 实现步骤 7(分界线):Crypto1 认证成功 → is_encrypted = 1 → 此后所有数据经 Crypto1 流密码加解密步骤 8-9(加密):走 M1 私有应用层 — 只有 M1 卡需要认证,NTAG21x 在步骤 6 后直接跳到页式读写(无步骤7)NTAG21x 路径:步骤1→2→3→4(两级)→5→6 → 直读页数据(跳过步骤7认证)→ 9

三、其他示例说明

只要完全了解了上面的流程,其他的卡片基本框架上都是一致的,我们都简单说明一下。

3.1 NTAG21x 示例

与 M1 卡读取不同的地方有

  • • NTAG21x 卡片式 7bit UID,需要经过 2 级防碰撞。
  • • 不需要经过 Crypto1 认证,可以直接读取卡片数据
main()├── nfca_pcd_init()                    // 初始化硬件├── nfca_pcd_lpcd_calibration()        // LPCD 基线校准└── nfca_pcd_test()                    // 主测试循环    └── while(1)        ├── nfca_pcd_start()           // 开射频场        ├── nfca_pcd_lpcd_check()      // 低功耗检卡        │   ├── 返回0 → "NO CARD" → 跳过        │   └── 非0 → "CARD DETECT"        ├── mDelaymS(5)               // 等卡稳定        ├── nfca_pcd_ctr_handle()      // PA7分压切换(可选)        │        ├── PcdRequest(PICC_REQALL)   // 步骤1: REQA        │   └── 判断 res == 0x0044?     // 不是Type2就跳过        │        ├── PcdAnticoll(CL1)          // 步骤2: 一级防碰撞        │   └── 检查 buf[0]==0x88?     // 级联标记        │       └── 取 UID[0:2]        │        ├── PcdSelect(CL1)            // 步骤3: 一级选中        │        ├── PcdAnticoll(CL2)          // 步骤4: 二级防碰撞        │   └── 取 UID[3:6]        │        ├── PcdSelect(CL2)            // 步骤5: 二级选中        │   └── 打印 SAK        │        ├── PcdNTAG21xGetVersion()     // 步骤6: 获取版本        │   └── 判断 storage_size → NTAG213/215/216/UNKNOWN        │        ├── PcdNTAG21xReadSIG()        // 步骤7: 读原厂签名(32B)        ├── PcdNTAG21xRead(0)          // 步骤8: 读PAGE 0-3 (16B)        ├── PcdNTAG21xFastRead(4,35)   // 步骤9: 快速读PAGE 4-35 (128B)        ├── PcdNTAG21xReadCNT()        // 步骤10: 读计数器        └── PcdHalt()                  // 步骤11: 休眠

3.2 PCD_SOFT 示例

软件解码和硬件解码,对于应用层来说,可以说流程完全一致。

只是对于部分函数替换成了 软件解码所需要的函数,带_soft 名称,但是这个库也是官方写好的,这里也没有什么需要过多说明,如果式想深入了解的同学可以自行研究软件的实现。

3.3 PICC 示例

模拟卡的整体框架也就是:

  1. 1. 初始化(硬件,库,卡片内容,注册回调函数 ) ;
  2. 2. 进入死循环等待读卡器来读取 ;
  3. 3. 通过中断回调函数进行逻辑交互处理 。

3.3.1 模拟 M1 卡

示例也简单给个流程图:

main()├── nfca_picc_init()                    // 初始化PICC硬件├── nfca_picc_m1_enable(uid)            // 注册M1回调 + 初始化内存│   ├── UID = {0x12, 0x34, 0x56, 0x78}│   ├── BCC = 0x12^0x34^0x56^0x78 = 0x78│   ├── SAK = 0x08, ATQA = {0x04, 0x00}│   └── 64块全部初始化:│       ├── 数据块(0,1,2,4,5,6...): 全0│       └── 尾块(3,7,11...): FF×6 + 07 80 69 + FF×6├── nfca_picc_start()                   // 启动(Timer0接收 + Timer3发送 + DMA)└── while(1)                            // 等读卡器来读

如果有读卡器靠近,会进入回调函数 nfca_picc_m1_data_handler 处理:

收到命令
在哪个状态
响应动作
REQA/WUPA (0x26/0x52)
IDLE
回 ATQA,→READY
SELECT_CL1 (0x93)
READY
验 UID,回 SAK=0x08,→ACTIVE,重置加密
AUTH1A/1B (0x60/0x61)
ACTIVE
Crypto1 setup,回挑战,→AUTHING
READ (0x30)
未认证
回 NAK (0x04),→IDLE
READ (0x30)
认证后
解密→读 16B→加密回
WRITE (0xA0)
认证后
回 ACK,→WRITING
WRITE 第二阶段
WRITING
解密→写 16B→回 ACK,→AUTHED_IDLE
HALT (0x50)
任意
→IDLE

3.3.2 模拟 Type 2 标签

模拟 Type 2 标签也差不多类似:

main()├── nfca_picc_init()├── nfca_picc_t2t_enable(uid)           // 注册T2T回调 + 初始化内存│   ├── UID = {0x00, 0xae, 0x38, 0xe2, 0xb5, 0x4c, 0x80}  (7字节)│   ├── CL1: CT(0x88) + UID[0:2] + BCC0│   ├── CL2: UID[3:6] + BCC1│   ├── ATQA = {0x44, 0x00}│   ├── Internal = 0x48 (EV1标识)│   ├── Lock = {0x00, 0x00}│   ├── CC = {0xE1, 0x10, 0x3E, 0x00}   // 声明504字节(像NTAG215)│   ├── Page 4 = {0x03, 0x00, 0xFE, 0x00} // 空NDEF│   └── 总共135页(可配置)├── nfca_picc_start()└── while(1)

回调函数 nfca_picc_t2t_data_handler 处理:

收到命令
动作
REQA/WUPA
回 ATQA={0x44,0x00},→READY1
SELECT_CL1 (0x93)
回 SAK=0x24(不完整标记),→READY2
SELECT_CL2 (0x95)
回 SAK=0x00,→ACTIVE
READ (0x30)
读4页(16B)+CRC回
WRITE (0xA2)
页号<135且>3才写,回ACK/NAK
HALT (0x50)
→IDLE
其他命令
不处理,直接→IDLE

结语

本文我们介绍了一下 NFC 的示例框架,通过 CH585 NFC 的示例再次巩固复习了一遍完整的通信流程 。

对于 CH585 NFC 的应用,本文加上上一篇《NFC 基础知识汇总》 已经足够带你入门,但是 NFC 真正应用的难点在于 NFC 电路的设计:天线滤波电路、匹配网络、天线设计、接收电路设计,对于这部分,官方在 NFCA 例程目录下还单独给出了《CH585 NFC读卡器应用指南》文档说明,博主后面也会重点的来带大家走一遍这部分流程,这部分需要好好准备一下。

当然在此之前,我们还有一篇文章带大家学习一下常用 NFC 卡片的数据格式。

好了,本文就到这里。谢谢大家!

感谢您的阅读分享与点赞!

相关学习资料