ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FM17550读卡程序设计:从RC522迁移到SPI接口的完整实战

FM17550读卡程序设计:从RC522迁移到SPI接口的完整实战 简介面向 STM32F103 嵌入式开发者的 FM17550 NFC 读卡程序源码包用于解决 ISO14443A/BAB 卡的读写与低功耗控制问题可应用在门禁、公交卡、支付等场景适合有一定嵌入式基础、需要快速落地 NFC 读卡功能的开发者。压缩包内共 13 个文件包含 7 个 .c 源文件和 6 个 .h 头文件既有 nfcTest 主流程、nfcSpi 底层通信也有 type_a/type_b 协议处理和低功耗卡片检测接口整体仅 26KB结构清晰便于快速移植。程序提供 SPI 速率、极性、相位等关键参数配置参考并给出睡眠、停机、待机等低功耗模式设置思路可帮助开发者规避通信异常延长电池供电设备的工作时间。目前已有 3534 人学习下载适合学习 NFC 协议栈、熟悉 FM17550 寄存器操作或复用到自有硬件平台的工程人员。 最近在调一个门禁项目的刷卡模块顺手把FM17550读卡程序的整个设计思路和踩坑过程整理了一下。FM17550是复旦微电子推出的一颗13.56MHz射频读卡芯片引脚和寄存器跟市面上常见的RC522高度兼容支持ISO14443A/B、Mifare S50/S70这类主流卡。说人话就是刷卡、读卡、写卡它能全包适合做门禁、储物柜、桌面读卡器、自助终端这些场景。要是你正要从RC522迁移到FM17550或者准备从零写读卡驱动、对NFC协议流程还不熟这篇内容应该能帮你节省不少时间。1. 整体设计思路先把选型和通信链路定下来1.1 为什么选FM17550而不是继续用RC522我最早接触这类读卡芯片是RC522后来项目要求支持更多卡片类型才把目光转到FM17550。FM17550和RC522的硬件引脚基本兼容SPI接口时序也相似很多寄存器可以直接沿用RC522的例程迁移成本很低。但它和RC522最大的区别在于FM17550在ISO14443A的基础上还支持ISO14443B逻辑加密卡、CPU卡这类场景也能覆盖RC522官方支持范围就没这么宽。实际项目里“能替换”是一件很省心的事。比如老产品已经量产了RC522模块想升级功能PCB不用大改把芯片换掉软件初始化函数微调读卡主流程基本不用动。这一点对生产端很友好BOM替换也方便。1.2 通信方式选择SPI是默认最优解FM17550数据手册上支持SPI、I2C、UART三种通信接口。我项目里用的是SPI原因很直接SPI速率高、时序简单在STM32、GD32这类MCU上都有现成外设驱动代码也容易调试。UART适合主控本身跑的是串口通信方案I2C的优势是节省IO但读卡数据交换比较频繁I2C的总线效率和SPI还是有差距。连接上一般模块会引出这些引脚引脚方向说明SDA/CS输入SPI片选低电平有效SCK输入SPI时钟MOSI输入主发从收MISO输出主收从发RST输入复位低电平复位IRQ输出中断输出可悬空VCC-3.3V供电GND-地IRQ引脚理论上可以接MCU外部中断但我在实际项目里几乎不用它直接轮询ComIrqReg寄存器就够了。读卡本身是个低频操作轮询方式足够快还能少占一个IO口省下来的引脚干点别的更有价值。2. 核心细节解析FM17550寄存器操作和初始化2.1 看懂寄存器是关键第一步FM17550的寄存器很多但真正写驱动用到的就那十几个。这里我把最核心的寄存器列出来方便后面看代码时对应寄存器地址作用CommandReg0x01命令寄存器写软复位、发送、接收等命令ComIrqReg0x04通信中断标志含RxIRq、ErrIRq、TimerIRqErrorReg0x06错误标志CRC错误、协议错误等Status1Reg0x07状态寄存器1FIFO是否空等信息Status2Reg0x08状态寄存器2含加密标志、卡片选择状态FIFODataReg0x09FIFO数据口读写数据都走这里FIFOLevelReg0x0AFIFO当前字节数读之前先看这里ControlReg0x0C控制寄存器含当前接收位数信息BitFramingReg0x0D位帧调节短帧发送、启动发送等CollReg0x0E冲突检测多卡防冲突时用ModeReg0x11工作模式CRC初始值、Mifare模式使能TxControlReg0x14发送控制最关键的是开天线RFCfgReg0x26射频配置接收器增益调节TModeReg0x2A定时器模式超时判断靠它很多朋友一上来就复制例程从不关心每个寄存器为什么要写这个值。结果遇到点异常就抓瞎。其实读卡程序本质上就是一个“状态机 数据收发”的过程寄存器的每个位都有明确含义花半天把上面这些寄存器过一遍后面调试效率会高很多。2.2 初始化必须做对的三件事初始化函数看起来就十几行但里面有三个重点软复位、开天线、定时器配置。缺任何一个读卡都跑不起来。软复位的作用是让芯片内部逻辑回到确定性状态尤其上电后或者之前操作卡失败导致状态机卡死时先复位再配置是最稳妥的做法。开天线则对应TxControlReg的0x83这个值不仅使能了发送通道还打开了天线驱动没有这一步芯片连射频场都不会建立卡片靠近根本没有反应。定时器配置则是为了在读写卡超时时能产生中断避免程序死等。下面是一段我实测可用的初始化代码void FM17550_Init(void) { // 1. 硬复位 FM17550_RST_HIGH(); delay_ms(10); FM17550_RST_LOW(); delay_ms(10); FM17550_RST_HIGH(); delay_ms(10); // 2. 软复位 FM17550_WriteReg(CommandReg, 0x0F); delay_ms(10); // 3. 基础配置 FM17550_WriteReg(ModeReg, 0x3D); // MIFARE模式CRC初始值0x6363 FM17550_WriteReg(TxModeReg, 0x00); // 发送编码方式 FM17550_WriteReg(RxModeReg, 0x00); // 接收编码方式 FM17550_WriteReg(TxControlReg, 0x83); // 开天线使能 FM17550_WriteReg(RFCfgReg, 0x48); // 接收器增益 FM17550_WriteReg(TxSelReg, 0x10); // 发送数据源选FIFO FM17550_WriteReg(RxSelReg, 0x10); // 接收数据源选L0 FM17550_WriteReg(DemodReg, 0x11); // 解调配置 FM17550_WriteReg(ModWidthReg, 0x26); // 调制宽度 FM17550_WriteReg(RxThresholdReg, 0x54); // 接收门限 // 4. 定时器配置超时100ms级别 FM17550_WriteReg(TModeReg, 0x8D); FM17550_WriteReg(TPrescalerReg, 0x3E); FM17550_WriteReg(TReloadRegH, 0x0E); FM17550_WriteReg(TReloadRegL, 0x2E); }SPI读写寄存器时要特别注意寻址方式。FM17550采用8位寄存器地址但通过SPI发送时高7位是寄存器地址最低位表示读写方向所以写寄存器要发(reg 1) 0x7E读寄存器则发((reg 1) 0x7E) | 0x80。FM17550的SPI接口同时兼容Mode0和Mode3但务必和MCU侧SPI配置保持一致我喜欢用Mode0稳定且例程多。3. 读卡流程实现从寻卡到读写块3.1 寻卡、防冲突、选卡的完整步骤ISO14443A的读卡流程可以拆成四步寻卡、防冲突、选卡、认证读写。这四步一步都不能省。很多入门朋友以为直接发个读命令就能拿到卡号实际不是这样的卡片和读卡器之间有一套完整的握手流程。寻卡是读卡器发送REQA或WUPA指令卡片回复ATQA。第二步防冲突解决多张卡同时进入射频场的问题读卡器获得完整UID。第三步选卡读卡器告诉卡片“我选中的就是你”卡片返回SAK应答。到这一步卡片才进入激活状态可以继续认证和数据操作。寻卡的时候指令模式和位帧很关键。发送0x26查所有卡发送0x52只查未休眠的卡。发送这两条指令时要设置BitFramingReg为0x07表示发送7位帧这是ISO14443A协议规定的短帧格式。等数据收完再清掉StartSend位避免影响下一次通信。uint8_t FM17550_Request(uint8_t req_mode, uint8_t *tag_type) { uint8_t status; uint32_t bits; FM17550_WriteReg(BitFramingReg, 0x07); // 7位帧 status FM17550_Transceive(req_mode, 1, tag_type, bits); return status; }防冲突和三段式通信调用Transceive实现uint8_t FM17550_Anticoll(uint8_t *snr) { uint8_t cmd[2]; uint8_t status; uint8_t i, check 0; uint32_t bits; cmd[0] 0x93; // ANTICOLLISION命令 cmd[1] 0x20; // 嵌套防冲突级联 FM17550_WriteReg(BitFramingReg, 0x00); status FM17550_Transceive(cmd, 2, snr, bits); if (status ! MI_OK) return status; for (i 0; i 4; i) check ^ snr[i]; if (check ! snr[4]) return MI_ERR_BCC; // 校验失败 return MI_OK; }选卡的实质是把5字节数据UID加BCC发给卡片卡片确认后返回SAK同时Status2Reg的加密相关标志位置位。选卡不成功后面认证一定失败。uint8_t FM17550_Select(uint8_t *snr) { uint8_t buf[7]; uint8_t status; uint32_t bits; buf[0] 0x93; buf[1] 0x70; // SELECT命令 for (int i 0; i 5; i) buf[2 i] snr[i]; FM17550_WriteReg(BitFramingReg, 0x00); status FM17550_Transceive(buf, 7, buf, bits); if (status ! MI_OK) return status; if (FM17550_ReadReg(Status2Reg) 0x08) return MI_OK; return MI_ERR_SELECT; }3.2 密钥认证与数据读写选完卡之后要读写数据块前必须做密钥认证。Mifare Classic卡默认有两个密钥A密钥命令是0x60B密钥命令是0x61默认出厂密钥一般是6个0xFF。认证命令需要把密钥和UID前4字节一起交给芯片芯片和卡片完成三重认证。uint8_t FM17550_Auth(uint8_t auth_mode, uint8_t block, uint8_t *key, uint8_t *snr) { uint8_t buf[12]; uint8_t status; buf[0] auth_mode; buf[1] block; for (int i 0; i 6; i) buf[2 i] key[i]; for (int i 0; i 4; i) buf[8 i] snr[i]; FM17550_WriteReg(CommandReg, 0x00); // 空闲状态 FM17550_WriteReg(FIFOLevelReg, 0x80); // 清FIFO for (int i 0; i 12; i) FM17550_WriteReg(FIFODataReg, buf[i]); FM17550_WriteReg(CommandReg, 0x0C); // PCD_AUTHENT status WaitForIrqAndClear(); if (status ! MI_OK) return status; if (FM17550_ReadReg(Status2Reg) 0x08) return MI_OK; return MI_ERR_AUTH; }认证成功后读块命令0x30加上块地址芯片返回18字节数据前16字节是块内容。写块命令0xA0分两步先发块地址让卡片准备卡片返回ACK后再把16字节数据发给卡片卡片再次返回ACK才算写入成功。第一次写卡的朋友经常忘记“等ACK”这个中间步骤导致写失败。uint8_t FM17550_ReadBlock(uint8_t block, uint8_t *data) { uint8_t cmd[2]; uint8_t status; uint32_t bits; cmd[0] 0x30; // MIFARE_READ cmd[1] block; status FM17550_Transceive(cmd, 2, data, bits); return status; }uint8_t FM17550_WriteBlock(uint8_t block, uint8_t *data) { uint8_t cmd[2]; uint8_t buf[16]; uint8_t status; uint32_t bits; cmd[0] 0xA0; // MIFARE_WRITE cmd[1] block; status FM17550_Transceive(cmd, 2, buf, bits); if (status ! MI_OK || (buf[0] 0x0F) ! 0x0A) return MI_ERR_WRITE; for (int i 0; i 16; i) buf[i] data[i]; status FM17550_Transceive(buf, 16, buf, bits); if (status ! MI_OK || (buf[0] 0x0F) ! 0x0A) return MI_ERR_WRITE; return MI_OK; }读卡过程中的所有收发包都靠一个底层的Transceive函数完成。这个函数做的事情是清空中断标志、把要发送的数据写入FIFO、启动PCD_TRANSCEIVE命令然后轮询ComIrqReg等待结果。等待超时很有讲究我习惯用芯片内部定时器中断来做超时判断这样不阻塞主循环太久。uint8_t FM17550_Transceive(uint8_t *cmd, uint8_t cmd_len, uint8_t *data, uint32_t *bits) { uint8_t irq; uint16_t timeout 0; FM17550_WriteReg(CommandReg, 0x00); // 空闲 FM17550_WriteReg(FIFOLevelReg, 0x80); // 清FIFO FM17550_WriteReg(ComIrqReg, 0x7F); // 清所有中断 for (int i 0; i cmd_len; i) FM17550_WriteReg(FIFODataReg, cmd[i]); FM17550_WriteReg(CommandReg, 0x0E); // PCD_TRANSCEIVE FM17550_SetBitMask(BitFramingReg, 0x80); // 启动发送 while (1) { irq FM17550_ReadReg(ComIrqReg); if (irq 0x01) return MI_ERR_TIMEOUT; // TimerIRq if (irq 0x08) return MI_ERR_CRC; // ErrIRq if (irq 0x20) break; // RxIRq if (timeout 20000) return MI_ERR_TIMEOUT; delay_us(10); } uint8_t n FM17550_ReadReg(FIFOLevelReg) 0x7F; for (int i 0; i n; i) data[i] FM17550_ReadReg(FIFODataReg); *bits (FM17550_ReadReg(ControlReg) 0x07) (n 3); return MI_OK; }注意清中断标志不能直接写0而是要把对应位写1这类芯片的中断标志是写1清零很多人第一次都栽在这。4. 实操过程一个完整读卡主循环的现场记录4.1 主循环怎么组织最顺手我项目里的主循环很简单先初始化SPI和FM17550然后在主循环里反复调用寻卡、选卡、认证、读块。卡片移开后要执行Halt命令让卡片休眠这样下次再刷卡才能及时被寻卡命令发现。void loop(void) { uint8_t atqa[2]; uint8_t snr[5]; uint8_t key[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t block_data[16]; if (FM17550_Request(0x52, atqa) MI_OK) { if (FM17550_Anticoll(snr) MI_OK) { if (FM17550_Select(snr) MI_OK) { if (FM17550_Auth(0x60, 1, key, snr) MI_OK) { FM17550_ReadBlock(1, block_data); // 业务处理... } } } FM17550_Halt(); } delay_ms(10); }这样一层一层if嵌套虽然看起来很土但对读卡这种强顺序流程来说最直观出问题也好定位。如果你想写得更优雅可以用状态机封装但业务没复杂到一定程度不建议为了抽象而抽象。4.2 现场调试时候的观测方法调试读卡程序最忌讳的是“黑盒调试”——烧录进去没反应然后开始乱试。我在现场调的时候第一件事就是加串口日志把每一步返回状态打出来。比如寻卡返回0是成功防冲突的BCC校验值是多少选卡后Status2Reg读到什么值这些信息一眼就能判断卡在哪一步。SPI时序出问题是最隐蔽的。如果怀疑SPI用示波器或逻辑分析仪抓SCK和MOSI重点看MISO上有没有卡片返回的数据波形。没有波形说明命令没发出去或者天线场没建立优先检查TxControlReg和硬件连接有波形但全是乱码才怀疑SPI模式或速率。5. 常见问题与排查技巧实录5.1 读卡调试遇到的五类典型故障读卡程序量不大但问题翻来覆去就那么几类我把实际项目中遇到的和朋友反馈过的问题整理成表格现象可能原因解决办法完全读不到卡天线没打开、SPI时序不对、供电电压不够、卡片类型不兼容检查TxControlReg是否为0x83量天线两端波形确认供电3.3V寻卡成功但防冲突失败FIFO数据没清干净、BCC校验问题、有小米手环等复合卡干扰清FIFO、校验BCC、打印原始数据比对选卡后认证失败认证命令格式错误、UID字节序反了、密钥不是默认密钥确认buf格式是“命令块号6字节密钥4字节UID”先试全FF密钥块数据读出来全是0xFF该块未认证、块地址越界、卡片损坏检查Auth是否成功确认扇区块地址映射读完一次卡后第二次读不到卡片没Halt休眠或者状态机没复位在流程末尾调用Halt必要时重新软复位5.2 避坑写Mifare卡数据前的几个检查点写卡操作比读卡更容易出问题。写入前一定要先读一遍目标块确认当前数据状态。如果目标块是扇区尾部块也就是存放密钥和控制位的那一块千万不要随便写一旦把控制位改错这张卡就永久废了。我见过不止一个同事因为写错控制位把一整叠测试卡变成砖。另外一个坑是块地址计算。Mifare S50有16个扇区每个扇区4块用户数据区是每扇区的块0、块1、块2块3是尾部块。很多新手把“第几扇区”理解成“第几块”直接拿扇区号去读当然读不出来。换算公式很简单块地址 扇区号 * 4 块内偏移0、1、2是数据块3是尾部块。5.3 天线匹配和读卡距离的经验FM17550读卡模块出厂一般调试过匹配网络读卡距离能到5厘米左右。但如果你自己做天线或者把模块装进金属外壳里读卡距离会明显下降。这时候先不要急着猛调寄存器先把天线匹配电容检查一遍再考虑软件调整。FM17550的RFCfgReg可以调节接收器增益默认值0x48对应一个中等增益。实测下来增大增益对读卡距离有一定改善但不能无脑调大增益太高会让芯片对噪声更敏感反而可能导致卡片靠近时通信不稳定。我调试时习惯每次只改一位记录当前距离和不稳定现象调完再用卡片在不同角度反复验证。金属外壳对天线场影响明显尽量让天线区域避开金属遮挡或者选用铁氧体隔磁片来改善。6. 驱动封装的个人建议这个FM17550读卡程序最后我封装成了几个独立文件底层SPI读写、FM17550寄存器操作、ISO14443A协议层、业务应用层。分层的好处是以后换芯片底层不变只换中间层驱动换主控平台也只改SPI读写部分协议层和业务层能直接复用。我自己的经验是协议层的函数命名要用“硬件能力”而不是“业务场景”比如FM17550_ReadBlock、FM17550_Auth不要写什么FM17550_OpenDoor这种。读卡驱动是给上层业务用的不是给需求文档用的。保持驱动层的纯粹性后续维护起来会轻松很多。最后分享一个个人习惯读卡程序跑通之后不要急着删调试日志。把串口日志级别保留下来比如默认关、调试开。因为读卡这玩意儿受环境影响很大今天在公司桌上好好的明天到现场装在铁壳机柜里可能就会出现时好时坏的现象。这个时候有日志和没有日志排查效率完全是两码事。哪怕只是临时加两行printf也比对着示波器猜半天强得多。本文还有配套的精品资源点击获取
返回列表