简介:面向STM32与FM17XX系列读卡芯片开发者的参考代码包,全面覆盖ISO14443A/B双协议寻卡流程,内含TypeA与TypeB卡片请求、防碰撞、选择等关键函数,并针对Mifare S50/S70、UltraLight、DESFire等常见卡片类型给出返回代码说明。工程中还提供SPI收发底层驱动、ADC采集、TIM定时器等外设配置,以及读卡成功后联动门锁电机的控制逻辑,适合门禁、智能锁、消费终端等场景的二次开发与调试。压缩包共259个文件,以C源文件与H头文件为主,同时包含Keil工程配置、汇编源文件、Hex固件、链接映射、HTM与说明文档等,总大小4.33MB,附带的编译中间文件便于还原完整工程环境。目前已有1173人学习下载,适合具备一定单片机基础、希望快速移植读卡方案或对照完整工程排查SPI通信、寻卡时序等问题的开发者。 这两年做嵌入式读卡器项目,我前前后后摸过NXP的RC522/RC531、复旦微的FM17XX系列,也踩过无数防冲突和天线匹配的坑。最近整理手上这套基于FM17XX的ISO14443A/B读卡参考代码时,才发现很多经验散落在各个项目的调试记录里,干脆集中写一篇,把芯片选型、协议差异、代码框架、调试避坑一次性讲清楚。如果你是做门禁、消费机、读写器或者需要快速适配非接触式IC卡的产品开发,这篇内容可以直接拿来当动手之前的底稿。
1. 这套参考代码解决的是什么问题
1.1 FM17XX:国产读卡芯片里的百搭选择
FM17XX是复旦微电子的非接触式读卡芯片家族,工作频率13.56MHz,支持ISO14443A/B协议,部分型号还兼容ISO15693。做嵌入式的人可能更熟悉RC522这类芯片,FM17XX在寄存器设计和命令集上跟NXP的RC系列有很大传承关系,很多型号可以直接用RC522的思路去驱动,但细节上又不一样。简单说,它就是一套“国产供应链、成本可控、双协议覆盖”的卡片读写方案。
在项目里选型时,要特别注意不同型号的能力边界:
| 型号 | 支持协议 | 接口 | 典型应用场景 |
|---|---|---|---|
| FM1702SL | ISO14443A | SPI | 门禁、考勤、M1卡读写 |
| FM1715SL | ISO14443A/B | SPI/UART/IIC | 通用读卡器、消费机 |
| FM1722SL | ISO14443A/B + ISO15693 | SPI/UART/IIC | 多协议读写器、图书管理 |
| FM17520 | ISO14443A | SPI/IIC | 低功耗消费、手环、水表 |
从实际项目看,只做M1卡读写,FM1702SL性价比最高;要兼容身份证、社保卡这类Type B卡片,最低得选FM1715SL,FM1702SL做不了Type B,这个坑我在早期选型时踩过,差点把硬件推倒重来。
1.2 参考代码的定位与适用场景
这套FM17XX读卡参考代码并不是某个官方SDK的搬运,而是把ISO14443A/B两种协议栈的完整操作流程做了模块化整理。代码从SPI底层驱动开始,到收发命令封装,再到寻卡、防冲突、选卡、认证、读写块,流程完整,注释清晰。
适合直接参考的场景包括:门禁读头开发、食堂消费机、充电桩读卡模块、校园卡读写终端,以及任何需要快速验证FM17XX芯片功能样机的项目。代码核心思路是“协议流程拆分 + 状态机驱动”,读卡操作不再是一堆寄存器赋值堆在一起,而是按协议阶段清晰推进,方便移植和排查问题。
2. 写驱动前,先理解ISO14443A/B的底层差异
2.1 Type A与Type B的基本区别
很多初接触非接触式IC卡的人,上来就写代码,结果发现同一套驱动有时灵有时不灵,本质是没搞清楚Type A和Type B的物理层和链路层差异。
Type A使用Modified Mill编码、100% ASK调制,从读卡器到卡片,再从卡片返回数据,用的是Miller编码和Manchester编码。这个编码体系的优点是抗干扰能力好,但接收端需要更精确的位同步。市面上最常见的M1卡(S50/S70)、NFC Tag,都是Type A。
Type B使用NRZ编码、10% ASK调制,返回数据采用BPSK。Type B的特点是调制深度低,对天线和功放要求更高,但数据传输效率相对高。第二代居民身份证、护照、社保卡、银行IC卡(PBOC芯片卡)基本都是Type B。
这两者最大的工程意义在于:你的天线匹配和读卡芯片硬件,必须同时满足两种调制方式,才能在同一个读头上跑A/B双协议。软件上如果只调好了Type A,Type B不一定好使。
2.2 防冲突机制是最容易翻车的协议细节
防冲突(Anti-collision)是ISO14443协议里最核心、也最容易写错的环节。Type A和Type B的防冲突思路完全不同。
Type A采用比特级防冲突。多个卡片同时进入场区时,读卡器发送SEL和NVB参数,卡片反馈自己的UID部分位。如果卡片数量多,读卡器收到的是叠加信号,寄存器里会记录发生冲突的位位置(CollPos),这时读卡器通过修改NVB逐步筛选,直到只剩一张卡。
Type B采用时隙机制。读卡器发送REQB后,卡片按照随机或计算出的时隙编号,在对应时隙返回ATQB。读卡器根据ATQB中的PUPI(唯一标识)锁定目标卡片,再用ATTRIB命令完成激活。
很多代码在Type A防冲突时出错,是因为没有正确处理Register中的冲突位置,或者没有在冲突后重新按位发送部分UID。Type B代码出错,则往往是REQB参数设置不合理,导致时隙错乱。
2.3 命令帧的位类型别忽视
ISO14443A中区分短帧(7bit)、标准帧(8bit)和长帧。寻卡命令REQA和WUPA是短帧,防冲突和后续读写命令是标准帧。FM17XX这类芯片会在寄存器里配置帧格式,发送前需要确认位帧寄存器和CRC使能是否与命令类型匹配。
比如REQA命令是0x26,只要7位就能表达,但如果你代码里按8位标准帧发,有些卡也能响应,有些卡就会判定为非法命令。这种“看起来能用、但兼容性差”的问题,根子就在帧格式配置上。
3. 核心代码架构与流程实现
3.1 初始化与底层收发封装
FM17XX作为从设备,初始化第一步是SPI总线的建立。SPI的速率建议控制在5MHz以内,具体以数据手册时序要求为准。除了收发函数,还要留出复位引脚控制,上电后对芯片做一次软复位,把状态机拉回已知位置。
void fm17xx_init(void) { // 软复位 fm17xx_write_reg(CMD_REG, CMD_SOFTRESET); fm17xx_delay_ms(10); // 配置发送和接收模式,打开CRC、启用天线 fm17xx_write_reg(MODE_REG, 0x3F); // 使能CRC_TX/CRC_RX,标准帧 fm17xx_write_reg(TX_MODE_REG, 0x00); fm17xx_write_reg(RX_MODE_REG, 0x00); fm17xx_write_reg(TX_ASK_REG, 0x40); // 调制深度 fm17xx_write_reg(TX_CTRL_REG, 0x50); // 开天线 }这段代码的用意不是照抄寄存器数值,而是让你理解初始化的几个关键动作:复位芯片、确认通信模式、配置编码调制参数、打开射频发射。FM17XX不同型号的寄存器初始值会有差异,调片子时一定要对照数据手册。
3.2 transceive封装是读卡驱动的灵魂
读卡操作本质上就是“发一帧命令,收一帧响应”。这个收发动作,芯片厂家叫作Transceive。FM17XX内部有64字节FIFO,数据收发都先经过它。
int fm17xx_transceive(uint8_t *tx_buf, uint8_t tx_len, uint8_t *rx_buf, uint8_t *rx_len) { uint8_t irq, timeout = 0; // 清FIFO和中断标志,防止上一帧残留数据干扰 fm17xx_write_reg(FIFO_LEVEL_REG, 0x80); fm17xx_write_reg(IRQ_REG, 0x7F); // 发送数据写入FIFO fm17xx_write_reg(FIFO_DATA_REG, tx_buf, tx_len); fm17xx_write_reg(BIT_FRAMING_REG, 0x00); // 启动Transceive命令 fm17xx_write_reg(CMD_REG, CMD_TRANSCEIVE); // 轮询等待接收中断,注意要加超时保护 while (1) { irq = fm17xx_read_reg(IRQ_REG); if (irq & RX_IRQ_BIT) { break; } if (timeout++ > 5000) { return ERR_TIMEOUT; } } *rx_len = fm17xx_read_reg(FIFO_LEVEL_REG); fm17xx_read_reg(FIFO_DATA_REG, rx_buf, *rx_len); return ERR_OK; }这里有个细节:发送命令前必须清FIFO和中断。我见过太多项目没做这一步,导致上一帧的残留数据混进来,CRC校验时好时坏。另外读FIFO的时候,如果接收到的数据有错,部分芯片会自动触发错误中断,代码里最好把ErrorReg的状态读出来,区分是超时、CRC错误还是帧错误,方便上层逻辑做不同处理。
3.3 Type A完整流程:寻卡、防冲突、选卡、认证、读写
Type A的流程是分阶段执行的:
第一步,寻卡。发送REQA(0x26)短帧,卡返回ATQA(2字节),ATQA的最低有效位后续用于级联判断。
第二步,防冲突。如果场区里只有一张卡,可以直接跳过防冲突,把UID当作全0处理。多张卡同时进场,就要执行比特防冲突。FM17XX有一个CollReg寄存器,冲突位置会记录在这里。核心逻辑是发送0x93(SEL)+ NVB + UID部分数据,根据CollReg反馈的冲突位调整NVB,迭代筛选UID。
第三步,选卡。UID完整解析后,发送SEL + NVB = 0x70 + 4字节完整UID + CRC,卡返回SAK(1字节)。SAK可以判断卡类型,比如M1 S50的SAK通常是0x08。
第四步,认证和读写。M1卡的密钥认证由FM17XX硬件完成,软件只需调用MFAuthent命令,把密钥和块地址写进FIFO。认证通过后,发送读取命令0x30 + 块地址,读取16字节块数据;发送写入命令0xA0 + 块地址 + 16字节数据,完成写块。
这套流程看起来不复杂,但代码组织上一定要按“状态机”来写。很多项目把寻卡、选卡、认证塞在一个大函数里,一旦某一步失败,后续状态完全不可控。参考代码里把每一步都封装成独立接口,上层Listen循环只管调用、判断返回值,异常恢复也清晰。
3.4 Type B流程:REQB、ATQB、ATTRIB
Type B相对Type A的流程更简单,但格式更严格。
第一步,发送REQB。标准REQB是5字节:0x05 + AFI + PARAM + CRC。PARAM里的N值决定时隙数量,常用N=1,表示卡片在1个时隙内随机应答。
第二步,接收ATQB。ATQB总长11字节(不含CRC),包含PUPI(4字节)、应用数据(4字节)、协议信息(3字节)。PUPI相当于卡片在本次会话中的临时身份,后续ATRIB需要用到。
第三步,发送ATTRIB。ATTRIB命令用于激活卡片,命令格式为0x1D + PUPI(4字节)+ 参数 + 可选字段 + CRC。激活成功后才能继续访问卡片内部应用。
Type B调试时特别容易遇到REQB参数不对导致无响应。AFI(应用族标识)用于过滤应用类型,普通读取设0x00即可。PARAM里的N值和ADV(附加帧保护)位会影响卡片应答,初学者尽量保持N=1,别去调整,稳定后再优化。
4. 真机调试中的踩坑记录与排查技巧
4.1 读不到卡片,先检查硬件而不是软件
读卡器“不上卡”是最高频的故障,九成是硬件问题。晶振起振了吗?用示波器看13.56MHz天线波形,幅度是否足够?天线匹配电容是否按规格书选型?FM17XX的TX1/TX2管脚要接天线匹配网络,这个网络设计直接影响读卡距离。
我调试时习惯先确认SPI链路,读芯片版本寄存器或ID寄存器,能读回来芯片就是活的。然后测场强:拿卡片贴近天线,用示波器看卡片加载调制后的波形。如果波形幅度极低,大概率是天线线圈没匹配好,这时候软件调得再勤都白搭。
注意:FM17XX天线匹配不是随便接个电容就行的。常见的匹配结构是TX1/EMC滤波/天线线圈/C1/C2并联谐振,谐振点要落在13.56MHz附近。硬件调好后,读卡距离才能稳。
4.2 防冲突定位不准导致选卡失败
这是一个非常典型的软件坑。Type A防冲突时,卡片返回的UID被检测到冲突,CollReg会记录冲突位。芯片文档里CollPos字段的值是基于从LSB开始的位偏移,很多参考代码直接把这个值当字节偏移用,导致后续NVB计算错误。
处理方式是:冲突位对应的“剩下要发送的位数”要重新计算,再组装NVB字节。NVB高4位表示已经完整发送的字节数,低4位表示最后一个字节里已发送的位数。很多代码在这里用了位运算或移位方式巧算,但逻辑一绕就容易错。我的建议是先写成最直观的版本:
// 假设CollPos表示冲突发生的位置(以位为单位) uint8_t complete_bytes = coll_pos / 8; uint8_t partial_bits = coll_pos % 8; uint8_t nvb = (complete_bytes << 4) | partial_bits;这样在调试串口里打印出来,每一步都看得见,比起一堆位运算宏好排查得多。
4.3 Type B卡片时灵时不灵
Type B使用10% ASK调制,幅度调制比Type A低得多。天线线圈如果Q值太高,调制信号可能被压掉,导致读卡器收不到Type B卡片的ATQB。FM17XX本身没问题,但天线匹配网络对Type B更敏感。
软件上,REQB命令发出后,等待ATQB的超时时间不能太短。Type B卡片有时候会在时隙末尾才应答,如果超时设成10ms,可能正好错过。建议至少给30ms以上。另外,有些项目在使用多协议轮询时,Type A和Type B交替执行,中间要留一点切换间隔,让场稳定下来,否则卡片状态机还没复位,收不到新的REQB。
4.4 连续读卡死机、看门狗复位
设备长时间运行后偶发死机,多半是某一个读卡错误中断没处理干净,导致后续中断一直被阻塞。我的做法是:每一次transceive之后,无论成功失败,都把中断标志寄存器读一遍并全部清掉;进入待机前再清一次FIFO。
另外,FM17XX的命令寄存器写入要遵循“先写参数、再写命令”的顺序。比如MFAuthent命令,必须先把密钥和块地址写FIFO,再写命令字。顺序反了,芯片行为不可预测。这种问题在代码review时不仔细看很难发现,只能靠调试经验沉淀。
这里给个经验值:在实际项目中,我会给每一条读卡指令加上总超时兜底,一般设置500ms。就算协议某个环节卡死,也能及时复位重来,不会把整个系统拖垮。
5. 关于这套代码的个人心得
代码整理过程中,最大的收获不是寄存器用熟了,而是对ISO14443A/B协议流程有了整体认知。读卡这件事,表面上是几条命令的拼装,实际上每个环节的状态转移、异常恢复、硬件交互都环环相扣。我在实际项目里用这套架构做过门禁读头和消费机,稳定性和可维护性都明显好于早期那版把所有逻辑写在一个大循环里的代码。
最后再分享一个小技巧:调试读卡项目时,一定留一路串口日志,把每次transceive的命令、返回值、错误码、耗时都打出来。很多奇怪问题,看协议分析仪反而看不出,打日志却能发现是某一帧命令的CRC使能没配对、或者FIFO残留数据导致下一帧错位。这套参考代码里也把日志接口留好了,格式你自己随便改。希望这篇能帮你少走几步弯路。
本文还有配套的精品资源,点击获取