前几天熟人做闸机项目,要求读三种卡:小区门禁用的14443A、校园卡遗留的14443B、还有图书馆里那批15693标签。他一开始弄的是RC522,结果B卡和15693完全没反应,回头问我怎么办。我让他换成CLRC663国产NFC模块,配合STM32F103C8T6最小系统板重写读卡逻辑,两天就把样机调通了。如果你也在做类似的多协议读卡器,这篇文章就是给你准备的。
我会把选型、接线、初始化、三种协议读UID的流程、工程骨架,还有第一次调板时踩过的坑全部过一遍。适合手里已经有一块STM32F103C8T6最小系统板、想低成本验证多协议NFC方案的工程师或者学生,不需要精通射频前端,跟着接线和调参就能跑起来。
1. 为什么RC522不够用:CLRC663在协议支持上是断层级的
1.1 RC522只能解决80%的常规卡
RC522这颗芯片在淘宝上到处都是,配套资料也很全,很多人一上来就会拿它做门禁。但RC522只支持ISO 14443A,也就是M1卡、NTAG系列这类A卡。你拿它刷14443B的校园卡、15693的图书标签,根本不会有响应。
如果你只是做桌面读卡器,读自家M1卡,RC522完全够用。但一旦进入“一卡通兼容”场景,比如小区门禁、校园一卡通、图书馆还书设备,要同时面对老式A卡、B卡、ISO15693标签,RC522再怎么改代码也救不回来。CLRC663这类芯片才是干这个活的。
1.2 CLRC663模块到底强在哪
CLRC663是NXP一颗13.56MHz前端芯片,相比RC522最核心的升级是多协议支持:ISO 14443A、ISO 14443B、FeliCa(ISO 18092),以及ISO 15693。市场上很多国产NFC读卡模块就是基于CLRC663做的,板上集成了天线和匹配电路,买回来直接飞线接MCU就能用,比拿裸片自己画射频板省事太多。
简单对比一下:
| 项目 | RC522类模块 | CLRC663类模块 |
|---|---|---|
| 支持协议 | ISO14443A、Mifare | ISO14443A/B、FeliCa、ISO15693 |
| 典型读卡距离 | 3-5cm | 5-10cm,取决于天线和调谐 |
| 常用接口 | SPI | SPI/I2C/UART |
| 串口日志/断点调试难度 | 低 | 中,寄存器偏多 |
| 适合场景 | 门禁、桌面M1读写 | 多协议一卡通、预付费终端、图书馆设备 |
| 模块价位 | 很低 | 中高,但比RC522强大得多 |
选型上我建议:如果你是毕业设计或者小批量产品,直接买带天线的CLRC663成品模块,不要买裸芯片自己画天线。原因很简单,CLRC663的天线匹配比RC522敏感,线圈尺寸、调谐电容、地平面处理都会直接影响读卡距离。成品模块已经做好了这些,你只需要关心数字侧。
2. 连线前的三个决定:电平、电源和SPI引脚分配
2.1 引脚对照表与CubeMX怎么填
STM32F103C8T6最小系统板上最容易用的就是SPI1,对应PA5、PA6、PA7。建议再加一个软件CS脚PA4,以及复位脚PB0、中断脚PB1。CLRC663模块上的引脚命名各家略有不同,常见的有MOSI、MISO、SCK、CS、RST、IRQ,也有标SI、SO、NSS的,接线逻辑一样。
| CLRC663模块引脚 | 接到STM32F103C8T6哪个脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 模块供电,千万不要接5V |
| GND | GND | 共地,必须接 |
| SCK | PA5 | SPI1时钟 |
| MOSI/SI | PA7 | SPI1主机输出 |
| MISO/SO | PA6 | SPI1主机输入 |
| CS/NSS | PA4 | 软件片选,GPIO控制 |
| RST/NRSTPD | PB0 | 复位控制,拉低复位,拉高运行 |
| IRQ | PB1 | 中断输出,可用下降沿触发或轮询 |
在CubeMX里,把PA5、PA6、PA7配为SPI1,PB0、PB1设为GPIO输出/输入,PA4设为GPIO输出。开启USART1,调试时把卡片UID信息打印到串口助手里,不然你对着寄存器干看也不知道芯片在工作没有。
2.2 电源和布局上的细节
很多人在这里犯的第一个错误是:直接拿STM32F103C8T6最小系统板上的3.3V引脚给CLRC663模块供电。在低负载调试时这样能跑,但CLRC663天线驱动瞬间电流不低,STM32板载LDO往往扛不住,会导致读卡距离忽远忽近,甚至复位重启。
我实际测试后的建议是:
- 单独用一片AMS1117-3.3或者低压差LDO,从USB 5V转3.3V给CLRC663模块供电,地线跟STM32共地。
- 在模块VCC和GND之间加一个100μF电解电容,再并联一个0.1μF陶瓷电容,滤波效果会好很多。
- 跳线尽量短。SPI时钟在STM32F103上能跑到十几MHz,但面包板上长跳线会带来振铃和误码。如果暂时只能面包板,把SPI分频调低到4.5MHz或更低,稳定优先。
- 天线正下方不要走其他信号线,也不要把卡直接放在金属桌面上测试。
还有一个容易忽略的点:有些CLRC663模块的RST脚默认是低电平需要拉高才能工作。如果固件初始化一直失败,先量一下RST引脚电压有没有被一个10k电阻拉低。我第一次用的模块,拿到手测RST只有0V,还以为是芯片坏了,实际上是模块出厂默认引脚悬空,我程序里也没主动拉高。
3. 让CLRC663开口说话:寄存器读写和SPI时序
3.1 SPI时序里的关键点
CLRC663的SPI很像RC522,但寄存器访问方式不太一样。进入正式操作前,先把SPI1配置成Master、8位数据、MSB先行、CPOL=0、CPHA=0,也就是常规模式0。硬件NSS关掉,用PA4软件片选。
片选时序上,每次读写寄存器都拉低PA4,发送首字节,首字节里包含了寄存器地址和读写方向。CLRC663的地址比RC522要规整,很多驱动库直接会这样构造首字节:reg << 1得到7位地址空间,最低位或最高位作为读写标志。我用过的国产模块驱动,写寄存器时首字节是地址左移一位,读寄存器时在最高位加0x80。如果你拿到的芯片资料里画法不同,以数据手册为准,但总体思路一样。
3.2 寄存器读写函数和最小驱动骨架
在STM32CubeMX生成工程之后,先写两个最基础的SPI读写函数:
void RC663_WriteReg(uint8_t reg, uint8_t val) { uint8_t addr = (reg << 1) & 0x7F; uint8_t data = val; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &addr, 1, 10); HAL_SPI_Transmit(&hspi1, &data, 1, 10); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } uint8_t RC663_ReadReg(uint8_t reg) { uint8_t addr = ((reg << 1) & 0x7F) | 0x80; uint8_t data = 0; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &addr, 1, 10); HAL_SPI_Receive(&hspi1, &data, 1, 10); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return data; }严格说这不是完整寄存器操作封装,但已经够用了。实际项目中我会再加一层连续读写,比如RC663_WriteBuffer和RC663_ReadBuffer,用于把ATQA、UID这类多字节数据一次性搬到FIFO里。
接下来是CLRC663的初始化。不同模块的天线匹配参数不一样,最好的办法是把模块卖家给的初始化数组直接焊进代码。如果卖家没给,网上搜对应芯片的rc663_init数组也可以。初始化流程一般是这样:
- RST拉低,延时100ms,再拉高,让芯片完成复位。
- 读取版本寄存器,确认SPI通路正常。如果读到非0值,说明通信没问题。
- 按照芯片手册或厂家例程,写调制解调、接收阈值、发送功率等相关寄存器。
- 清空FIFO,关掉不必要的中断。
- 打开射频发射,进入读卡状态。
不要跳过版本寄存器这一步。我每拿到一块模块,第一件事就是读版本号。如果读出来全是0xFF或者0x00,先检查接线和SPI配置,而不是一头扎进读卡调试。你连芯片都没唤醒,后面谈协议没有意义。
4. 三类卡片的UID提取逻辑:命令帧构造并没有想想中复杂
4.1 14443A的REQA加防冲突
ISO14443A是最常用的一类。拿到一张A卡,读UID的第一步是发REQA命令,CLRC663收到后会返回2字节的ATQA。然后发防冲突命令,典型的帧是:
0x93 0x20这个帧的意思是“防冲突级联级别1,NVB=0x20”。卡返回5字节数据,其中前4字节是UID,最后1字节是BCC校验。只读UID的话,把这4字节存下来就可以了。
0x93 0x70 [UID0] [UID1] [UID2] [UID3] [BCC]上面这个是选择命令,返回SAK一字节。SAK决定UID是4字节还是7字节。如果SAK表明有级联,你还要用0x95和0x97继续读第二、第三段UID。很多国产模块驱动库已经把这一步封装好,直接调用函数就能返回完整UID。
4.2 15693的Inventory命令
ISO15693的读UID流程比A卡更直观。它不需要一帧一帧地防冲突,直接发Inventory命令,一次就能返回8字节UID。最常用的命令帧是:
0x01 0x01 0x00三个字节分别是:请求标志、Inventory命令码、Mask长度。如果Mask长度为0,就是让所有在场的15693标签响应。标签返回的帧通常是:
标志字节 DSFID UID也就是10字节数据里,UID在第3到第10字节。很多15693标签支持双字节写模式,但读UID不需要关心这些,知道这个命令就能开始调板了。
CLRC663的高层驱动里往往有专门的ISO15693Inventory命令,底层寄存器帮你组帧。但调试时我习惯自己构造命令发到FIFO,这样能更直观地判断是CLRC663没发出去,还是标签没回应。
4.3 B卡和F卡的快速上手要点
ISO14443B和FeliCa在命令格式上跟A卡、15693差异很大。
B卡发REQB命令时,不能只发一个0x05。典型格式是0x05 + AFI + PARAM + CRC,其中AFI和PARAM都有具体含义。如果你用CLRC663的自动CRC功能,FIFO里可能只需要放前面的字节,但要注意不同驱动对CRC的处理方式不一样。B卡返回的ATQB里有一个4字节PUPI,很多B类卡直接拿PUPI当卡号。
FeliCa卡在CLRC663里也有独立轮询命令。FeliCa的UID不叫UID,叫IDm,长度是8字节。跟A卡那种“REQA + 防冲突 + SELECT”完全不是一个路子,别混着用。轮询命令一般以0x00开头,后面跟系统码和时隙参数。手头没有FeliCa卡的话,这段可以先跳过,等真正需要时再查FeliCa协议手册。
总结成一张表:
| 卡片类型 | 第一步命令 | 关键返回字段 | 拿哪个字段当卡号 |
|---|---|---|---|
| ISO14443A | REQA 0x26 | ATQA 2字节 | UID,通常4字节或7字节 |
| ISO14443B | REQB | ATQB 12字节左右 | PUPI,前4字节 |
| FeliCa | Polling | IDm 8字节 | IDm |
| ISO15693 | Inventory 0x01 0x01 0x00 | 标志+DSFID+UID | UID,8字节 |
5. 可用工程骨架:CubeMX配置加一段能跑起来的读卡主循环
5.1 CubeMX配置顺序
不要小看这一节,很多人就是死在配置顺序上。我推荐的顺序是:
- 选择STM32F103C8T6。
- 配置RCC时钟,用8MHz外部晶振,系统时钟拉到72MHz。
- 配置SPI1,Master,8位,模式0,分频设为16,也就是4.5MHz。等跑通了再往高调。
- 配置PA4、PB0、PB1为GPIO,PA4输出高电平,PB0输出低电平,PB1输入上拉。
- 配置USART1,115200,8-N-1。
- 生成Keil工程或STM32CubeIDE工程。
SPI分频在初期不要追求极限。我试过用9MHz SPI跑CLRC663,在面包板上能工作,但线稍长就出错。回到4.5MHz后读卡距离反而更稳定,因为误码少了。
5.2 main.c中的读卡循环
初始化之后,写一个简单的“读UID”主循环。假设你已经有了RC663_Transceive这个函数,它的作用是:把发送帧写入FIFO,启动Transceive命令,等待IRQ,然后从FIFO读回响应数据。
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); if (RC663_Init() != 0) { printf("CLRC663 init failed\r\n"); while (1); } printf("CLRC663 ready\r\n"); while (1) { uint8_t uid[16] = {0}; uint8_t uidLen = 0; // 先试A卡 if (RC663_ReadTypeA_UID(uid, &uidLen) == 0) { printf("A: "); PrintHex(uid, uidLen); } // 再试15693 else if (RC663_Read15693_UID(uid, &uidLen) == 0) { printf("15693: "); PrintHex(uid, uidLen); } HAL_Delay(300); } }中间两个读卡函数可以这样写,关键就是调RC663_Transceive:
uint8_t RC663_ReadTypeA_UID(uint8_t *uid, uint8_t *len) { uint8_t tx[2]; uint8_t rx[16]; uint8_t rxLen = 0; // REQA tx[0] = 0x26; if (RC663_Transceive(tx, 1, rx, &rxLen) != 0) return 1; if (rxLen < 2) return 1; // 防冲突 CL1 tx[0] = 0x93; tx[1] = 0x20; if (RC663_Transceive(tx, 2, rx, &rxLen) != 0) return 1; if (rxLen < 5) return 1; memcpy(uid, rx, 4); *len = 4; return 0; } uint8_t RC663_Read15693_UID(uint8_t *uid, uint8_t *len) { uint8_t tx[3] = {0x01, 0x01, 0x00}; uint8_t rx[16]; uint8_t rxLen = 0; if (RC663_Transceive(tx, 3, rx, &rxLen) != 0) return 1; if (rxLen < 10) return 1; memcpy(uid, &rx[2], 8); *len = 8; return 0; }这里有个细节:RC663_Transceive里的超时一定要做,不能傻等IRQ。没有卡片时,如果CLRC663一直没有返回响应,MCU会卡死在等待状态。我的做法是设置一个超时计数器,比如100ms内IRQ没拉低就返回错误。实际测试中,刷一次卡一般几十毫秒就完成了。
6. 调板时最容易翻车的问题:我的排查顺序和经验
6.1 从“读不到卡”开始分析
我第一次用CLRC663调板时,先后遇到三个问题,每个都很有代表性:
第一个问题是SPI读回来全是0xFF。排查了半小时,发现是MOSI和MISO接反了。模块上MOSI标的是“主设备输出从设备输入”,接到STM32的MISO脚上,等于我把一条单行道修反了。这里强烈建议:接线之前先看模块丝印,SI不是“SPI输入给STM32”的意思,它是“CLRC663从SPI总线接收数据”,也就是接MCU的MOSI。
第二个问题是IRQ一直不触发。程序里配置了PB1下降沿中断,但红外中断标志永远没清掉。后来改用轮询PB1引脚,发现也能正常工作。CLRC663的中断状态寄存器有多个,每个中断源都要单独清,你在初学阶段直接用轮询,省心非常多。
第三个问题是15693卡完全没反应,但A卡正常。最后发现不是协议问题,而是CLRC663的发射功率寄存器配置不对,天线场覆盖太弱。把发送器相关寄存器改成厂家推荐的配置之后,15693标签在三厘米外就能读到了。
6.2 关于天线和环境的3个提醒
天线周围有金属,是最容易忽视频繁出现的坑。CLRC663模块天线的感应区很小,你把模块放在铁皮外壳旁边,会看到读卡距离瞬间掉一半。产品化时,外壳开窗位置要避开天线正下方,最好用泡棉把模块垫高。
电源噪声也会让UID偶尔跳变。表面上看芯片复位正常,SPI也能通信,但卡的响应帧里某一位被干扰,导致UID末尾一个字节时不时变化。把模块供电改成独立LDO之后,问题就消失了。如果你实验时发现UID不稳定,先别怀疑算法,去查供电。
还有一点,CLRC663对天线参数比RC522敏感得多。你买的裸模块,厂家已经把匹配调好了,但如果你后来手痒去改天线线圈,哪怕只是多加一根跳线,匹配都会变。调试读卡距离的时候,建议保持硬件不动,只调寄存器。等哪天你真的要去设计自己的天线,再单独学S11参数和天线手工调试。
我自己的经验是:这类多协议读卡器,硬件连接和初始化比协议代码更容易出错。CLRC663的协议命令其实不复杂,翻一下14443和15693的帧格式,把REQA、防冲突、Inventory这几个命令跑通,大半个项目就完成了。剩下的时间基本都花在排查供电、接线和天线干扰上。你现在如果也卡在某个“读不到卡”的状态,可以先把手放在模块上方看一下距离有没有变化,再用示波器抓一下SPI波形,一般来说很快就能定位问题。