FM175XX SPI读写Mifare卡:从UID读取到数据块写入的完整实现
简介面向NFC与嵌入式开发者的FM175XX Mifare卡读写工程源码包基于复旦微FM175XX系列芯片通过SPI接口与Mifare卡通信实现卡片ID读取及数据写入适配门禁、支付等非接触式应用场景。压缩包内共56个文件约239KB以C源码、头文件、Keil工程文件uvproj/uvopt为核心辅以hex固件、lst/map编译列表及obj目标文件工程结构完整可直接在Keil中打开编译验证。该资源已有453人学习内容覆盖FM175XX的寄存器初始化、SPI通信时序、Mifare卡认证与读写流程并包含调试输出文本等辅助资料方便对照排查。资源以实际可运行工程形式提供有助于开发者快速验证FM175XX读写功能并基于此扩展自定义应用。适合正在学习NFC底层驱动或需要快速搭建读卡器原型的嵌入式工程师可作为课程设计、毕设或产品预研的参考模板。1. 为什么门禁和储物柜项目里都在用FM175XX做Mifare卡读写嵌入式开发里有一种很常见的回路甲方给了一沓Mifare卡要求设备读出卡号、往卡里写用户数据。直接用NXP的RC522当然能行但遇到成本压力或交付周期时复旦微FM175XX就是最常被拉来做替换的选项。这个工程解压后就是一个完整的Keil工程FM175X.uvproj代码把FM175XX通过SPI接口连到MCU实现了Mifare Classic卡的寻卡、防冲突读UID、密钥认证和块数据读写。也就是说拿这套代码在开发板上跑通串口就能看到卡片ID再往下就能写入数据。适合手里正好有FM175XX模块、想把读写卡功能快速落地的人也适合想搞懂NFC读卡器内部命令交互的工程师。Mifare非接触式卡片工作在没有电源的13.56MHz场里底层数据帧由FM175XX硬件处理MCU只负责通过SPI发送命令字和读取FIFO这也是这套方案很容易上手的根本原因。2. FM175XX的SPI链路与Mifare Classic命令体系2.1 为什么选SPI主机接口FM175XX支持SPI、UART、I2C三种主机通信方式。这个工程从工程名到代码都锁定了SPI选型逻辑很实际SPI全双工、速率可以跑到10Mbps量级一次命令交互通常只有几十字节实时性和代码量都优于UART和I2C。UART要约定波特率I2C要处理从机地址和ACK位SPI只需要片选加4根线时序也直观。FM175XX的SPI从机模式支持Mode 0CPOL0CPHA0和Mode 2CPOL1CPHA0工程默认采用Mode 0即空闲时钟为低、数据在第一个边沿采样。和普通SPI Flash不同FM175XX对片选非常敏感NSS必须在整帧命令地址字节、数据字节、CRC期间保持低电平中途拉高会被视为命令终止。所以工程里一般不用MCU的硬件NSS自动片选而是用普通GPIO软件控制。硬件连接参考下表FM175XX引脚MCU引脚方向说明SDA/NSSGPIO输出片选低有效整帧保持SCKSPI_SCK输出SPI时钟空闲低电平MOSISPI_MOSI输出主机到FM175XX数据MISOSPI_MISO输入FM175XX到主机数据RSTGPIO输出复位低电平有效IRQGPIO输入中断输出轮询模式可悬空这个表格里的RST引脚容易被人忽略。上电后要拉低再拉高高电平持续至少1msFM175XX内部PLL才锁得住如果跳过这一步写寄存器经常表现为写不进去、读回来全是0xFF。IRQ在纯轮询模式下可以不接但在低功耗场景建议接上让FM175XX在检测到卡片时主动拉高唤醒MCU。2.2 SPI命令帧的发送与读取FM175XX的SPI接口协议分两个阶段地址阶段和数据阶段。主机先发一个字节高5位是寄存器地址低3位是命令模式读或写然后才传数据。下面这段是工程里最底层的收发函数也是连接HAL库和FM175XX驱动之间的桥。// fm175xx_spi_rw: 向FM175XX发送一帧命令 // 参数addr 寄存器地址含读写标志buf 数据缓冲区len 数据长度 // 返回最后一个响应字节用于快速判断 uint8_t fm175xx_spi_rw(uint8_t addr, uint8_t *buf, uint8_t len) { uint8_t i; FM_NSS_LOW(); // 软件拉低片选开始帧 spi_read_write(addr); // 发地址字节进入数据阶段 for (i 0; i len; i) { buf[i] spi_read_write(buf[i]); // 读写数据MISO在SCK驱动下返回 } FM_NSS_HIGH(); // 拉高片选结束帧 return buf[len - 1]; }这里最关键的是spi_read_write的用法它是全双工的每调用一次MOSI发一个字节同时从MISO读回一个字节。读FM175XX寄存器时MOSI上可以发0x00纯粹为了产生SCK时钟写寄存器时MOSI发的数据就是要写入的值。工程里常见的一个错误是在读响应阶段把MOSI拉死导致SCK没有时钟沿MISO永远读不到数据。另一个注意点是addr参数的高位要区分读写FM175XX寄存器空间里读命令和写命令的地址偏移不一样通常读用0x00开头、写用0x80开头具体以datasheet的寄存器映射表为准。2.3 Mifare Classic的完整通信流程Mifare Classic卡的访问流程可以拆成五个环节。第一步是请求REQA主机通过FM175XX发送0x26卡片进入场并应答ATQA第二步是防冲突ANTICOLLISION发送0x93命令卡片返回4字节UID加1字节BCC校验第三步是选择SELECT发送0x70命令传入UID卡片返回SAK确认类型第四步是认证指定要读写的扇区并校验密钥第五步才是真正的读写操作。FM175XX把ISO14443-A物理层的编解码、奇偶校验和CRC全部在硬件里完成MCU侧只需要关心这几步命令字的收发。常用命令字和用途如下表FM175XX命令命令字说明PcdRequest0x52请求卡进入场返回卡类型ATQAPcdAnticoll0x93防冲突读取4字节UIDPcdSelect0x60SAK选中卡片主机传入UIDPcdAuth0x60/0x61认证A密钥或B密钥PcdRead0xB0读16字节块数据PcdWrite0xA0写16字节块数据注意PcdRequest的0x52是FM175XX的请求命令寄存器值和ISO14443-A协议层的REQA0x26是两回事前者是芯片级的控制字后者是芯片转发到空口的帧内容。工程里封装这些命令时要区分清楚哪个值写给控制寄存器、哪个值由芯片FIFO发到天线。搞混的话常见现象是寻卡时FM175XX状态寄存器一直报超时而代码逻辑看不出任何问题。3. 核心代码拆解从读UID到写数据块3.1 读卡ID的实现打开工程后能看到的第一个完整逻辑是从卡片读UID。第2章说过读UID只需要请求和防冲突两步。下面的代码来自工程中fm175xx.c的封装去掉了无关日志保留了核心流程。// get_card_uid: 读取Mifare卡的4字节UID // 参数uid_out 输出缓冲区长度至少4字节 // 返回0成功非0为错误码 uint8_t get_card_uid(uint8_t *uid_out) { uint8_t status; uint8_t atqa[2]; // 卡类型应答此处只做校验用 status fm175xx_request(atqa); // 发送REQA并等待ATQA if (status ! STATUS_OK) { return ERR_NO_CARD; // 超时或场内无卡 } status fm175xx_anticoll(uid_out); // 发送0x93防冲突命令 if (status ! STATUS_OK) { return ERR_ANTICOLL; // 多卡冲突或通信干扰 } return STATUS_OK; }在STM32的轮询主循环里调用这个函数时每50ms调用一次就能稳定拿到卡片UID。fm175xx_anticoll内部做的事情是把0x93写入FIFO启动FM175XX发送防冲突命令然后从FIFO读回5个字节。这5个字节里前4个是UID第5个是BCC校验BCC应该等于前4字节的按位异或。我在调试时会把BCC也打印出来如果它和计算值对不上说明天线附近有金属干扰或者卡片没有完全贴合天线区域。3.2 认证扇区密钥读写Mifare Classic的数据块之前必须先对块所在的扇区做密钥认证。每个扇区有独立的A密钥和B密钥各6字节。认证命令需要指定三种信息用A密钥还是B密钥、扇区内的块地址、6字节密钥。下面是一个封装好的认证函数。// mifare_auth: 对指定块做密钥认证 // 参数block 块号0-63key 6字节密钥uid 4字节卡UID // 返回STATUS_OK或错误码 uint8_t mifare_auth(uint8_t block, uint8_t *key, uint8_t *uid) { uint8_t status; uint8_t auth_mode 0x60; // 0x60用A密钥0x61用B密钥 status fm175xx_auth(auth_mode, block, key, uid); if (status ! STATUS_OK) { // 认证失败常见原因 // 1. 密钥不正确 // 2. block地址不属于有效扇区 // 3. UID和卡不匹配防冲突读到的是别的卡 } return status; }fm175xx_auth的最后一个参数uid很重要。Mifare Classic的认证算法是挑战-响应模式卡片端会把UID作为随机数生成的种子之一所以FM175XX在做认证前必须知道当前选中卡的完整UID。如果认证时报状态寄存器错误先别急着怀疑密钥检查一下传给fm175xx_auth的UID是不是和防冲突阶段读到的一致。工程里我见过最典型的错误是缓存了第一张卡的UID然后对第二张卡直接做认证结果两张卡UID不同认证永远失败。3.3 读块与写块认证通过后读写块就简单了。读一块就是把块地址发给FM175XX然后从FIFO里取16字节数据写一块则要先发写命令再连续送16字节。Mifare Classic的数据块是16字节扇区最后的块是尾部块Sector Trailer存的是密钥和访问位直接对这个块做读操作会得到密钥密文普通应用不会去碰它。// mifare_write_block: 写一个数据块 // 参数block 块号data 指向16字节数据缓冲区 // 返回STATUS_OK或错误码 uint8_t mifare_write_block(uint8_t block, uint8_t *data) { uint8_t status; uint8_t key[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // 出厂默认密钥 status mifare_auth(block, key, g_card_uid); // 先认证 if (status ! STATUS_OK) { return status; } status fm175xx_write_block(block, data); // 发送PcdWrite命令 if (status ! STATUS_OK) { return ERR_WRITE; // 写失败多为卡片不在场 } return STATUS_OK; }生产环境里绝对不要继续使用这个0xFF的默认密钥。Mifare Classic的密钥存储区就在每个扇区的尾部块里用默认密钥登录后任何人都能把A密钥改成别的值改完再丢就找不回来了。项目里建议的做法是把密钥编译进固件同时把卡片出厂默认密钥的事写进产品说明书让用户在部署后自行改密。fm175xx_write_block内部会处理PcdWrite命令的收发和CRC校验主机只需要保证data指向的缓冲区有16字节不要传一个不足16字节的数组否则FIFO会写入半帧卡片拒绝响应状态寄存器报格式错误。3.4 把流程串起来的轮询状态机实际工程里不会单独调用上面任何一个函数而是把它们放在一个状态机里。读卡循环的逻辑大致是请求卡成功后就防冲突取UID判断这张卡是不是已经在缓存表里如果是就直接查数据库不是再做一次完整认证并读取需要的块。这样处理的好处是卡片停留在天线区域期间不需要重复做防冲突和认证减少单次交互时间间接降低卡片功耗和读卡冲突概率。// 主循环每50ms调用一次 void card_polling_task(void) { uint8_t uid[4]; uint8_t block_data[16]; if (get_card_uid(uid) ! STATUS_OK) { g_card_present 0; return; } if (memcmp(uid, g_last_uid, 4) 0 g_card_present) { return; // 同一张卡跳过重复认证 } memcpy(g_last_uid, uid, 4); g_card_present 1; if (mifare_auth(1, g_app_key, uid) STATUS_OK) { fm175xx_read_block(1, block_data); // 读业务数据块 } }这个循环里有个容易被忽视的细节get_card_uid成功读回UID后要立刻memcpy保存因为下一次调用会把缓冲区覆盖。另外卡片离开天线区域后FM175XX不会主动通知MCU需要靠get_card_uid返回超时来清除g_card_present标志。如果漏了这个清除逻辑卡拔走之后系统还认为卡在场上门就一直开着这在门禁场景里是事故级别的bug。4. Keil工程移植与调试实战4.1 工程文件结构与启动顺序解压后看到的工程文件列表并不复杂核心文件是FM175X.uvprojKeil MDK工程、FM175X.uvopt工程选项包含调试器配置以及UART_MSG.txt串口日志输出文件。uvproj是主工程文件双击就能在Keil里打开uvopt属于用户级配置每台电脑的调试器路径可能不同如果Keil打开后报找不到调试器检查这个文件里ULINK或ST-Link的设置。UART_MSG.txt是实测时的串口打印内容对快速验证很有用里面能看到寻卡间隔、UID值和读写结果。文件作用FM175X.uvprojKeil主工程定义源码文件和编译选项FM175X.uvopt调试器与Flash下载配置FM175XX.plg编译器日志前一次构建输出UART_MSG.txt串口调试输出记录了UID和操作结果Inc/头文件目录包含FM175XX驱动和配置宏Code/源码目录fm175xx.c、main.c等启动顺序上main函数里先初始化SystemClock然后配置SPI GPIO和SPI外设紧接着调用fm175xx_init。fm175xx_init内部会做一次软复位拉低RST至少10us拉高后延时1ms再写FM175XX的命令寄存器。这里有个细节如果SPI外设的时钟速度超过10MbpsFM175XX的SPI接口可能产生误码典型表现是工程能编译能下载但运行后串口打印全是乱码。把SPI波特率降到1到5Mbps问题会立刻消失。4.2 移植到其他MCU的SPI底层适配如果你手里的开发板不是工程原生的MCU型号只需要替换SPI底层三个函数即可spi_write_byte、spi_read_write和两个GPIO控制宏。以STM32CubeMX生成的项目为例初始化SPI后底层函数只需要三行代码。// STM32 HAL版SPI底层适配FM175XX uint8_t spi_read_write(uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi2, byte, rx, 1, 100); // 全双工收发 return rx; } void spi_write_byte(uint8_t byte) { HAL_SPI_Transmit(hspi2, byte, 1, 100); // 只发不收 }移植到Linux平台时不需要HAL用SPI dev接口或ioctl即可片选还是用GPIO控制代码逻辑可以复用差异只在这层收发函数。CubeMX里配置SPI时要注意把模式设成Full-Duplex Master、数据宽度8bit、MSB First这些参数不匹配会导致FM175XX收到错误的命令字。工程里FM_NSS_LOW和FM_NSS_HIGH两个宏本质就是GPIO写电平改成任意平台的gpio_set_value就能跑。4.3 常见问题与排查方向实际调试中经常会遇到几种现象原因和排查方向列在下面现象可能原因排查方法读UID超时天线参数不对示波器看TX1/TX2波形调匹配电容寄存器读回0xFFRST复位时序不对拉高RST后延时至少1ms再操作串口乱码SPI波特率过高降到1-5Mbps重试写块失败块号是尾部块避开块3/7/11/15等区域卡片拔出后仍显示在场缺少轮询超时清除按3.4节补状态清除逻辑通信偶发错帧片选用了硬件NSS改成软件GPIO片选天线匹配是个大坑。FM175XX的天线谐振频率必须在13.56MHz附近偏了之后最典型的表现是读卡距离极近或者读卡不稳定。调整时用示波器探头看TX1和TX2的差分波形调节并联的匹配电容让谐振点落在13.56MHz。没有网络分析仪的情况下多数项目是靠读卡距离来粗调距离小于1cm说明天线失谐严重把电容换一个档位再试。天线线圈的形状和匝数也会影响阻抗开发板和量产板的天线不一样时匹配电容基本都要重新调一遍。5. 进阶技巧让读卡更稳、更快、更安全5.1 显式软件片选控制FM175XX的SPI接口对片选的要求和普通外设不同硬件NSS在字节间自动拉高再拉低恰好会打断FM175XX的命令帧。所以我在实际项目中从来不用硬件片选一律用GPIO控制还能顺带在调试时用逻辑分析仪观察片选时序是否正确。// 软件片选宏定义挂到任意GPIO即可 #define FM_NSS_LOW() HAL_GPIO_WritePin(NSS_GPIO_Port, NSS_Pin, GPIO_PIN_RESET) #define FM_NSS_HIGH() HAL_GPIO_WritePin(NSS_GPIO_Port, NSS_Pin, GPIO_PIN_SET)用软件片选配合Mode 0时序再加上前面提到的超时重试逻辑读卡成功率能稳定在99%以上。片选信号从拉低到第一个SCK沿之间至少要留出一个SPI时钟周期的建立时间CubeMX里如果开了极速模式可以在片选拉低后插入一个空循环。5.2 重试与指数退避卡片在场但不稳定时读卡器会频繁收到错误响应。直接连续重发命令不仅无效还会让卡片端状态机混乱。常见的做法是退避重试连续失败2次后把命令间隔从20ms拉长到100ms给卡片的防冲突状态机留出复位时间。Mifare Classic在收到错误命令后会进入异常状态需要重新请求才能恢复指数退避正好利用了这个特性。uint8_t retry_read_uid(uint8_t *uid) { uint8_t retries 0; uint8_t delay 20; while (retries 5) { if (get_card_uid(uid) STATUS_OK) { return STATUS_OK; } delay_ms(delay); delay * 2; // 20 - 40 - 80 - 160 if (delay 200) delay 200; retries; } return ERR_TIMEOUT; }这段代码里retries限制5次避免死循环延时指数增长让卡片场上的电荷有足够时间泄放并自恢复。这里delay的单位是毫秒工程里如果跑的是RTOS建议用osDelay替换delay_ms。5.3 安全加固与卡型选择Mifare Classic的密码学算法是私有的CRYPTO1已被学术界证明存在弱点密钥可能被窃听。因此在门禁、支付等对安全要求高的场景不建议把密钥明文存到卡里更不要默认使用0xFF。FM175XX系列本身支持多种卡型如果业务允许优先选用Mifare Plus或DESFire它们的认证流程更健壮。对存量Classic系统至少做到密钥写死在固件而非Flash参数区、每次交易前重新认证、生产日志里不打印密钥内容。这样做的目的不是教读者破解而是提醒开发者正视已知弱点避免上线后的安全责任事故。最后一个细节Mifare Classic卡的UID并不保证唯一出厂时卡片厂商可能会复刻UID对安全要求严格的系统读取UID后要再读取一个数据块做二次校验确认卡内的业务数据与UID绑定。本文还有配套的精品资源点击获取