简介:面向嵌入式开发者的STM32+PN532 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,毕竟价格便宜、例程多。但实际把两个方案放在一起对比,差异还是挺明显的。
| 对比项 | PN532 | RC522 |
|---|---|---|
| 支持协议 | 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 引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 供电,见下方注意事项 |
| GND | GND | 共地 |
| SDA | PB7 | I2C1 数据线 |
| SCL | PB6 | I2C1 时钟线 |
| RSTO | PA4 | 复位控制,可选 |
| IRQ | PA5 | 中断输出,可选 |
接线前先把 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 之间的通信是典型的“命令-响应”式,每一帧数据长这样:
| 字段 | 值/说明 |
|---|---|
| Preamble | 0x00 |
| Start Code | 0x00 0xFF |
| LEN | TFI + PDU 长度 |
| LCS | LEN 的补码(取反加1) |
| TFI | 0xD4 表示主机→PN532,0xD5 表示 PN532→主机 |
| PDU | 指令码 + 参数 |
| DCS | TFI + PDU 所有字节和的补码 |
| Postamble | 0x00 |
比如最常见的获取固件版本指令 GetFirmwareVersion,完整帧是:
00 00 FF 02 FE D4 02 2A 00拆开看就是:前导 00 00 FF,LEN=0x02(后面 D4 和 02 两个字节),LCS=0xFE 是 0x02 的补码,TFI=0xD4 代表主机发送,PDU=0x02 是 GetFirmwareVersion 指令,DCS=0x2A 是 0xD4+0x02=0xD6 的补码,最后补一个后导 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 寻卡与读取 UID
PN532 的寻卡指令是 InListPassiveTarget,指令码 0x4A,参数包括最大目标数和通信速率。扫描 Mifare 卡时速率选 106kbps:
uint8_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 i=0; i<uid_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.3V,GND 是否和 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,但有些批次或二手的卡会被改写密钥。排查思路是换一张全新白卡测试,排除卡的问题。另外确认认证命令用的是 0x60(KeyA)还是 0x61(KeyB),两者密钥位置不同,用错就会返回失败状态码。
5.5 STM32F1 硬件 I2C 卡死无响应
这个问题我单独拿出来写,因为它太经典了。F1 系列的硬件 I2C 在配合高频外部干扰、长杜邦线、供电波动时,偶尔会卡在 HAL_I2C_Master_Transmit 里等不到事件标志。处理方案有三个:一是所有 I2C 函数都加上超时时间;二是把 I2C 速率拉低到 100kHz;三是干脆放弃硬件 I2C,用两个 GPIO 引脚写软件模拟 I2C。实测下来,GPIO 模拟 I2C 虽然占用一点 CPU,但稳定性反而更好,临时验证方案时非常实用。
最后再分享一个小技巧:如果只想快速验证 PN532 模块和卡片好不好用,可以先用电脑连一个读卡器模块或者手机 NFC 工具把卡读一遍,确认卡本身正常,再回头查 MCU 代码。这样能省掉很多“明明卡坏了还以为是代码问题”的无效调试时间。这次 demo 跑通之后,我又把状态机里的读写逻辑单独抽了出来,后续接指纹模块做组合门禁,或者加 ESP8266 做数据上云,都不用改底层帧协议代码。
本文还有配套的精品资源,点击获取