ARTICLE · 1049003
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 负担加重,程序开发复杂度也更高。
1.2 卡模式
卡模式(PICC)即 CH585 主动模拟成一张 NFC 卡,被外部读卡器(或手机)读取。
CH585 提供两种卡模拟:PICC_M1(模拟 M1 卡)和 PICC_T2T(模拟 Type 2 标签)。
两种卡模式对比
模拟 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 为宏定义
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. 打开13.56M 信号发送; 2. 发送100us 左右后(以便于卡片获取能量可以正常工作),开始发送 WUPA 命令; 3. 发送完 WUPA 命令后,等待卡片回复(协议规定时间约为 92us)。 4. 如有回复,则进行后续操作,如果无回复,则关闭天线退出检卡。
常规低功耗检卡需要工作一段时间才可以判断有卡无卡 。
超低功耗检卡
在一些使用对功耗要求极高的场合,可以使用超低功耗建卡,但是它的注意事项比较多,也有一些严格的条件控制。
步骤如下:
1. 打开13.56M 信号发送; 2. 检测NFC 内部ADC 的值; 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. pcd 读卡器初始化 nfca_pcd_init:配置对应的 GPIO 引脚;分配内存缓冲区给 NFC 库;根据配置 初始化 pcd_lib 。2. 低功耗检卡校准 nfca_pcd_lpcd_calibration:这个校准函数就是在无卡状态下测一个基线。天线在弱射频场下,ADC 采样天线信号幅度,作为无卡时的基线值。后续每次 nfca_pcd_lpcd_check() 时,采一次 ADC 和基线比,差得够多就说明有卡靠近了。检测到有卡才启动完整射频场做 REQA/防碰撞,以此来达到低功耗检卡的目的。
必须在无卡时候进行校准。工程应用中,每次开机,或者定期,或者出厂时候校准。
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. 初始化(硬件,库,卡片内容,注册回调函数 ) ; 2. 进入死循环等待读卡器来读取 ; 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 处理:
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 处理:
结语
本文我们介绍了一下 NFC 的示例框架,通过 CH585 NFC 的示例再次巩固复习了一遍完整的通信流程 。
对于 CH585 NFC 的应用,本文加上上一篇《NFC 基础知识汇总》 已经足够带你入门,但是 NFC 真正应用的难点在于 NFC 电路的设计:天线滤波电路、匹配网络、天线设计、接收电路设计,对于这部分,官方在 NFCA 例程目录下还单独给出了《CH585 NFC读卡器应用指南》文档说明,博主后面也会重点的来带大家走一遍这部分流程,这部分需要好好准备一下。
当然在此之前,我们还有一篇文章带大家学习一下常用 NFC 卡片的数据格式。
好了,本文就到这里。谢谢大家!
