ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32+PN532实现NFC读写:从接线到代码的完整实战

STM32+PN532实现NFC读写:从接线到代码的完整实战 简介面向嵌入式开发者的STM32PN532 NFC读写示例工程完整演示了STM32与NXP PN532芯片通过SPI或I2C接口实现非接触式卡片的数据交互适合正在学习NFC协议栈、STM32外设驱动或准备快速搭建门禁、支付类应用原型的开发者。代码工程同时支持IAR与Keil4两种开发环境工程文件与库函数封装齐全便于在不同工具链之间迁移与二次开发。压缩包共585个文件容量8.75MB主要包含C/H源代码、IAR工程配置.ewp/.ewd、Keil工程文件.uvproj以及编译生成的HEX、O、ELF等固件文件另有Android端APK与Java源码、PDF/DOC说明文档和PNG截图可辅助理解完整调用流程。源码中涉及PN532初始化、寻卡、读写MIFARE系列卡片的关键流程以及SPI驱动与HAL/LL库的封装方式通过阅读可以快速掌握NFC卡与STM32通信的底层细节。项目已有3032人学习下载适合作为快速验证NFC功能或移植到其他STM32型号的参考基础。 最近做项目遇到一个很典型的嵌入式需求用 STM32 读写 NFC 卡。手里正好有一块 PN532 模块Mifare 白卡也有好几张索性把“STM32 PN532”这套 NFC 读写 demo 完整跑了一遍从接线到帧协议再到代码实现都捋清楚了。这篇文章就记录这次实操的过程内容包括为什么用 PN532、怎么接线、CubeMX 怎么配置、PN532 帧协议怎么理解、读 UID 和读写 Mifare 数据块的核心代码以及调试期间踩过的坑。适合用 STM32 入门 NFC、或者在项目里临时加读卡功能但还没定方案的朋友参考。1. 项目开始前方案选型与整体思路1.1 为什么选 PN532而不是 RC522不少人在做 NFC 读卡时第一反应是 RC522毕竟价格便宜、例程多。但实际把两个方案放在一起对比差异还是挺明显的。对比项PN532RC522支持协议ISO14443A/B、Felica、Mifare主要是 ISO14443A / Mifare通信接口I2C、SPI、HSU串口SPI / UART板载天线多数模块自带读卡距离约3-5cm部分模块需要自己画天线区域驱动复杂度芯片自带协议栈命令帧简单需要自己处理防碰撞、认证等底层时序价格稍贵便宜RC522 不是不能用只是它的很多底层操作要自己写防碰撞、选卡、认证、读块写块每一步都要按 Mifare 协议来。PN532 的优势在于内置协议栈MCU 只需要通过帧命令告诉它“我要读卡”“我要读哪一块”剩下的奇偶校验、CRC、防碰撞它自己处理。对于 demo 级和产品原型来说PN532 可以省下大量调试时间这也是我这次选它的核心原因。1.2 demo 要实现的四个功能点这次 demo 的功能定位非常简单明确总共四件事上电后读取 PN532 的固件版本确认 I2C 通信链路正常。轮询检测进入天线区域的 NFC 卡片。读出卡片 UID 并打印出来。对 Mifare Classic 卡S50的某个数据块执行认证、读、写操作。功能 1 和 2 是验证“通没通”功能 3 是读卡最常用场景功能 4 是很多实际应用门禁、会员卡、标签存储数据的基础。把这四步跑通了后面不管接指纹模块、蓝牙模块还是做上位机逻辑都不会变。2. 硬件准备与接线细节2.1 材料清单这次用的都是一些常见器件STM32F103C8T6 最小系统板一块蓝色板子那种就行。PN532 NFC 模块一个带天线的那种蓝色模块。Mifare S50 白卡一张就是 IC 卡常用的那种。杜邦线若干。USB 转 TTL 模块一个用于查看串口打印信息。这里有个建议卡不要只备一张。实际调试时你会发现有的卡出厂密钥不是全 FF有的卡不支持标准 S50 指令多备一两张白卡能快速区分“代码问题”和“卡的问题”。2.2 PN532 与 STM32 的 I2C 接线PN532 模块支持 I2C、SPI、HSU 三种接口这次 demo 走 I2C原因很简单连线少、代码量适中而且 PN532 的 I2C 时序不算复杂。接线如下PN532 引脚STM32F103C8T6 引脚说明VCC3.3V供电见下方注意事项GNDGND共地SDAPB7I2C1 数据线SCLPB6I2C1 时钟线RSTOPA4复位控制可选IRQPA5中断输出可选接线前先把 PN532 模块上的 DIP 拨码开关切到 I2C 模式。不同品牌的模块丝印不一样但一般会标注 I2C/HSU/SPI 的位置按丝印拨到 I2C 即可。模块出厂默认可能就是 I2C但还是检查一下别在这一步浪费一晚上。2.3 供电和电平的几个关键注意点PN532 模块标称电压是 3.3V-5V很多模块上也有 5V 供电跳线但我强烈建议 VCC 接 3.3V而不是 5V。原因是 STM32 的 PB6/PB7 是 3.3V 电平如果模块上的 I2C 上拉电阻接的是 VCC而 VCC 是 5V那 SDA/SCL 闲置时会被拉到 5V直接怼进 STM32 的引脚虽然 STM32 的引脚大多耐压但长期跑并不安全。VCC 接 3.3V 是最稳妥的做法。另外I2C 总线上需要上拉电阻。大部分 PN532 模块板上已经配了上拉不需要再额外加如果买的是纯裸芯片自己搭电路记得在 SDA 和 SCL 上各接一个 4.7kΩ 电阻到 3.3V否则 I2C 通信会非常不稳定。RSTO 和 IRQ 两个引脚在最小 demo 里可以不接PN532 上电默认就运行。但接上更好后面想自己做低功耗唤醒或者看中断状态时会方便很多。3. 基础框架CubeMX 配置与协议理解3.1 STM32CubeMX 初始化配置打开 STM32CubeMX建立基于 STM32F103C8T6 的工程需要配置的内容如下RCC选择 HSE 外部晶振Clock Configuration 里把系统时钟设到 72MHz。I2C1选择 I2C 模式把速度降到 100kHz。Standard Mode 即可不用 Fast Mode。USART1异步模式115200-8-N-1用于打印调试信息。如果接了 RSTO 和 IRQ把 PA4、PA5 配置为 GPIO 输出/输入。生成工程时记得选 HAL 库。关于 I2C 速率这里特意提一句STM32F1 系列的硬件 I2C 口碑确实一般很多人调不通就是卡在这里。如果把速率降到 100kHz 还是不稳定可以直接改用 GPIO 模拟 I2C反正 PN532 的 I2C 时序非常标准网上也能找到现成的模拟 I2C 代码后面我会在问题排查部分再展开讲。3.2 必须看懂的 PN532 帧协议PN532 和 MCU 之间的通信是典型的“命令-响应”式每一帧数据长这样字段值/说明Preamble0x00Start Code0x00 0xFFLENTFI PDU 长度LCSLEN 的补码取反加1TFI0xD4 表示主机→PN5320xD5 表示 PN532→主机PDU指令码 参数DCSTFI PDU 所有字节和的补码Postamble0x00比如最常见的获取固件版本指令 GetFirmwareVersion完整帧是00 00 FF 02 FE D4 02 2A 00拆开看就是前导 00 00 FFLEN0x02后面 D4 和 02 两个字节LCS0xFE 是 0x02 的补码TFI0xD4 代表主机发送PDU0x02 是 GetFirmwareVersion 指令DCS0x2A 是 0xD40x020xD6 的补码最后补一个后导 0x00。理解这个帧结构很重要因为后面所有操作——读 UID、认证、读写块——都是在这个帧格式上换 PDU 而已。说白了PN532 就是一个很听话的从机你把指令打包好发给它它就把结果打包好回给你。3.3 底层收发函数实现先封装两个基础函数发送命令帧、读取响应帧。#define PN532_I2C_ADDR 0x48 #define PN532_PREAMBLE 0x00 #define PN532_STARTCODE1 0x00 #define PN532_STARTCODE2 0xFF #define PN532_HOST_TO_PN532 0xD4 #define PN532_PN532_TO_HOST 0xD5 uint8_t PN532_SendCommand(uint8_t cmd, uint8_t *params, uint8_t params_len) { uint8_t frame[64]; uint8_t len params_len 2; // TFI cmd params uint8_t checksum PN532_HOST_TO_PN532 cmd; uint8_t idx 0; frame[idx] PN532_PREAMBLE; frame[idx] PN532_STARTCODE1; frame[idx] PN532_STARTCODE2; frame[idx] len; frame[idx] ~len 1; frame[idx] PN532_HOST_TO_PN532; frame[idx] cmd; for (uint8_t i 0; i params_len; i) { frame[idx] params[i]; checksum params[i]; } frame[idx] ~checksum 1; frame[idx] 0x00; return HAL_I2C_Master_Transmit(hi2c1, PN532_I2C_ADDR 1, frame, idx, 1000); }读取响应的逻辑类似先接收前 6 个字节拿到帧头、LEN、LCS校验无误后再继续读 LEN 个数据字节最后校验 DCS。把这两个函数写好后面的功能代码就都建立在它们之上。4. 功能实现从读 UID 到数据块读写4.1 上电初始化获取固件版本与 SAMConfig程序启动后第一步不是直接读卡而是先确认 PN532 有没有正常响应。调用 GetFirmwareVersion 指令uint8_t PN532_GetFirmwareVersion(uint8_t *buf) { if (PN532_SendCommand(0x02, NULL, 0) ! HAL_OK) return 0; // 读取响应并解析响应中包含 IC、版本、支持协议等信息 }如果 I2C 接线没问题串口能打印出类似PN532 FW Version: 1.6的信息这时候才能继续往下走。紧接着还需要发一个 SAMConfig 指令参数是0x01作用是让 PN532 进入正常的读写卡模式。这一步漏掉的话后面寻卡命令经常返回异常属于隐藏的坑。uint8_t cfg[1] {0x01}; PN532_SendCommand(0x14, cfg, 1); // SAMConfiguration4.2 寻卡与读取 UIDPN532 的寻卡指令是 InListPassiveTarget指令码 0x4A参数包括最大目标数和通信速率。扫描 Mifare 卡时速率选 106kbpsuint8_t PN532_ReadUID(uint8_t *uid, uint8_t *uid_len) { uint8_t cmd[2] {0x01, 0x00}; // 最多1张卡106kbps uint8_t resp[32]; uint8_t resp_len; if (PN532_SendCommand(0x4A, cmd, 2) ! HAL_OK) return 0; if (!PN532_ReadResponse(resp, resp_len)) return 0; // 响应格式: 0xD5 0x4B [NbTg] [Tg] [NFCID Length] [NFCID...] *uid_len resp[5]; for (uint8_t i 0; i *uid_len; i) { uid[i] resp[6 i]; } return 1; }完整响应帧解析时需要根据实际长度调整偏移但整体结构就是上面这样。能读到 UID说明天线、射频、寻卡流程全部正常。4.3 Mifare Classic 认证与块读写Mifare S50 卡片数据区分为 16 个扇区每个扇区 4 个块每块 16 字节。每个扇区的第 3 块是扇区尾部存放 KeyA、访问位和 KeyB。读写数据块之前必须先使用密钥进行认证。认证、读块、写块都是通过 InDataExchange 指令0x40发送底层 Mifare APDU 完成。认证命令uint8_t block 0x01; uint8_t key[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // 默认KeyA uint8_t cmd[9] {0x01, 0x60, block, key[0], key[1], key[2], key[3], key[4], key[5]}; PN532_SendCommand(0x40, cmd, 9); // 0x60 使用KeyA认证认证通过后读块指令uint8_t cmd[3] {0x01, 0x30, block}; // 0x30 READ PN532_SendCommand(0x40, cmd, 3);读回来的响应就是 16 字节的块数据。写块指令类似把 0x30 换成 0xA0后面跟上 16 字节目标数据uint8_t cmd[19] {0x01, 0xA0, block, d0, d1, ... d15}; PN532_SendCommand(0x40, cmd, 19);这里有一个容易忽略的细节Mifare 卡的块 0 存放厂商代码和 UID不能改扇区尾部用来存密钥和访问位也不能随便当普通数据块写。demo 里我选的是扇区 0 的块 1也就是地址从 0x01 开始这是安全且常见的演示位置。4.4 主循环状态机设计主循环不要写成“读一次就结束”而是用状态机轮询这样更贴近实际产品逻辑while (1) { switch(state) { case WAIT_CARD: if (PN532_ReadUID(uid, uid_len)) { printf(UID: ); for (int i0; iuid_len; i) printf(%02X, uid[i]); printf(\r\n); state CARD_FOUND; } break; case CARD_FOUND: if (auth_block(0x01, key)) { read_block(0x01, data); data[0] (data[0] 1) 0xFF; // 简单示例每轮读后改一字节再写回 write_block(0x01, data); } state WAIT_CARD; HAL_Delay(500); break; } }轮询间隔 500ms 足够既不会太吃 CPU也能保证卡片放上去后半秒内响应。实际产品如果对功耗有要求可以改成 PN532 的 IRQ 引脚唤醒 MCU这套状态机的核心逻辑不用变。5. 调试中遇到的高频问题与排查技巧5.1 卡片到底应该怎么放听起来很基础但真的会卡住新手。PN532 模块的天线是 PCB 上的一部分感应的有效区域比较小不是手机 NFC 背面那种大面积。正确做法是把卡平放在模块天线区域中央保持贴合不要悬空也不要来回晃动。模块读卡时通常会点亮一个 LED 或者发出蜂鸣声以这个作为放置成功的反馈。5.2 I2C 一直无响应或收到全 FF先检查三件事PN532 模块的拨码开关是否拨到 I2C 位置。VCC 是不是 3.3VGND 是否和 STM32 共地。SDA/SCL 是否接反。排除硬件后再用 I2C 扫描代码看 0x48 地址上有没有 ACK。如果扫描不到很可能是模块压根没进 I2C 模式或者供电异常如果 I2C 能扫描到但通信不稳定尝试把 I2C 时钟从 400kHz 降到 100kHz。5.3 烧录时报 no stm32 target found 或 Debug Authentication 错误这个报错属于 ST-Link 连接问题不是 PN532 的锅但调试过程里很可能混在一起出现。典型原因包括SWD 线接触不良、芯片开启了读保护、代码把 SWD 引脚复用成了普通 GPIO。处理办法是按住 STM32 的复位键不放点击烧录出现下载进度后再松开复位如果还不行把 BOOT0 拉高再上电进入系统存储器模式先擦除整个 Flash 再恢复正常下载。5.4 认证一直失败认证失败 90% 是密钥不匹配。Mifare 出厂白卡的默认 KeyA/KeyB 通常是全 FF但有些批次或二手的卡会被改写密钥。排查思路是换一张全新白卡测试排除卡的问题。另外确认认证命令用的是 0x60KeyA还是 0x61KeyB两者密钥位置不同用错就会返回失败状态码。5.5 STM32F1 硬件 I2C 卡死无响应这个问题我单独拿出来写因为它太经典了。F1 系列的硬件 I2C 在配合高频外部干扰、长杜邦线、供电波动时偶尔会卡在 HAL_I2C_Master_Transmit 里等不到事件标志。处理方案有三个一是所有 I2C 函数都加上超时时间二是把 I2C 速率拉低到 100kHz三是干脆放弃硬件 I2C用两个 GPIO 引脚写软件模拟 I2C。实测下来GPIO 模拟 I2C 虽然占用一点 CPU但稳定性反而更好临时验证方案时非常实用。最后再分享一个小技巧如果只想快速验证 PN532 模块和卡片好不好用可以先用电脑连一个读卡器模块或者手机 NFC 工具把卡读一遍确认卡本身正常再回头查 MCU 代码。这样能省掉很多“明明卡坏了还以为是代码问题”的无效调试时间。这次 demo 跑通之后我又把状态机里的读写逻辑单独抽了出来后续接指纹模块做组合门禁或者加 ESP8266 做数据上云都不用改底层帧协议代码。本文还有配套的精品资源点击获取
返回列表