
做嵌入式读写卡开发的朋友应该都有过这种体会项目一开始只要求读M1卡用RC522轻松搞定没过多久客户说要兼容身份证或公交卡RC522直接歇菜再后来又说要支持图书标签、巡检标签这类15693卡只能重新选型。如果你手头有一块STM32F103C8T6最小系统板又希望用一套硬件把ISO14443A、ISO14443B、ISO15693乃至FeliCa全都能覆盖到那CLRC663国产NFC模块几乎是最划算的一条路。这篇文章把我从零驱动CLRC663的完整过程整理出来包括硬件接线、CubeMX配置、寄存器读写、三大协议的收发流程以及我实际调试中踩过的坑。整篇内容围绕STM32F103C8T6加CLRC663模块展开用的都是国产模块商家常见的“邮票孔小板”形态代码基于HAL库和SPI1接口方便直接移植。无论你是刚开始接触NFC协议栈的新手还是被RG522逼到想换芯片的老手这篇文章都能帮你少走不少弯路。1. 整体方案设计与选型思路1.1 为什么是CLRC663而不是RC522或PN532很多人在选NFC前端芯片的时候第一反应是RC522因为资料多、例程烂大街、成本也低。但RC522最大的问题在于它只支持ISO14443A协议的MIFARE系列卡片M1、Ultralight、NTAG这类卡没问题遇到ISO14443B协议的卡片就完全没辙更不要提ISO15693这类远距离标签了。PN532表面上看支持A、B、FeliCa三种协议而且自带I2C、SPI、UART三种主机接口很多人拿它做开发板玩。但PN532本质上被定位成一个“NFC前端加协议栈”的综合芯片很多协议处理是由内部固件完成的灵活性受限而且它不支持15693协议。也就是说在需要同时读14443A、14443B和15693卡的项目里PN532一样无能为力。CLRC663就不一样了它本身是NXP推出的多协议NFC前端芯片完整支持ISO/IEC 14443A、ISO/IEC 14443B、ISO/IEC 15693、ISO/IEC 18092以及FeliCa协议。它的工作频率是13.56MHz发射功率和接收灵敏度都要比RC522好一个档次。国产模块商把它做成小板子之后价格已经下探到能大量使用的水平所以在“一套硬件通吃多协议”这个需求上CLRC663是当前性价比很高的选择。1.2 国产模块的典型形态与STM32F103C8T6的适配性市面上常见的CLRC663国产模块大概分两种形态。一种是芯片加PCB天线的完整小板模块本身带陶瓷天线或绕线天线直接焊排针就能用另一种是裸芯片加外围匹配电路的半成品需要自己设计天线。我自己用的是第一种带邮票孔和排针的模块这种最省事直接飞线到STM32F103C8T6最小系统板就行。STM32F103C8T6这个芯片虽然是个老将但资源对于驱动CLRC663来说绰绰有余。它主频最高72MHz有64KB Flash和20KB SRAMCLRC663驱动加上协议处理代码总共也就占几KB的Flash跑FreeRTOS都还有大量余量。而且STM32F103C8T6引出的SPI1正好是PA5、PA6、PA7、PA4跟大部分CLRC663模块的SPI引脚一一对应接线非常顺手。1.3 一套代码能覆盖的场景把CLRC663跑起来之后你能做的就是全协议卡片的读卡、写卡、认证和扇区操作了。比如ISO14443A协议的M1 S50、S70、NTAG213、NTAG215ISO14443B协议的SRIX4K、SRI512ISO15693协议的ICODE SLI、ICODE SLIX以及多用于海外地区的FeliCa卡片。这套组合可以覆盖门禁、交通、图书管理、资产盘点、生产追溯等很多实际项目场景。我这次做的就是一台手持设备上的多协议读卡器MCU用的STM32F103C8T6读卡模块用CLRC663国产小板目标是同一个读卡界面能自动识别三种卡型并读出卡号。后面所有内容都围绕这个目标展开。2. 硬件连接与初始化配置2.1 接线、供电与电平匹配先讲接线。STM32F103C8T6的供电是3.3VCLRC663模块的供电也是3.3V所以两者之间可以直接连接不需要电平转换。Pins分配我用了硬件SPI1加两个普通GPIO来控制NSS、RST和IRQ。STM32F103C8T6引脚CLRC663模块引脚说明3.3VVCC模块主供电GNDGND共地PA5SCKSPI时钟PA6MISOSPI主设备输入从设备输出PA7MOSISPI主设备输出从设备输入PA4NSS / CS片选软件控制PA3IRQ中断请求可轮询也可外部中断PB0RST复位控制有一点要特别提醒CLRC663模块的天线发射瞬间电流比较大峰值可能到150mA以上。如果直接从STM32F103C8T6最小系统板板载的3.3V LDO取电读卡瞬间电压容易跌落轻则读卡距离变短重则模块直接复位。我最早的版本就是这么干的结果读卡时好时坏后来改成外接一个单独的3.3V稳压模块给CLRC663供电问题立刻消失。如果你项目上只能共用3.3V至少要在模块电源引脚附近加一个大电容100uF的钽电容或电解电容都行实测能改善很多。2.2 用CubeMX初始化SPI和GPIO我习惯用STM32CubeMX先生成工程骨架再手动补充驱动代码。新建STM32F103C8T6工程后做几件事。RCC配置里打开外部高速时钟HSESYS里Debug选择Serial Wire避免烧录一次后SWD口被禁用。SPI1选择Full-Duplex Master模式波特率预分频我选了16分频也就是72MHz除以16等于4.5MHz的SPI时钟。CLRC663的SPI从机模式最高能跑多少要看数据手册但4.5MHz这个速度足够稳也没有必要追求更高。时钟极性CPOL选Low时钟相位CPHA选1Edge也就是SPI Mode 0大部分模块默认就是这个模式。然后配置PA4为GPIO输出用作NSS片选PA3为GPIO输入用作IRQ读取PB0为GPIO输出用作复位控制。PA3我这里没开外部中断直接在主循环里轮询简单很多。如果你用的是标准外设库而非HAL库逻辑完全一样只是初始化代码写法不同。核心就是先把SPI模式配对其余交给时间。2.3 寄存器读写函数先让MCU能和模块对话CLRC663的寄存器和RC522同源但并不是完全一样。它的SPI地址格式有点特别寄存器地址要左移一位再把最低位或最高位作为读写标志。以我用的例程为基础读寄存器时地址组合是(reg 1) | 0x80写寄存器时是(reg 1)。很多第一次接触CLRC663的人在这里就卡住了直接用(reg | 0x80)去读读回来的数据全都不对。下面是我实际使用的寄存器读写函数基于HAL库static void clrc663_cs_low(void) { HAL_GPIO_WritePin(NSS_GPIO_Port, NSS_Pin, GPIO_PIN_RESET); } static void clrc663_cs_high(void) { HAL_GPIO_WritePin(NSS_GPIO_Port, NSS_Pin, GPIO_PIN_SET); } uint8_t clrc663_read_reg(uint8_t reg) { uint8_t tx[2] {0}; uint8_t rx[2] {0}; tx[0] (uint8_t)((reg 1) | 0x80U); tx[1] 0x00U; clrc663_cs_low(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 2, 10); clrc663_cs_high(); return rx[1]; } void clrc663_write_reg(uint8_t reg, uint8_t val) { uint8_t tx[2] {0}; tx[0] (uint8_t)(reg 1); tx[1] val; clrc663_cs_low(); HAL_SPI_TransmitReceive(hspi1, tx, tx, 2, 10); clrc663_cs_high(); }这里读操作比较特殊要先发一个地址字节再发一个空字节才能把数据读回来。原因是SPI总线是全双工的主机发时钟的同时从机也把数据放到了MISO线上所以读寄存器要两个字节完成。写完读写函数后建议先做一次软复位验证通信是否打通。软复位的方法是往CommandReg寄存器写0x09命令字。如果复位成功延时一段时间后读某些状态寄存器不会出现全0xFF说明SPI链路已经通了。void clrc663_soft_reset(void) { clrc663_write_reg(0x00, 0x09); // CommandReg SoftReset HAL_Delay(10); }3. 核心驱动框架与三大协议收发流程3.1 公共的Transceive收发函数是整个驱动的地基无论读哪种协议的卡片底层的操作模式都是一样的把待发送的命令字节写进CLRC663的FIFO然后启动Transceive命令芯片会自动完成调制、发送、接收、解调、CRC校验最后把收到的数据放进FIFO主机再从FIFO里读出来。这个流程可以封装成一个通用函数后面三个协议都会反复用到。实际工程里我的Transceive函数大概长这样uint8_t clrc663_transceive(uint8_t *tx_buf, uint8_t tx_len, uint8_t *rx_buf, uint8_t *rx_len) { uint8_t irq 0; uint16_t timeout 2000; // 1. 先回到Idle清掉上一次状态 clrc663_write_reg(CommandReg, 0x00); // 2. 清空FIFO这里需要写FIFOLevelReg的清空位具体值参考例程 clrc663_clear_fifo(); // 3. 把要发送的数据依次写入FIFO数据寄存器 for (uint8_t i 0; i tx_len; i) { clrc663_write_reg(0x08, tx_buf[i]); // FIFODataReg } // 4. 启动Transceive命令 clrc663_write_reg(CommandReg, 0x07); // 5. 轮询ComIrqReg等待接收完成或错误 while (timeout--) { irq clrc663_read_reg(0x02); // ComIrqReg if (irq 0x20) { break; // RxIRq置位说明接收完成 } if (irq 0x01) { return 0xFF; // ErrIRq置位通信出错 } } if (timeout 0) { return 0xFE; } // 6. 读取FIFO里的接收数据 uint8_t len clrc663_read_reg(0x09); // FIFOLevelReg if (len *rx_len) { len *rx_len; } for (uint8_t i 0; i len; i) { rx_buf[i] clrc663_read_reg(0x08); } *rx_len len; // 7. 清中断标志回到Idle clrc663_write_reg(CommandReg, 0x00); return 0x00; }需要注意CLRC663的发送和接收参数是由TxModeReg、RxModeReg这些寄存器控制的。你要读14443A卡之前就得先把这些寄存器配置成14443A对应的模式切到15693之前又要改成15693对应的模式。这些配置语句在每个模块商的例程里都有一大段我强烈建议你直接用厂家给的初始化函数不要自己去数据手册里一个个抠。抠寄存器不是不行只是性价比很低而且不同模块的天线匹配参数还不一样。3.2 ISO14443A协议从反碰撞到读M1块ISO14443A是大家最熟悉的一套协议M1卡、NTAG卡都走这个协议。完整的读一个M1卡块数据的流程分四步请求、防碰撞、选卡、认证然后才是读块。第一步叫REQA也就是发一个0x26的7位短帧命令让卡片回应ATQA。这里要注意发送的位长度BitFramingReg要配成0x07表示最后发送的字节只发7位。这个细节非常容易漏漏了之后卡片根本没反应。第二步是防碰撞。如果卡片在感应区里只有一张直接发反碰撞命令0x93 0x20卡片会返回4字节UID加1字节BCC校验。第三步是选卡发0x93 0x70加上UID和BCC卡片返回SAK字节从SAK可以判断卡类型是M1 S50还是S70。第四步是认证。M1卡默认有密钥要把密钥先加载到芯片里或直接在认证命令里带上KeyA或KeyB和块地址芯片会根据UID自动完成加密认证。之后就可以发0x30加块地址读指定块的数据了。我之前在M1卡这里写了一套简化代码供参考uint8_t read_m1_block(uint8_t block, uint8_t *data) { uint8_t buf[16]; uint8_t len 0; uint8_t cmd[2] {0x30, block}; // 假定已经完成了REQA、反碰撞、选卡、认证 len sizeof(buf); clrc663_transceive(cmd, 2, buf, len); if (len 16) { memcpy(data, buf, 16); return 0x00; } return 0xFF; }CLRC663和RC522一个比较大的区别是CLRC663内部的自动状态机能力更强很多字节级的CRC计算、帧封装都帮你处理了。但代价是寄存器变多初始化配置比RC522要繁琐尤其是调制深度、接收增益这些参数直接影响读卡距离。3.3 ISO14443B协议REQB、ATQB和ATTRIBISO14443B协议在国内最常见的应用是身份证和部分银行卡但很多开发板厂商很少给B卡例程所以不少人第一次调B卡时都很懵。B卡的流程和A卡不太一样。首先要发REQB命令唤醒B卡。REQB命令由0x05和0x00两个字节组成卡片收到后会返回ATQB应答里面包含了PUPI、应用数据、协议信息等一共12个字节左右。拿到ATQB之后主设备要发ATTRIB命令来“选中”这张卡。ATTRIB命令以0x1D开头后面跟上从ATQB里解析出的4字节PUPI标识等参数。这个步骤和A卡的Select类似但格式完全不同。选卡成功之后就可以按卡片的应用层协议进行块读写操作了。我调试B卡时踩过最大的坑是CRC计算方式。CLRC663的Transceive模式下CRC是否自动加取决于TxModeReg里的CRC配置位。如果初始化时把CRC配成14443A的CRC_A那么发B卡命令时CRC算法就是错的卡片不可能有响应。B卡命令要切换成CRC_B所以每次切换协议时必须把CRC模式一起切过去。uint8_t iso14443b_wakeup(uint8_t *atqb, uint8_t *atqb_len) { uint8_t reqb[2] {0x05, 0x00}; uint8_t len 16; clrc663_transceive(reqb, 2, atqb, len); *atqb_len len; // len若在12字节左右说明拿到了ATQB否则为超时 return (len 12) ? 0x00 : 0xFF; }B卡看起来命令比A卡少但实际调试难度比A卡大因为很多B卡芯片的应答速度差异很大接收超时参数必须留够余量。3.4 ISO15693协议Inventory与阅读块数据15693协议的标签主要用于图书管理、档案追踪、衣物洗涤、医疗器械管理等需要更长读取距离的场景。CLRC663在15693协议下能支持比较远的读卡距离实际测下来比14443A的M1卡要远不少。15693协议里最常用的命令是Inventory也就是盘点命令。模块例程里通常这样发送uint8_t inv[3] {0x26, 0x01, 0x00}; clrc663_transceive(inv, 3, buf, len);第一个0x26是Flag字节表示使用高速率、单子载波、不包含AFI字段第二个0x01是Inventory命令码第三个0x00是Mask长度读取全部标签时设为0代表不带Mask。标签收到后返回的应答里包含DSFID和8字节UID。有些例程用0x01作为Flag不同配置帧格式会有细微差别但效果一样关键是要和模块初始化时的参数一致。拿到UID之后要读取标签某个块的数据发读单块命令。15693的读单块命令由三部分组成Flag(0x00或0x20)、命令码0x20、块号。例如uint8_t read_cmd[3] {0x00, 0x20, 0x01}; // 读第1块 clrc663_transceive(read_cmd, 3, buf, len);标签返回的数据长度通常是块长度加一个标志字节例如返回5个字节其中第一个字节是应答标志后4个是块数据。15693协议的块大小由标签本身决定常见的是4字节一块。15693协议有个好处是帧格式相对宽松Response时间较长不用像14443A那样卡着严格的时序所以调试起来反而不容易超时。3.5 FeliCa也要简单说两句FeliCa在国内用得少但在日本、新加坡等地很普及手机NFC里的很多公交卡功能就基于它。CLRC663在硬件上支持FeliCa协议也就是ISO/IEC 18092的被动模式。FeliCa的初始化流程和前面三个协议差异比较大最典型的是它的Polling命令命令码是0x00后面跟着请求的系统码通常为0xFFFF。它没有类似ISO14443A先来一个7位短帧REQA这个过程协议栈里直接把命令按NFC帧格式发出即可。如果你的目标市场只在国内FeliCa可以先不深入把A、B、15693三条路跑通已经能解决绝大多数需求。4. 调试经验与常见问题速查4.1 读寄存器全是0xFFSPI链路不通如果你调用读寄存器函数发现不管读哪个地址都返回0xFF十有八九是SPI通信压根没建立起来。先检查NSS片选代码有没有正常工作很多人只配置了SPI1的硬件NSS却没有把PA4配置成普通GPIO输出并软件控制高低电平。再一个很容易犯的错是MISO和MOSI接反。模块板子上的丝印有时候标的是MISO、MOSI有时候标的是SDO、SDI接错之后通信全乱。用万用表或者逻辑分析仪量一下主机MOSI引脚上的电平变化应该能传到模块的MOSI引脚上这一条能查掉一大半的接线问题。4.2 SPI读写正常但卡片完全不响应如果是SPI寄存器读写都正常复位也成功但天线放上卡片一点反应都没有优先怀疑不是协议问题而是硬件工作在开机状态但天线没有发射。拿示波器探头放在天线两端应该能看到13.56MHz的正弦波。如果载波幅度很低或者干脆没有说明天线匹配电路有问题或者模块的天线使能寄存器没配置对。很多国产模块在第一次上电时默认关闭天线发射需要往TxControlReg写0xC0之类的值才能打开。具体数值每个模块可能不一样直接抄商家例程里的初始化顺序。还有一个小细节是供电。我前面反复强调过CLRC663的瞬时电流比较大如果电源不稳天线功率上不去读卡距离会变得特别短。你可以做一个简单测试读卡瞬间用示波器看3.3V电压有没有明显塌陷。电压下拉超过0.3V基本就是供电不足。4.3 三种协议切换时互相“打架”我在做多协议自动识别时遇到一个问题刚读完一张15693卡马上切回14443A读M1卡发现M1卡没有响应了。排查了半天原因是15693和14443A的初始化参数差异非常大交换协议时必须把包含TxMode、RxMode在内的相关寄存器重新配置成目标协议的值不能只切换命令字节。更实用的做法是给协议切换做一个状态机。每次切换协议前调用对应协议的init函数把调制方式、子载波模式、CRC方式全部重新设置一遍。虽然多花了几毫秒但对系统的稳定性帮助很大。另外在FreeRTOS环境下如果有多个任务都访问SPI和CLRC663一定记得加互斥锁否则任务调度导致SPI传输被切断模块状态会乱掉。中断处理建议用一个二值和信号量通知读卡任务不要让读卡逻辑直接跑到中断回调里做。4.4 常见问题速查表现象可能原因解决办法读寄存器全0xFFSPI接线错误、NSS配置错误检查MOSI/MISO、检查PA4软件片选软复位后仍无响应RST引脚没拉对时序复位拉低至少1ms再拉高延时后再操作A卡能读B卡无反应CRC模式没切到CRC_B协议切换时重设TxMode/RxMode15693标签读不到天线距离过近或电源供电不足单独供电或加电容确认天线发射打开读卡距离明显偏短3.3V电压跌落严重外接LDO供电电源走线加粗频繁读失败但寄存器正常天线调谐偏移用示波器检查13.56MHz载波幅度中断一直触发IRQ配置错误确认IRQ有效电平清中断标志4.5 几个独家的调试心得如果你手头有逻辑分析仪务必把SPI的四根线全部抓下来看一遍。CLRC663的寄存器读写时序并不复杂但地址左移这一下特别容易在新驱动里写错。抓一次时序就能看出地址字节对不对比自己盯着调试器看半天下结论快得多。CLRC663的寄存器数量比RC522多不少但大部分寄存器用厂商给的例程初始化一次就够真正需要经常动的其实就那么几个。建议你写一个小工具函数把当前主要的几个配置寄存器值都打印出来切换协议时对比一下差异这样排查问题会非常清晰。很多模块商家的例程是用STM32F103的寄存器操作写的如果你用HAL库不建议整体照搬而是自己封装一层。寄存器的位定义是一样的但SPI的发送方式不一样HAL库的HAL_SPI_TransmitReceive自带多字节收发反而比寄存器操作更简洁。最后再分享一个实际操作中的体会CLRC663这块芯片虽然支持协议多但底层的天线匹配和不同协议的子载波参数非常敏感。国产模块好处是便宜、供货快但每个批次甚至每个商家的板子初始化参数都会有一点出入。所以拿到新模块的第一件事不是急着去读卡而是把商家给的例程原封不动跑一遍确认硬件没问题再改到自己的工程里。这样后面调试协议的时候每次出问题都能锁定在软件层面。这套代码跑通之后后面再做多协议门禁、图书盘点、资产追踪之类的项目基本都是直接复用。当初纠结了很久的选型问题现在回头看CLRC663这条路走得很值。