news 2026/9/12 10:28:08

FM175XX SPI读写Mifare卡:从UID读取到数据块写入的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 0(CPOL=0,CPHA=0)和Mode 2(CPOL=1,CPHA=0),工程默认采用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引脚容易被人忽略。上电后要拉低再拉高,高电平持续至少1ms,FM175XX内部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请求卡进入场,返回卡类型ATQA
PcdAnticoll0x93防冲突,读取4字节UID
PcdSelect0x60(SAK)选中卡片,主机传入UID
PcdAuth0x60/0x61认证A密钥或B密钥
PcdRead0xB0读16字节块数据
PcdWrite0xA0写16字节块数据

注意PcdRequest的0x52是FM175XX的请求命令寄存器值,和ISO14443-A协议层的REQA(0x26)是两回事,前者是芯片级的控制字,后者是芯片转发到空口的帧内容。工程里封装这些命令时,要区分清楚哪个值写给控制寄存器、哪个值由芯片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-63),key 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.uvproj(Keil 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外设的时钟速度超过10Mbps,FM175XX的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绑定。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 10:27:37

农业AI成熟度检测系统:YOLO多版本调度与SpringBoot工程化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 10:25:43

Java集合框架核心解析与实战应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 10:25:40

MicroDuck:面向具身智能的静态可验证边缘运行时

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 10:25:38

夜间行车安全:后视镜防眩光技术与升级方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 10:25:25

ClickHouse高可用集群架构设计与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华