简介:面向嵌入式开发者和物联网工程师,这份基于NXP CLRC663芯片的双协议NFC读写器工程源码,覆盖ISO14443A(门禁、交通卡)与ISO15693(电子标签、库存管理)卡片的UID读取、数据块读写及ATS解析等关键流程,适合具备一定单片机基础、希望快速搭建非接触式读卡方案的开发者。
压缩包共896个文件,以H/C源文件为主(238个.h、161个.c),并配有o/crf编译中间文件、Keil MDK工程配置(.uvprojx)及PDF/readme说明文档,整体约28.72MB,目录结构清晰,便于按模块检索。
已有782人学习下载,工程内整合了STM32F1xx HAL库驱动与CLRC663底层命令实现,并包含ATS解析、数据块读写等例程,同时给出CLRC63302读取ATS的参考实现,可帮助开发者理解非接触式卡片的通信时序与认证流程;源码目录结构清晰,配合PDF说明和readme,可快速定位驱动层与业务层代码,减少重复开发,便于直接移植到实际项目中。
1. 一个读写包背后的三件事:1443A 块、15693 块、ATS 解析
CLRC663 这类多协议 NFC 前端芯片,真正的工作量不在协议文档,而在把「SPI 寄存器访问 + FIFO 收发 + 中断等待」这套公共骨架搭对,再往骨架上挂 14443A 读写块、15693 读写和 ATS 读取。标题把 1443A、15693、ATS 放进同一个压缩包,恰好对应读卡器最常见的三件事:ISO 14443A 的 MIFARE 块读写、ISO 15693 的 ICODE 远距离读卡、ISO 14443-4 的 ATS 响应解析。对做过 RC522 的工程师来说,CLRC663 最大的变化是寄存器更多、命令更完整,15693 甚至直接给了硬件命令;对第一次接触的工程师,最该先啃的是它 64 字节 FIFO 和中断标志位。下面按 STM32 HAL 库 SPI 接口展开,寄存器地址以 CLRC663 数据手册的寄存器表为准,代码可以直接平移到其他 MCU。
2. CLRC663 读写框架:SPI 寄存器、FIFO 与 Transceive 的三层骨架
2.1 CLRC663 寄存器表里的关键地址与主机接口选择
CLRC663 支持 SPI、I2C、UART 三种主机接口,封装上通过引脚电平选择。绝大多数嵌入式项目选 SPI,速度最高、时序最简单,读卡流程和 MFRC522/PN532 的习惯接近。CLRC663 的 SPI 从机接口兼容模式 0 和模式 3,代码里选 Mode 0 即可稳定工作;如果发现首字节漂移,切成 Mode 3 交叉验证。SPI 帧格式是:CS 拉低后先发一个地址字节,最高位表示方向,写操作为 1、读操作为 0,后 7 位是寄存器地址,之后每个时钟周期传输一个数据字节。
CLRC663 寄存器空间按功能分组,日常读写用不到全部,先记住下面这几个就够把任何协议跑通。
| 寄存器 | 地址 | 作用 |
|---|---|---|
| CommandReg | 0x01 | 写命令字,Transceive 是 0x20 |
| FIFODataReg | 0x02 | 数据入口,连续读写即 FIFO 突发 |
| FIFOLevelReg | 0x0A | 当前 FIFO 内字节数,读响应长度靠它 |
| PrimaryIRQReg | 0x0C | 中断请求位,bit0 TxIRq、bit1 RxIRq、bit2 ModeIRq |
| PrimaryIRQMskReg | 0x0E | 中断使能掩码,写 1 使能 |
| ControlReg | 0x10 | 芯片运行控制,含软复位 |
| RxControlReg | 0x13 | 接收控制,和帧格式、解码器相关 |
| TxControlReg | 0x16 | 发送控制,含 CRC 使能位 |
| TimerControlReg | 0x1E | 片内 12 位定时器控制 |
提示:不同修订版手册对个别地址有差异,本文地址来自常见参考驱动,做产品前务必和你手头数据手册第 6 章寄存器表核对一遍。
2.2 用 STM32 HAL 库写 RegRead / RegWrite 与 FIFO 突发
和 W25Q64 这类 SPI Flash 不一样,CLRC663 的 SPI 写不是把数据写进存储介质,而是写进 FIFO 触发一次射频交互,所以寄存器访问函数要写得足够薄,别在驱动层堆业务。
#define CLRC663_REG_COMMAND 0x01 #define CLRC663_REG_FIFO_DATA 0x02 #define CLRC663_REG_FIFO_LEVEL 0x0A #define CLRC663_REG_IRQ0 0x0C #define CLRC663_REG_IRQ0_MASK 0x0E static void clrc663_cs(GPIO_PinState level) { HAL_GPIO_WritePin(NFC_CS_PORT, NFC_CS_PIN, level); } static uint8_t clrc663_read_reg(uint8_t reg) { uint8_t tx[2] = { reg & 0x7F, 0x00 }; /* 最高位 0 表示读 */ uint8_t rx[2] = {0, 0}; clrc663_cs(GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10); clrc663_cs(GPIO_PIN_SET); return rx[1]; /* 首字节返回的是无效值 */ } static void clrc663_write_reg(uint8_t reg, uint8_t val) { uint8_t tx[2] = { (reg & 0x7F) | 0x80, val }; /* 最高位 1 表示写 */ clrc663_cs(GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, tx, 2, 10); clrc663_cs(GPIO_PIN_SET); }地址字节自带 R/W 位,所以读写分成两个函数而不是合并;HAL_SPI_TransmitReceive 必须用双字节,因为芯片在发地址的同一拍就开始回数据,第一个字节时钟内返回的内容无效,第二字节才是寄存器值。CS 每轮必须重新拉高再拉低,否则芯片会认为你在连续地址访问。FIFO 突发也一样,地址字节后直接跟数据,不需要每字节都发一次地址:
static void clrc663_write_fifo(const uint8_t *data, uint16_t len) { uint8_t head = (CLRC663_REG_FIFO_DATA & 0x7F) | 0x80; clrc663_cs(GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &head, 1, 10); for (uint16_t i = 0; i < len; i++) HAL_SPI_Transmit(&hspi1, (uint8_t *)&data[i], 1, 10); clrc663_cs(GPIO_PIN_SET); }生产代码里一般把负载拼进一个数组一次性发走,减少片选翻转和 SPI 中断次数;这里用循环是想把「地址字节 + 数据负载」的边界讲清楚。CLRC663 的 FIFO 只有 64 字节,写入前先看 FIFOLevelReg 还有多少空间,超出部分会被直接丢弃,这是块写失败最常见的原因之一。
2.3 Transceive 最小实现:写 FIFO、下发 0x20、等 ModeIRq
所有协议交互最终收敛到同一个动作:把要发的字节写进 FIFO,写 CommandReg = 0x20 启动 Transceive,等中断,然后按 FIFOLevel 把 FIFO 读空。和 RC522 不一样,CLRC663 的 Transceive 在下发命令后就自动开始发送,不需要额外置位 StartSend 位。
static int clrc663_wait_irq(uint8_t want, uint32_t timeout_ms) { uint32_t t0 = HAL_GetTick(); do { uint8_t irq = clrc663_read_reg(CLRC663_REG_IRQ0); if ((irq & want) == want) return (int)irq; } while (HAL_GetTick() - t0 < timeout_ms); return -1; } int clrc663_transceive(const uint8_t *tx, uint16_t tx_len, uint8_t *rx, uint16_t rx_cap, uint16_t *rx_len) { clrc663_write_reg(CLRC663_REG_COMMAND, 0x01); /* Idle,停掉上一个空中命令 */ clrc663_write_reg(CLRC663_REG_FIFO_LEVEL, 0x00); /* 复位 FIFO 指针 */ clrc663_write_reg(CLRC663_REG_IRQ0_MASK, 0x07); /* 使能 Tx/Rx/Mode 三个中断 */ clrc663_write_fifo(tx, tx_len); clrc663_write_reg(CLRC663_REG_COMMAND, 0x20); /* Transceive */ if (clrc663_wait_irq(0x04, 100) < 0) /* ModeIRq:命令走完,含超时 */ return -1; uint8_t irq = clrc663_read_reg(CLRC663_REG_IRQ0); if (!(irq & 0x02)) /* 没有 RxIRq 说明卡片没回 */ return -2; uint8_t level = clrc663_read_reg(CLRC663_REG_FIFO_LEVEL); if (level > rx_cap) level = rx_cap; for (uint8_t i = 0; i < level; i++) rx[i] = clrc663_read_reg(CLRC663_REG_FIFO_DATA); *rx_len = level; return 0; }中断判断是这套代码的要点:ModeIRq 表示整条命令结束,RxIRq 表示 FIFO 里进了数据。很多实现只等 RxIRq,遇到 15693 大卡片响应慢时会误判超时;反过来只等 ModeIRq,不读 FIFOLevel 又会把上一条命令的残留数据当成新响应。参数上,超时 100ms 对 14443A 和 15693 都够用,如果做低功耗轮询可以拆成 10ms 多轮。
注意:这个函数只做一次「一问一答」。14443A 的 REQA、防碰撞、SELECT 每一步都是独立交互,不能把整串塞进一次 Transceive。
3. ISO 14443A(1443A)读写块:选卡、认证、块命令序列
3.1 14443A 选卡三步:REQA、防碰撞、SELECT 的命令字节
写块之前必须先让卡片进入 ACTIVE 态。MIFARE 1K 这类 Type A 卡分三步:REQA 让卡片回 ATQA,防碰撞拿到完整 UID,SELECT 让卡片进入可寻址状态。CLRC663 上这三步全部走 Transceive,SOF/EOF 由芯片自动加,我们要控制的是命令字节和 CRC 开关。
| 步骤 | 命令字节 | 是否需要 CRC_A | 卡片响应 |
|---|---|---|---|
| REQA | 0x26 | 否,短帧 | ATQA 2 字节 |
| 防碰撞(级联 1) | 0x93 0x20 | 否 | UID 前 4 字节 + BCC |
| SELECT(级联 1) | 0x93 0x70 + UID4 + BCC | 是 | SAK 1 字节 |
| 防碰撞(级联 2) | 0x95 0x20 | 否 | 7 字节 UID 中间 4 字节 |
| SELECT(级联 2) | 0x95 0x70 + UID4 + BCC | 是 | SAK 1 字节 |
int clrc663_14443a_select(uint8_t *uid_out, uint8_t *sak_out) { uint8_t buf[16], rx[16]; uint16_t n; buf[0] = 0x26; /* REQA */ if (clrc663_transceive(buf, 1, rx, 4, &n) < 0 || n < 2) return -1; buf[0] = 0x93; buf[1] = 0x20; /* NVB=0x20:只发 SEL 和 NVB */ if (clrc663_transceive(buf, 2, rx, 8, &n) < 0 || n < 5) return -1; uint8_t bcc = rx[0] ^ rx[1] ^ rx[2] ^ rx[3]; if (bcc != rx[4]) /* 单标签场景,BCC 必须自洽 */ return -1; memcpy(uid_out, rx, 4); buf[0] = 0x93; buf[1] = 0x70; /* SELECT,CRC 交给芯片 */ memcpy(buf + 2, uid_out, 4); buf[6] = uid_out[0] ^ uid_out[1] ^ uid_out[2] ^ uid_out[3]; if (clrc663_transceive(buf, 7, rx, 4, &n) < 0 || n < 1) return -1; *sak_out = rx[0]; if (*sak_out & 0x04) { /* 级联位:7 字节 UID */ buf[0] = 0x95; buf[1] = 0x20; if (clrc663_transceive(buf, 2, rx, 8, &n) < 0 || n < 5) return -1; memcpy(uid_out + 4, rx, 3); /* 第二段 UID */ buf[0] = 0x95; buf[1] = 0x70; memcpy(buf + 2, uid_out + 4, 4); /* UID 后 4 字节含 BCC */ if (clrc663_transceive(buf, 7, rx, 4, &n) < 0 || n < 1) return -1; *sak_out = rx[0]; return 7; } return 4; }防碰撞的返回值是 4 字节 UID 加 1 字节 BCC,BCC 等于前 4 字节按位异或,这是校验选卡是否出错最便宜的手段。这里展示的是单标签场景;多标签同时入场时,卡片响应会带碰撞位,正确做法是从 NVB=0x20 开始逐步增加有效位重试,直到只剩一张卡响应,量产项目里这段逻辑要补齐。SELECT 之后,SAK 的 bit5(0x20)表示卡片支持 ISO 14443-4,这类卡才需要后续的 RATS 流程。
3.2 MIFARE 认证与块读写:CLRC663 的三个硬件命令
进入 ACTIVE 后,MIFARE Classic 的块读写分两步:先认证再操作。认证涉及 Crypto1 流密码,裸 Transceive 没法做,CLRC663 专门给了三个硬件命令:MFAuthent(0x21)、MFRead(0x22)、MFWrite(0x23)。认证命令的 FIFO 内容在 NXP 参考驱动里是块号、6 字节密钥、4 字节 UID,不同参考代码对顺序有差异,以你数据手册 7.x 节为准。
int clrc663_mf_auth(uint8_t block, uint8_t key_type, const uint8_t key[6], const uint8_t uid[4]) { uint8_t fifo[12]; fifo[0] = block; /* 目标块 */ memcpy(fifo + 1, key, 6); /* 密钥 */ memcpy(fifo + 7, uid, 4); /* UID 参与密钥流生成 */ clrc663_write_fifo(fifo, 11); clrc663_write_reg(CLRC663_REG_IRQ0_MASK, 0x07); clrc663_write_reg(CLRC663_REG_COMMAND, 0x21); /* MFAuthent */ if (clrc663_wait_irq(0x06, 100) < 0) return -1; return clrc663_read_reg(CLRC663_REG_FIFO_LEVEL) == 0 ? 0 : -1; }认证成功后整条射频链路被 Crypto1 门控,之后不能用 Transceive 手动发包,必须继续走硬件命令。读块只要把块号写进 FIFO,响应是 16 字节明文数据:
int clrc663_mf_read_block(uint8_t block, uint8_t out[16]) { clrc663_write_reg(CLRC663_REG_IRQ0_MASK, 0x07); clrc663_write_fifo(&block, 1); clrc663_write_reg(CLRC663_REG_COMMAND, 0x22); /* MFRead */ if (clrc663_wait_irq(0x06, 100) < 0) return -1; uint8_t level = clrc663_read_reg(CLRC663_REG_FIFO_LEVEL); if (level < 16) return -1; for (uint8_t i = 0; i < 16; i++) out[i] = clrc663_read_reg(CLRC663_REG_FIFO_DATA); return 0; }写块的 FIFO 是块号加 16 字节数据共 17 字节,发 MFWrite 命令后卡片会回 4 字节确认,首字节 0x0A 表示 ACK:
int clrc663_mf_write_block(uint8_t block, const uint8_t data[16]) { uint8_t fifo[17], rx[4]; uint16_t n; fifo[0] = block; /* 先块号 */ memcpy(fifo + 1, data, 16); /* 再整块数据 */ clrc663_write_reg(CLRC663_REG_IRQ0_MASK, 0x07); clrc663_write_fifo(fifo, 17); clrc663_write_reg(CLRC663_REG_COMMAND, 0x23); /* MFWrite */ if (clrc663_wait_irq(0x06, 100) < 0) return -1; uint8_t level = clrc663_read_reg(CLRC663_REG_FIFO_LEVEL); if (level >= 1) rx[0] = clrc663_read_reg(CLRC663_REG_FIFO_DATA); return rx[0] == 0x0A ? 0 : -1; }卡片返回的 16 字节数据是自带 EEPROM 掉电不丢失存储,写入后有实际擦写周期,所以写块命令结束后要额外等 3~5ms 再发下一条命令,不然卡片在擦写期间收不到任何请求,读卡器会看到随机超时。
3.3 块读写必调的 3 个参数:发送时序、超时、FIFO 水位
第一是发送后的接收时序。CLRC663 的 TxControlReg 和 ProtocolConfigReg 里有发送结束到开启接收的间隔参数,14443A 的 MIFARE 卡片响应很快,间隔太长会吃掉卡片响应头,表现是偶尔能读、换张卡就超时。第二是片内定时器,CLRC663 的定时器是 12 位可重载,做主超时防呆,主控侧再用 HAL_GetTick 兜底,双超时是量产固件的标准做法。第三是 FIFO 水位,MFRead 失败后先读 FIFOLevelReg,如果值在 12~15 之间说明卡片响应被截断,优先怀疑天线匹配而不是命令格式;如果值是 0,再去查上一步认证状态。块号范围也要盯住,MIFARE 1K 只有 0~63,每个扇区的尾部块(3、7、11…)写错会把密钥区覆盖,卡直接报废。
4. ISO 15693 块读写与 ATS 读取:硬件命令和协议切换
4.1 15693 Inventory 与块读写的 FIFO 组织
ISO 15693 的帧格式和 14443A 完全不同,但 CLRC663 把最常用的几条做成了硬件命令,帧头、命令码、CRC 都自动拼,我们只需要按固定顺序填 FIFO。
| 命令码 | 功能 | FIFO 写入内容 | FIFO 读回 |
|---|---|---|---|
| 0x30 | Inventory 寻卡 | 空 | 每张卡 8 字节 UID,部分固件前多 1 字节 DSFID |
| 0x31 | Select 选中 | UID 8 字节 | 无 |
| 0x3B | Read Single Block | flags + UID8 + 块号 | 4 字节块数据 |
| 0x33 | Write Single Block | flags + UID8 + 块号 + 数据4 | 无有效数据 |
int clrc663_15693_inventory(uint8_t uid[8]) { uint8_t rx[32]; clrc663_write_reg(CLRC663_REG_CRC_CONTROL, 0x03); /* 切到 15693 的 16 位 CRC */ clrc663_write_reg(CLRC663_REG_IRQ0_MASK, 0x07); clrc663_write_reg(CLRC663_REG_COMMAND, 0x30); /* Inventory */ if (clrc663_wait_irq(0x06, 100) < 0) return -1; uint8_t n = clrc663_read_reg(CLRC663_REG_FIFO_LEVEL); for (uint8_t i = 0; i < n && i < 32; i++) rx[i] = clrc663_read_reg(CLRC663_REG_FIFO_DATA); if (n < 8) return -1; memcpy(uid, rx + n - 8, 8); /* 无论有没有 DSFID,取最后 8 字节当 UID */ return 0; }取最后 8 字节当 UID 是兼容不同固件版本的小技巧,Inventory 响应在标准里是响应标志、DSFID、UID 三部分,CLRC663 部分固件会把 DSFID 一起塞进 FIFO,固定偏移反而容易错位。ICODE 这类标签块大小是 4 字节,写块 FIFO 组织如下:
int clrc663_15693_write_block(const uint8_t uid[8], uint8_t block, const uint8_t data[4]) { uint8_t fifo[14]; fifo[0] = 0x22; /* 单副载波+高速率(0x02) + 寻址模式(0x20) */ memcpy(fifo + 1, uid, 8); fifo[9] = block; memcpy(fifo + 10, data, 4); clrc663_write_reg(CLRC663_REG_IRQ0_MASK, 0x07); clrc663_write_fifo(fifo, 14); clrc663_write_reg(CLRC663_REG_COMMAND, 0x33); /* Write Single Block */ if (clrc663_wait_irq(0x04, 100) < 0) /* 只等 ModeIRq */ return -1; HAL_Delay(5); /* 标签 EEPROM 写入周期 */ return 0; }flags 的 0x22 拆开看:bit0、bit1 拼起来是 0x01 表示单副载波加高速率,bit5(0x20)是地址标志,置位后命令帧里才带 UID。对 ICODE SLI 这类小容量标签,读回 4 字节后要自己比对上次内容,15693 没有 MIFARE 那种自动 CRC 校验反馈,CRC 错只反映在 FIFO 数据异常上。
4.2 14443A / 15693 协议切换的寄存器顺序
一套天线环境两个协议来回切,最容易出问题的是残留状态。常见做法是切之前先发 Idle 停掉空中命令,把 FIFO 指针复位,再改 CRCControlReg 和配置帧格式的寄存器,最后延时 5ms 等模拟前端稳定。顺序反了会出现一个典型症状:14443A 刚读完块立刻切 15693 Inventory,第一轮永远超时,第二轮才正常。
void clrc663_switch_protocol(uint8_t proto) { clrc663_write_reg(CLRC663_REG_COMMAND, 0x01); /* Idle */ clrc663_write_reg(CLRC663_REG_FIFO_LEVEL, 0x00);/* 清 FIFO */ if (proto == 0x03) { /* 15693 */ clrc663_write_reg(CLRC663_REG_CRC_CONTROL, 0x03); } else { /* 14443A */ clrc663_write_reg(CLRC663_REG_CRC_CONTROL, 0x01); /* CRC_A */ } HAL_Delay(5); /* 让 Rx/Tx 通路切换到目标帧格式 */ }不要把这个函数理解成万能复位,它不重配天线驱动电流。15693 和 14443A 对 antenna driver 的输出级需求不一样,批量项目里换协议后最好把 RFControlReg 的驱动配置一起重写,否则两个协议各自都能跑通,来回切换后灵敏度会下降几个厘米。
4.3 读取并解析 ATS:RATS 参数、TL/T0、CLRC63302 兼容
ATS 是 ISO 14443-4 的 Answer To Select,和 PCIe ATS/ATC(Address Translation Service/Caching)不是一个东西,搜资料时关键词带 ISO14443 才不会查偏。只有 SELECT 回来的 SAK 带 0x20 位(支持 14443-4)的卡才需要发 RATS,比如 DESFire 和各类 CPU 卡。RATS 命令帧是 0xE0 加一个参数字节,参数高 4 位是 CID,低 4 位是 FSDI,0x50 表示 CID=0、FSD=64 字节,兼容性最好。
int clrc663_read_ats(uint8_t *ats, uint8_t *ats_len) { uint8_t ratscmd[2] = { 0xE0, 0x50 }; /* RATS */ uint8_t rx[32]; uint16_t n = 0; if (clrc663_transceive(ratscmd, 2, rx, 32, &n) < 0 || n < 2) return -1; memcpy(ats, rx, n); *ats_len = n; return 0; }ATS 结构固定:TL 是总长度且包含自身,T0 的高位表示 TA1/TB1/TC1 是否存在,低 4 位是 FSCI。解析时先按 T0 跳过接口字节,剩下的才是历史字节:
void ats_parse(const uint8_t *ats, uint8_t len, uint8_t *fsci, uint8_t *hist, uint8_t *hist_len) { uint8_t t0 = ats[1]; uint8_t pos = 2; *hist_len = 0; *fsci = 0; if (len < 2) return; if (t0 & 0x40) pos++; /* TA1 */ if (t0 & 0x20) pos++; /* TB1 */ if (t0 & 0x10) pos++; /* TC1 */ if (pos > len) return; *fsci = t0 & 0x0F; *hist_len = len - pos; if (*hist_len > 15) *hist_len = 15; /* 协议规定历史字节最多 15 字节 */ memcpy(hist, ats + pos, *hist_len); }FSCI 决定后续数据交换的帧长上限,对照关系是 0→16、1→24、2→32、3→40、4→48、5→64、6→96、7→128、8→256 字节。如果 FSCI=0,后续走 APDU 前必须做 PPS 协商放大帧长,否则超过 16 字节的命令会被卡拒收。标题里的 CLRC63302 属于市场上常见的 CLRC663 兼容型号,寄存器表和命令码基本照搬,我按上面流程读 ATS 没遇到差异;但兼容芯片的中断实现可能简化过,HighAlert/LowAlert 这类水位中断位可能恒为 0,别拿它们做 FIFO 满判断,FIFO 深度也按 60 字节留余量更稳。
5. 用寄存器快照定位 ATS 超时与块读写失败
5.1 失败后先 dump 这三个寄存器
排查协议问题最忌讳反复改参数试,先打印现场。IRQ0、FIFOLevel、TxControlReg 三个地址的值能把大多数失败归到三类:没发出去、发了没回、回了但被截断。
void clrc663_dump_state(void) { printf("IRQ0=%02X FIFOL=%02X TXCTL=%02X CRCCTL=%02X\r\n", clrc663_read_reg(0x0C), clrc663_read_reg(0x0A), clrc663_read_reg(0x16), clrc663_read_reg(0x1F)); }IRQ0 全零说明命令没执行,先查 SPI 时序和 CS 极性;IRQ0 有 ModeIRq 但 FIFOLevel 为 0,是卡片没响应,去查天线和卡片供电;FIFOLevel 在 12~15 之间,是响应头被接收时序吞掉,去调发送结束到接收开启的间隔。块写失败时多打一行块号,MIFARE Classic 的尾块写保护会静默失败,不回 NACK 也不报错。
5.2 ATS 解析的三个边界 case
ATS 解析本身不难,坑都在边界。第一,TL 和实际收到长度不一致,以实际收到短者为准;第二,T0 声称有 TA1/TB1/TC1 但总长度不够,直接截断,别越界读历史字节;第三,历史字节超过 15 字节是协议违规,按 15 截断后继续流程。另外很多轻量卡根本不回 ATS,SAK 里 0x20 位没置位就压根别发 RATS,发了只会空等超时。把这三个判断写进解析函数,比在调用侧堆 if 干净得多。
5.3 连续读 100 次的冒烟脚本
验证读写稳定性用最简单的方式,同一块连续读 100 次,前 50 次卡在 REQA 阶段重试,后 50 次不允许重试,两次结果必须逐字节一致。小于 16 字节的响应一律算失败,别用 memcpy 硬拷。
int clrc663_burn_test(uint8_t block) { uint8_t a[16], b[16]; for (int i = 0; i < 100; i++) { if (clrc663_mf_read_block(block, a) < 0) return -1; if (clrc663_mf_read_block(block, b) < 0) return -1; if (memcmp(a, b, 16) != 0) return -2; /* 两次读不一致,天线或供电有问题 */ } return 0; }同一块连续读两次做比对,能把天线驻波、卡片供电跌落这类偶发问题变成可复现的失败。15693 读写同样做一轮,但块大小改成 4 字节,写完后立即读回比对,再断电 5 秒上电读一次,确认数据真落进了标签的 EEPROM。这套冒烟跑完,协议栈基本可以交付;剩下偶发失败,就回到 5.1 的寄存器快照,打印 SAK、TL、FIFOLevel 三个值再动手改配置,比反复试 RATS 参数快得多。
本文还有配套的精品资源,点击获取