简介:面向物联网与智能卡开发者的CLRC663读写器工程包,覆盖14443A与ISO/IEC 15693两种主流非接触式协议,适用于门禁、交通卡、库存管理、商品防伪等场景,解决UID读取、数据块读写、卡片认证及ATS解析等关键问题。包内共有896个文件,以C/H源码、Keil工程文件(uvprojx/uvoptx)、编译生成的O/CRF中间文件为核心,辅以汇编、包含文件、PDF与CHM说明文档等,便于在STM32平台上直接移植、编译与调试。压缩包整体28.72MB,目录组织清晰,可快速定位驱动层、应用层及参考文档。已有782人学习下载。借助该工程,读者既能掌握14443A协议下防冲突、选卡、认证与块读写的完整流程,也能理解15693协议对EPC、TID及用户数据块的访问方法;重点代码如ATS响应解析、CLRC63302读取流程均有对应实现,能显著缩短非接触式读写模块的开发周期,适合有一定STM32基础并希望深入NFC/RFID底层交互的工程师。
1. 从 ATS 到读写:CLRC663 为什么能同时吃下 14443A 和 15693
门禁、图书借阅、工业托盘管理这类设备里,CLRC663 是很常见的一颗 NFC 前端。它支持 ISO 14443A/B、ISO 15693,还能兼容 MIFARE 系列卡片,这意味着同一块电路板既可以读身份证式的小卡,也能批量清点货架上的超高频标签。标题里把 14443A 读写块、15693 和读取 ATS 放在一起,正好对应项目落地时最常踩的三个坑:协议切换时指令帧对不上、块地址映射搞混、ATS 解析不到位。ATS(Answer To Select)是 14443A 卡片被选中后回给读写器的协议参数帧,决定后续帧的 CID、NAD 以及位速率能否切换。CLRC63302 这类型号在寄存器层与 CLRC663 完全一致,所以下文所有代码可以直接照搬到同系列芯片上。先从最底层的命令交互说起。
2. CLRC663 寄存器与命令层:读写 14443A 块之前先把宿主总线打通
2.1 三种主机接口选型:SPI/I2C/UART 各自的初始化差异
CLRC663 的 EA1 和 EA0 引脚决定主机接口。复位时芯片会锁存这两个引脚的电平组合,常见做法是优先选 SPI,因为 CLRC663 的 SPI 最高能到 10 Mbps,而 I2C 只有 400 kHz,UART 模式还要额外组装专用的串口帧,反而更麻烦。只做门禁刷卡这种慢速操作,I2C 也够用,但一旦涉及 15693 多标签轮询或连续多块读取,FIFO 里的数据会频繁搬移,SPI 的时序余量明显更足。
| EA1 | EA0 | 接口 | 最大速率 | 典型场景 |
|---|---|---|---|---|
| L | L | SPI | 10 Mbps | 多标签轮询、高速块读写 |
| H | L | I2C | 400 kHz | 低功耗门禁、单卡操作 |
| L | H | UART | 1228.8 kbps | 旧平台串口直连 |
初始化 SPI 时,NSS 不要接硬件片选,而是用 GPIO 手动控制。CLRC663 的寄存器读写要求在 NSS 低电平期间完成地址和数据两个字节的交替发送,硬件 NSS 的自动翻转会在某些 MCU 上提前拉高,导致读回来的数据整体错位。下面是最小读写例程,以 STM32 HAL 库为例:
// CLRC663 SPI 读寄存器:地址字节低位置 0 表示读 uint8_t clrc663_read_reg(uint8_t addr) { uint8_t tx[2] = { (addr << 1) | 0x00, 0x00 }; uint8_t rx[2] = { 0, 0 }; CLRC663_NSS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10); CLRC663_NSS_HIGH(); return rx[1]; } // CLRC663 SPI 写寄存器:地址字节低位置 1 表示写 void clrc663_write_reg(uint8_t addr, uint8_t val) { uint8_t tx[2] = { (addr << 1) | 0x01, val }; CLRC663_NSS_LOW(); HAL_SPI_Transmit(&hspi1, tx, 2, 10); CLRC663_NSS_HIGH(); }地址左移一位是因为 CLRC663 的总线协议把地址和读写标志位交织在一个字节里。拿到总线读写能力后,第一件事是读 ChipID 寄存器确认时序工作正常,CLRC663 的 ChipID 固定为 0x07。如果读出来不是 0x07,先查 NSS 时序、SPI 模式(CPOL=0/CPHA=0)和 3.3V 供电是否稳定,不要急着往下调试射频。
2.2 FIFO 缓冲与 Command 寄存器的配合——写命令、读状态的基本时序
CLRC663 内部有一个深度 512 字节的 FIFO,所有对外发送的数据都要先写进 FIFO,再通过 TransactionCommand 寄存器触发动作。同理,收到的响应也会出现在 FIFO 里。整个交互过程可以封装成一个transceive函数,后续 14443A 和 15693 的读写都复用它:
// 将 data 写入 FIFO 并触发 Transceive,等待响应后读回 int clrc663_transceive(const uint8_t *data, uint16_t len, uint8_t *resp, uint16_t max_rsp_len, uint16_t *resp_len) { // 0x0D 是 FIFO 清空命令对应的 Command 值 clrc663_write_reg(REG_COMMAND, CMD_CLEAR_FIFO); for (int i = 0; i < len; i++) { clrc663_write_reg(REG_FIFO_DATA, data[i]); } // 发送并等待响应:Transceive 命令 clrc663_write_reg(REG_COMMAND, CMD_TRANSCEIVE); // 轮询 FIFO 长度,等待响应数据写入 uint32_t start = HAL_GetTick(); while (clrc663_read_reg(REG_FIFO_LENGTH) == 0) { if (HAL_GetTick() - start > 50) return -1; // 50ms 超时 } uint16_t rlen = clrc663_read_reg(REG_FIFO_LENGTH); *resp_len = rlen; for (int i = 0; i < rlen && i < max_rsp_len; i++) { resp[i] = clrc663_read_reg(REG_FIFO_DATA); } return rlen; }每次发送前先执行 FIFO 清空,是为了避免上一次残留数据混进本次命令。CMD_TRANSCEIVE触发后,CLRC663 会自动处理帧起始、CRC 校验和接收窗口,MCU 不用介入底层位流。响应数据读出来后,还需要检查 ErrorFlag 寄存器,如果该寄存器不为 0,说明通信过程中发生了 CRC 错误、奇偶校验错误或帧格式错误。这段封装是后面所有示例代码的基础,移植时只需要根据具体头文件把REG_COMMAND、REG_FIFO_DATA等宏定义替换成实际地址即可。
3. 14443A 协议流程与 CLRC663 读取 ATS 的实现
3.1 从 REQA 到 ATS:14443A 激活窗口的字节级拆解
ISO 14443A 卡片从进入射频场到能够交换数据,中间隔着 REQA → ATQA → 防碰撞 → SEL → SAK → RATS → ATS 这一串步骤。REQA 由 CLRC663 自动完成,MCU 只需要在收到 SAK 后判断下一步动作。SAK 的 bit6(值为 0x20)如果被置位,说明卡片支持 ISO 14443-4 协议层,那就需要发 RATS 并等待 ATS。
很多人在这里偷懒:SAK 拿到后直接发 APDU,结果卡片毫无回应。原因就是跳过了 RATS/ATS 这个“握手”动作。RATS 是一个两字节帧,第一个字节固定为 0xE0,第二个字节低四位是 CID(卡片标识),高四位是 FSDI(读写器最大帧尺寸指数)。我一般发0xE0 0x80,即 CID=0、FSDI=8,表示读写器能接收 256 字节的帧。
读写器 -> 卡片: RATS (E0 80) 卡片 -> 读写器: ATS (TL T0 TA TB TC 历史字节)ATS 的第一个字节 TL 表示整个 ATS 的长度,第二个字节 T0 的位含义很关键:高四位表示后面跟的历史字节数,bit3(0x08)表示存在 TA,bit2(0x04)表示存在 TB,bit1(0x02)表示存在 TC。这些字段决定后续传输的帧尺寸、等待时间和 CID 使用规则。
3.2 用 CLRC663 发 RATS 并解析 ATS 响应的 C 代码框架
读取 ATS 的完整流程分两步:先通过transceive发送 RATS,再把响应的字节填充到结构体里。下面是可直接用于项目的代码:
typedef struct { uint8_t tl; uint8_t t0; uint8_t ta; uint8_t tb; uint8_t tc; uint8_t hist[15]; uint8_t hist_len; } clrc663_ats_t; // 发送 RATS 并读取 ATS 原始字节 int clrc663_read_ats(uint8_t *ats_buf, uint8_t *ats_len) { uint8_t rats[2] = { 0xE0, 0x80 }; // CID=0, FSDI=8, FSD=256 uint8_t rsp[32]; uint16_t rsp_len = 0; int ret = clrc663_transceive(rats, 2, rsp, sizeof(rsp), &rsp_len); if (ret < 0) return -1; // 检查协议错误标志 if (clrc663_read_reg(REG_ERROR_FLAG) != 0) return -2; memcpy(ats_buf, rsp, rsp_len); *ats_len = (uint8_t)rsp_len; return 0; }解析 ATS 时要注意 T0 的位映射关系。历史字节数存放在 T0 的高四位,不要直接用t0 & 0x0F去取长度,那样会把 TA/TB/TC 的标记位混进来:
// 解析 ATS 结构体,buf 是 ATS 原始字节 int clrc663_parse_ats(const uint8_t *buf, uint8_t len, clrc663_ats_t *ats) { if (len < 2) return -1; ats->tl = buf[0]; ats->t0 = buf[1]; ats->ta = ats->tb = ats->tc = 0; ats->hist_len = 0; uint8_t idx = 2; if (ats->t0 & 0x08) ats->ta = buf[idx++]; // TA 存在 if (ats->t0 & 0x04) ats->tb = buf[idx++]; // TB 存在 if (ats->t0 & 0x02) ats->tc = buf[idx++]; // TC 存在 // 高四位是历史字节数,最多 15 字节 ats->hist_len = (ats->t0 >> 4) & 0x0F; if (ats->hist_len > 15) ats->hist_len = 15; for (int i = 0; i < ats->hist_len; i++) { ats->hist[i] = buf[idx++]; } return 0; }clrc663_read_ats里等待响应时依赖transceive中的 FIFO 轮询,如果卡片不支持 RATS(部分 SAK 不含 0x20 的卡),函数会一直等到超时。实际使用时,建议把超时时间从 50ms 缩短到 20ms,避免在轮询多张卡时卡顿。ATS 的 TL 字段可以用于校验响应是否合法:TL 最小为 1,最大为 20,超过 20 字节直接判定为非法帧。
3.3 ATS 里 TL/T0/TA/TB/TC 的意思与参数边界
ATS 各字段的用处要在实际项目里落到寄存器配置上。TA 中的 FSCI 字段表示卡片能接收的最大帧尺寸,取值从 0 到 8,对应 16 到 256 字节。TB 中的 FWI 表示帧等待时间指数,SFGI 表示防卡死保护时间指数。TC 的 bit0 表示卡片是否支持 CID,bit1 表示 NAD。这些参数会直接影响后续 APDU 传输的超时设置和分帧逻辑。
| ATS 字段 | 含义 | 常见值 | 实际影响 |
|---|---|---|---|
| TL | ATS 总长度 | 1~20 | 超过 20 判为非法 |
| T0 高四位 | 历史字节数 | 0~15 | 决定解析偏移 |
| TA | 帧尺寸和类型 | FSCI | FSD 上限 |
| TB | 等待和保护时间 | FWI/SFGI | 超时计算 |
| TC | CID/NAD 支持 | bit0/bit1 | 命令帧构造 |
FWI 换算成实际时间要知道 CLRC663 的基准单位:1/fc 约等于 73.7ns,FWT 等于 256 乘以 2 的 FWI 次方再除以 fc。FWI=8 时 FWT 约 4.83ms,FWI=13 时约 154.6ms。所以读 ATS 时如果拿到 FWI=8,后续命令的超时至少留 6ms,否则高速传输时会在等待中断上白白丢卡。
4. 15693 读写与 14443A 读写:块地址与命令帧的差异
4.1 15693 的 Inventory/Read/Write 命令和 14443A 块读写的差异
ISO 15693 的帧格式与 14443A 完全不同。15693 的命令帧由命令码、UID 和参数三部分组成,读写的最小单位是块(Block),大多数厂商的块大小固定为 4 字节。而 14443A 的块读写往往走 MIFARE 或 ISO 14443-4 的 APDU,块地址、块大小因卡型而异。两者不能直接混用。
| 对比项 | 14443A(MIFARE 类) | 15693 |
|---|---|---|
| 读取命令 | READ 0x30 | Read Single Block 0x20 |
| 块大小 | 16 字节(Classic) | 4 字节(常见) |
| 块地址范围 | 0~63(页面翻倍) | 0~255 |
| UID 长度 | 4/7/10 字节 | 8 字节 |
| 指令头 | 命令码 + 块地址 | 命令码 + 8 字节 UID + 块地址 |
15693 的 Inventory 命令返回的 UID 是 8 字节,这比 14443A 的 UID 多出不少,导致驱动代码里 buf 操作很容易越界。另一个常见差异是错误响应:15693 对不存在的块号返回特定错误码,而 14443A 通常是直接无响应。所以写代码时要把超时当成一种正常分支来处理,不能只依赖返回值。
4.2 用 CLRC663 完成 15693 单块读写的最小代码
15693 的 Read Single Block 命令格式是命令码 0x20 加 8 字节 UID 再加 1 字节块号。CLRC663 会自动生成 CRC,MCU 只需要填充数据:
// 15693 读取单个块,uid 为 8 字节数组 int nfc15693_read_block(const uint8_t *uid, uint8_t block_no, uint8_t *out) { uint8_t cmd[10]; cmd[0] = 0x20; // READ_SINGLE_BLOCK memcpy(cmd + 1, uid, 8); cmd[9] = block_no; // 块号 0~255 uint8_t rsp[8]; uint16_t rsp_len = 0; int ret = clrc663_transceive(cmd, 10, rsp, sizeof(rsp), &rsp_len); if (ret < 0) return -1; // 响应第一个字节为状态:0x00 成功,0x02 块不存在 if (rsp[0] != 0x00) return rsp[0]; memcpy(out, rsp + 1, 4); // 4 字节块数据 return 0; }Write Single Block 的格式与读类似,命令码改为 0x21,指令里带上要写入的 4 字节数据,响应只有一个状态字节。注意:15693 写操作完成后,有些标签需要几十毫秒的内部写周期,此时不能立即再发下一条命令,否则标签可能不响应。我一般会在写操作后加一个 5 到 20ms 的延时,具体值看标签数据手册。
15693 的多块读取命令是 0x23,可以一次读多个块,响应数据按块顺序排列。使用多块读取时,FIFO 长度可能超过 32 字节,要确认transceive函数的接收缓冲区足够大,并且把 FIFO 水位中断打开,否则数据可能被截断。15693 的 256 字节最多 64 块,读满整卡的操作需要分多次进行。
4.3 14443A 块读写实操与常见误区
14443A 的块读写比 15693 多一层复杂度。以 MIFARE Classic 为例,读取一个块要先通过认证,认证通过后才能用 READ 命令读 16 字节数据。CLRC663 的认证命令与普通 Transceive 不同,它是芯片内部完成密钥计算后直接发认证帧,MCU 只需要配置密钥寄存器和认证命令的参数。下面这段代码是在完成认证之后读取块数据的框架:
// 14443A MIFARE Classic 读块,块地址为 block_no int mifare_read_block(uint8_t block_no, uint8_t *out) { // MIFARE Classic 读命令:0x30 + 块地址 + CRC uint8_t cmd[4] = { 0x30, block_no, 0x00, 0x00 }; // CRC 由 CLRC663 自动追加,发送两字节即可 uint8_t rsp[18]; uint16_t rsp_len = 0; int ret = clrc663_transceive(cmd, 2, rsp, sizeof(rsp), &rsp_len); if (ret != 16) return -1; // 有效响应是 16 字节数据 memcpy(out, rsp, 16); return 0; }这里的关键坑在于 MIFARE Classic 的块地址是从 0 开始按扇区折叠的,而 MIFARE Ultralight/NTAG 的块地址直接对应页地址,读写都是 4 字节。把 16 字节的 Classic 块大小和 4 字节的 UL 页大小搞混,是 14443A 读写出错率最高的原因。另外,MIFARE Classic 认证失败时 CLRC663 不会抛出明显的中断,只会让后续 Transceive 超时,调试时优先确认认证阶段是否成功。
5. CLRC663 多协议切换、超时参数和天线调优
5.1 协议切换时的复位与模式重配
从 14443A 切到 15693,或者反过来,不能只改命令帧格式。CLRC663 的 TxMode 和 RxMode 寄存器里写死了当前帧的调制方式、位速率和 SOF 模板,切换协议时这两个寄存器必须重新配置。我一般会先对芯片执行一次 Soft Reset,把内部状态机切回初始态,再重新写协议相关的寄存器组,最后清空 FIFO。
// 协议切换统一入口 void clrc663_switch_to_15693(void) { clrc663_write_reg(REG_COMMAND, CMD_SOFT_RESET); HAL_Delay(5); // 配置 15693 需要的 TxMode/RxMode clrc663_write_reg(REG_TX_MODE, 0xAB); // 15693 高速率 clrc663_write_reg(REG_RX_MODE, 0xAB); // 接收端同步适配 clrc663_write_reg(REG_TX_CONTROL, 0x05); // 打开天线驱动 clrc663_write_reg(REG_COMMAND, CMD_TRANSCEIVE); }tx_mode和rx_mode的具体取值取决于帧格式,不要把 14443A 的值沿用过来。Soft Reset 会花掉几毫秒,但对多协议切换场景来说,比后续丢帧排查要省时间。
5.2 超时与位速率参数对照
不同协议和不同卡片对响应时间的容忍度差异很大。ATS 里的 FWI 决定事后等待时间,15693 自己也有固定等待参数。CLRC663 有专门的定时器寄存器,可以设置自动超时,避免 MCU 死等。
| 协议 | 典型位速率 | 建议超时 | 对应寄存器 |
|---|---|---|---|
| 14443A | 106/212/424 kbps | FWT + 20% 余量 | TReload 定时器 |
| 15693 低速 | 26.48 kbps | 30 ms | 同上 |
| 15693 高速 | 53.97 kbps | 20 ms | 同上 |
实际项目里,我倾向于把超时做成可配置参数,ATS 解析出来以后动态更新定时器初值。FWI=8 的卡把超时从 5ms 改到 6ms,15693 标签从 20ms 改到 30ms,这样既不拖慢轮询,也不会在低温或天线欠谐时丢卡。注意 CLRC663 的定时器计数基于内部 13.56MHz 时钟分频,配置时要算好分频系数。
5.3 一块板子上验证 ATS 和多协议读写的完整流程
拿一块 CLRC663 核心板做验证时,建议按下面三步走。先读 ChipID 确认 SPI 链路,再切到 15693 做一次 Inventory 拿到 UID,最后切回 14443A 激活卡片读取 ATS。三步都跑通,说明硬件收发和协议切换都正常,可以开始业务逻辑开发。
# 伪代码顺序,用于验证协议切换 # 1. 读 ChipID,确认 SPI 正常 chip_id = clrc663_read_reg(0x05) # 应为 0x07 # 2. 切到 15693,做 Inventory clrc663_switch_to_15693() uid = nfc15693_inventory() # 获取 8 字节 UID nfc15693_read_block(uid, 0, buf) # 读第 0 块 # 3. 切回 14443A,读 ATS clrc663_switch_to_14443a() clrc663_read_ats(ats_buf, &ats_len) # 解析 ATS验证时最容易忽略的是天线匹配。CLRC663 的发射功率取决于天线谐振是否调到 13.56MHz,用示波器探头靠近天线线圈看波形只能粗略判断。建议直接用一张已知良好的 14443A 卡片做读 ATS 测试,如果 ATS 经常超时但卡片静置时又能偶尔读取成功,优先检查天线的 Q 值和匹配电容,而不是改软件超时。读写块操作和 ATS 读取用到的底层链路一致,链路稳定后所有协议都能正常跑通。
本文还有配套的精品资源,点击获取