
简介面向嵌入式开发者和物联网工程师这份基于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 寄存器空间按功能分组日常读写用不到全部先记住下面这几个就够把任何协议跑通。寄存器地址作用CommandReg0x01写命令字Transceive 是 0x20FIFODataReg0x02数据入口连续读写即 FIFO 突发FIFOLevelReg0x0A当前 FIFO 内字节数读响应长度靠它PrimaryIRQReg0x0C中断请求位bit0 TxIRq、bit1 RxIRq、bit2 ModeIRqPrimaryIRQMskReg0x0E中断使能掩码写 1 使能ControlReg0x10芯片运行控制含软复位RxControlReg0x13接收控制和帧格式、解码器相关TxControlReg0x16发送控制含 CRC 使能位TimerControlReg0x1E片内 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 14443A1443A读写块选卡、认证、块命令序列3.1 14443A 选卡三步REQA、防碰撞、SELECT 的命令字节写块之前必须先让卡片进入 ACTIVE 态。MIFARE 1K 这类 Type A 卡分三步REQA 让卡片回 ATQA防碰撞拿到完整 UIDSELECT 让卡片进入可寻址状态。CLRC663 上这三步全部走 TransceiveSOF/EOF 由芯片自动加我们要控制的是命令字节和 CRC 开关。步骤命令字节是否需要 CRC_A卡片响应REQA0x26否短帧ATQA 2 字节防碰撞级联 10x93 0x20否UID 前 4 字节 BCCSELECT级联 10x93 0x70 UID4 BCC是SAK 1 字节防碰撞级联 20x95 0x20否7 字节 UID 中间 4 字节SELECT级联 20x95 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; /* NVB0x20只发 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; /* SELECTCRC 交给芯片 */ 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 字节 BCCBCC 等于前 4 字节按位异或这是校验选卡是否出错最便宜的手段。这里展示的是单标签场景多标签同时入场时卡片响应会带碰撞位正确做法是从 NVB0x20 开始逐步增加有效位重试直到只剩一张卡响应量产项目里这段逻辑要补齐。SELECT 之后SAK 的 bit50x20表示卡片支持 ISO 14443-4这类卡才需要后续的 RATS 流程。3.2 MIFARE 认证与块读写CLRC663 的三个硬件命令进入 ACTIVE 后MIFARE Classic 的块读写分两步先认证再操作。认证涉及 Crypto1 流密码裸 Transceive 没法做CLRC663 专门给了三个硬件命令MFAuthent0x21、MFRead0x22、MFWrite0x23。认证命令的 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 表示 ACKint 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 掉电不丢失存储写入后有实际擦写周期所以写块命令结束后要额外等 35ms 再发下一条命令不然卡片在擦写期间收不到任何请求读卡器会看到随机超时。3.3 块读写必调的 3 个参数发送时序、超时、FIFO 水位第一是发送后的接收时序。CLRC663 的 TxControlReg 和 ProtocolConfigReg 里有发送结束到开启接收的间隔参数14443A 的 MIFARE 卡片响应很快间隔太长会吃掉卡片响应头表现是偶尔能读、换张卡就超时。第二是片内定时器CLRC663 的定时器是 12 位可重载做主超时防呆主控侧再用 HAL_GetTick 兜底双超时是量产固件的标准做法。第三是 FIFO 水位MFRead 失败后先读 FIFOLevelReg如果值在 1215 之间说明卡片响应被截断优先怀疑天线匹配而不是命令格式如果值是 0再去查上一步认证状态。块号范围也要盯住MIFARE 1K 只有 063每个扇区的尾部块3、7、11…写错会把密钥区覆盖卡直接报废。4. ISO 15693 块读写与 ATS 读取硬件命令和协议切换4.1 15693 Inventory 与块读写的 FIFO 组织ISO 15693 的帧格式和 14443A 完全不同但 CLRC663 把最常用的几条做成了硬件命令帧头、命令码、CRC 都自动拼我们只需要按固定顺序填 FIFO。命令码功能FIFO 写入内容FIFO 读回0x30Inventory 寻卡空每张卡 8 字节 UID部分固件前多 1 字节 DSFID0x31Select 选中UID 8 字节无0x3BRead Single Blockflags UID8 块号4 字节块数据0x33Write Single Blockflags 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 表示单副载波加高速率bit50x20是地址标志置位后命令帧里才带 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 读取并解析 ATSRATS 参数、TL/T0、CLRC63302 兼容ATS 是 ISO 14443-4 的 Answer To Select和 PCIe ATS/ATCAddress Translation Service/Caching不是一个东西搜资料时关键词带 ISO14443 才不会查偏。只有 SELECT 回来的 SAK 带 0x20 位支持 14443-4的卡才需要发 RATS比如 DESFire 和各类 CPU 卡。RATS 命令帧是 0xE0 加一个参数字节参数高 4 位是 CID低 4 位是 FSDI0x50 表示 CID0、FSD64 字节兼容性最好。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 字节。如果 FSCI0后续走 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 在 1215 之间是响应头被接收时序吞掉去调发送结束到接收开启的间隔。块写失败时多打一行块号MIFARE Classic 的尾块写保护会静默失败不回 NACK 也不报错。5.2 ATS 解析的三个边界 caseATS 解析本身不难坑都在边界。第一TL 和实际收到长度不一致以实际收到短者为准第二T0 声称有 TA1/TB1/TC1 但总长度不够直接截断别越界读历史字节第三历史字节超过 15 字节是协议违规按 15 截断后继续流程。另外很多轻量卡根本不回 ATSSAK 里 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 参数快得多。本文还有配套的精品资源点击获取