PN532 这个芯片,玩物联网和嵌入式开发的应该都不陌生。不管你是做 NFC 门禁、校园卡模拟、公交卡读取,还是想给 Arduino、STM32、树莓派加上刷卡功能,基本绕不开它。我最早接触 PN532 是在做一个小型读卡器项目,当时手里同时有 I2C、SPI 和 HSU 三种接口的模块,本来以为换个接口只是改几根线的事,结果踩了一堆坑,从时序到配置挨个折腾了一遍。这篇文章就把我实际试过的经验整理出来,重点讲清楚 I2C、SPI、HSU 三种协议在 PN532 上的差异、初始化细节、代码实现和排查方法,适合刚接触 NFC 的开发者,也适合准备把 PN532 集成到自己项目里、但不确定选哪种接口的人。
先给还没上车的朋友补一句背景:PN532 是 NXP 推出的一颗 NFC 芯片,支持读卡、写卡、卡模拟、点对点通信。它最有意思的地方是主机接口很灵活,既可以接 I2C,也可以接 SPI,还可以接 HSU(其实就是一个高速 UART)。这意味着同一个芯片能适配不同主控的资源——有的主控硬件 I2C 不够用,有的 SPI 引脚被占满了,还有的老单片机只有串口,PN532 三种接口都能接,就很香。
不过接口多了也有烦恼。很多芯片的接口在出厂后就固定了,PN532 却是上电时通过引脚电平来判断当前走哪种协议。如果引脚下拉或者上拉没处理好,时序模块初始化得再对也没用。这篇文章我会从硬件选型、引脚配置、通信帧格式,到实际代码一步步拆开,最后再附上我调试时经常遇到的几个诡异问题。
1. 三种通信接口的硬件设计与选型思路
1.1 I2C、SPI、HSU 的本质区别
先明确一个概念:这三种接口都是 PN532 和主控之间通信的“通道”,不是 NFC 无线通信本身。PN532 通过天线和外面的卡片通信走的是 13.56MHz 射频,而主控发指令给 PN532 则是走 I2C、SPI 或者 HSU。所以上层代码逻辑基本一样,变的只是传输层。
I2C 是两根线(SCL、SDA),半双工,速度一般跑 100kHz 或 400kHz。因为 I2C 是开漏输出加外部上拉电阻,所以它天然支持多设备挂一条总线,适合主控引脚紧张、总线上还有其他传感器的场景。但速度不算快,如果你要频繁读取大块数据,比如从 NFC 标签里连续读几百字节,I2C 会比其他两种慢不少。
SPI 是四根线(SCLK、MOSI、MISO、CS),全双工,速率可以跑到几 MHz,是三种接口里速度最快的。它适合对吞吐量有要求、需要大量数据交换的场景。缺点是多占引脚,而且一个 SPI 总线上一般用片选引脚来区分设备,占用的 CS 引脚一多,优势就没那么明显了。
HSU 就是 UART,只是 PN532 给它起名叫 High Speed UART,默认波特率是 115200,8 数据位、无校验、1 停止位。串口的好处是几乎所有单片机都有,调试也方便,用 USB 转串口模块就能直接跟电脑通信。缺点也是半双工,速率受波特率限制,跟 SPI 比起来传输大块数据时会慢一些。
下面这张表是我整理的三者对比:
| 接口 | 引脚数 | 速率 | 工作方式 | 典型场景 |
|---|---|---|---|---|
| I2C | 2 | 100k/400kHz | 半双工,总线式 | 引脚紧缺,总线已有多设备 |
| SPI | 4 | 最高数MHz | 全双工,主从式 | 大数据量交互,对速度敏感 |
| HSU | 2 | 默认115200bps | 半双工,点对点 | 主控仅有串口,调试方便 |
选哪种接口不是拍脑袋定的,我一般按这个顺序来考虑:先看主控还有什么外设可用,再看通信数据量大小,最后考虑 PCB 布线和模块兼容性。
1.2 PN532 上电接口检测原理
PN532 有一个很容易忽略的机制:它在复位上电的时候,会去采样两个引脚的电平,用这两个引脚来决定后续主机接口走 I2C、SPI 还是 HSU。这两个引脚在不同模块上可能标注不太一样,但原理是一样的。
以常用的模块为例,I0 和 I1 的电平组合决定了接口模式:
| I0 | I1 | 通信接口 |
|---|---|---|
| 0 | 0 | I2C |
| 1 | 0 | SPI |
| 0 | 1 | HSU |
| 1 | 1 | 串行调试模式(一般不用于正常通信) |
电平 0 表示接地,电平 1 表示接高电平或通过上拉电阻接 VCC。这个配置必须在芯片上电之前就稳定下来,因为芯片是在复位释放后的很短时间内采样这两个引脚。
我看到不少新手在调试 SPI 的时候,模块明明标着 SPI 接口,但代码怎么调都没反应。最后用万用表一量,发现 I0 引脚是被模块内部下拉到地的,等于芯片一直工作在 I2C 模式,主控却往 SPI 引脚上发数据,自然石沉大海。所以拿到一个模块,第一件事不是写代码,而是确认板上 I0/I1 的默认电平,或者看模块原理图确认出厂模式。
如果你的模块上这两个引脚引出到排针,那就好办,可以通过跳线帽或者直接焊接选择模式。如果没有引出,那基本是出厂固定了,只能按模块标注的接口来用。我自己手头有几个红色 PCB 的 PN532 模块,它们默认是 HSU 模式,但也把 I2C 和 SPI 引脚都引出来了,要切换到别的模式就得自己改跳线。
1.3 硬件接线和电平匹配
选好接口之后,接线看起来很简单,但有几个隐藏细节特别容易出问题。
第一是电源。PN532 数字部分和天线驱动部分需要 3.3V 供电,注意尽量用稳压芯片,不要直接从主控的 3.3V LDO 上取电,因为天线发射时电流波动比较大,有可能把主控电压拉低,导致复位或者不稳定。我在做第一版电路时直接把 PN532 的 VCC 接到了 STM32 的 3.3V 上,结果读卡的时候屏幕偶尔闪一下,后来查到就是电源瞬态干扰。
第二是电平匹配。如果你的主控是 5V 单片机,比如 Arduino Uno 这类,直接跟 3.3V 的 PN532 通信,I2C 和 UART 电平不匹配,长期使用或者某些引脚上电瞬间,有概率把芯片搞坏。最稳妥的做法是加一个电平转换模块。如果是 I2C,还要注意两边上拉电阻都挂在 3.3V 上,不能挂到 5V。
第三是地线。所有通信说到底都是共地问题,主控和 PN532 一定要共地,否则波形就是漂浮的,特别是 I2C 和 UART 这种靠电平判断信号的协议,地电位差会让信号乱成一团。
2. 接口初始化与关键参数配置
2.1 模式切换与引脚配置
不同接口模式下,PN532 的引脚复用不一样,不是说切换成 I2C 之后 SPI 引脚就完全没事了,而是要把不用的引脚处理掉,避免浮动电平干扰芯片内部逻辑。
I2C 模式下,SCL 和 SDA 需要外部上拉电阻,典型值 4.7kΩ,如果总线速率跑 400kHz,也可以用 2.2kΩ 或更低。I2C 地址默认是 7 位地址 0x24,换算成 8 位写地址是 0x48,读地址是 0x49。在很多单片机库里,你看到的地址参数如果是 0x24,通常库内部会自动左移一位;如果是 0x48,那就是直接用了 8 位地址。这个换算容易搞混,最好先查一下自己用的库是怎么处理的。
SPI 模式下,PN532 要求 SPI 模式 0,也就是 CPOL=0、CPHA=0。这意味着时钟空闲时是低电平,数据在时钟上升沿采样。片选引脚 CS 低有效,每次传输开始前下拉,结束后释放。如果主控 SPI 硬件不支持模式 0,可以用软件模拟 SPI,但速度会慢很多。另外 PN532 的 SPI 是只支持从机模式的,主控作为主机来控制通信节奏。
HSU 模式下,波特率默认 115200,这个参数不能随意改,除非你在程序里主动发送波特率切换指令。我自己就干过一件傻事:以为自己用的是通用串口模块,把波特率设成了 9600,结果 PN532 一个字节都不回。后来看数据手册才发现,HSU 默认就是 115200,接收命令前不会自动检测波特率。
2.2 命令帧格式与校验计算
PN532 的通信协议本身和接口无关,不管 I2C、SPI 还是 HSU,传输的数据都是同样格式的帧。理解这个帧格式,排查问题会快很多。
PN532 的命令帧结构是:
- Preamble:固定 0x00
- Start Code:固定 0x00 0xFF
- LEN:数据长度,指 TFI + PD0 + PD1... 这个字段的长度
- LCS:LEN 的校验值,计算方法是对 LEN 取反后加 1,使得 LEN + LCS = 0
- TFI:方向标识,主机发给 PN532 是 0xD4,PN532 返回给主机是 0xD5
- PD0:命令码,比如获取固件版本是 0x02
- PD1...PDn:命令参数
- DCS:数据校验,等于 TFI + PD0 + PD1... 所有数据字节取反加 1
- Postamble:固定 0x00
初次接触时感觉计算校验很麻烦,其实写代码就三行。我一般在发送前这样算:
static uint8_t PN532_Checksum(uint8_t *data, uint8_t len) { uint8_t sum = 0; while (len--) { sum += *data++; } return (uint8_t)(0x00 - sum); }这个方法能算 LCS,也能算 DCS,因为核心思想就是“所有字节求和后低字节为 0”。对应到校验值就是将所有需要参与校验的字节相加,再取反加 1。验证的时候把参与校验的字节和校验值加起来,低字节应该是 0x00。
还有一个细节是:PN532 收到主机命令后,会先回一个 ACK 帧,固定是00 00 FF 00 FF 00。这个 ACK 不代表命令执行完毕,只是告诉主机它收到数据了。真正的响应数据要等芯片内部处理完才会返回,这个间隔有时长有时短,取决于命令复杂度。如果你发送完命令立刻去读响应,很可能读到的是 ACK 甚至什么都没读到,所以驱动代码里一定要有等待机制。
2.3 时序与通信流程
之前我提到接口选择引脚,这里展开讲讲不同接口的时序差异在调试时到底意味着什么。
I2C 通信的时序简单说就是:主机发送起始条件,然后发设备地址和写标志,接着发送帧数据,最后发停止条件。PN532 在 I2C 模式下有点特殊,它处理完命令后,会把数据放到内部的发送缓冲区,主机需要以读方式再发一次设备地址才能把响应读出来。也就是说,I2C 模式下你通常要分两步:先写命令,再读响应。
SPI 的时序相对清爽,主机拉低 CS,然后按字节发送命令帧,PN532 会在 MISO 上回 ACK。注意读完 ACK 后不能马上拉高 CS,要等芯片把响应帧准备好再继续读。如果 CS 控制得太急,PN532 可能根本来不及组装响应。
HSU 最简单,就像普通串口一样,往 TX 发帧,从 RX 收 ACK 和响应。它是最容易忍住想摸逻辑分析仪的接口,但也是最容易犯低级错误的接口,比如波特率配置错、接线接反。
我实际测试下来,三种接口在“获取固件版本”这种简单命令上,速度差距很小,真正拉开差距的是大量读写 NFC 数据时。如果你做的是门禁这种“刷卡-比对-开门”的轻量操作,I2C 完全够了;如果你要快速读写高频标签里的数据,SPI 会明显更跟手。
3. 实战:STM32F103 通过 I2C 读取 PN532 版本信息
3.1 环境准备与接线
理论讲了那么多,还是得上手操作。我选 STM32F103 作为主控,因为它太经典了,网上资料也多,很多人手里都有现成开发板。软件环境用 STM32CubeMX 生成初始化代码,HAL 库操作。
硬件连接(I2C 模式)是这样的:
| PN532 引脚 | STM32 引脚 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SDA | PB7(I2C1_SDA) |
| SCL | PB6(I2C1_SCL) |
| IRQ | 不接(可选) |
我建议接上 IRQ 引脚,后面讲轮询还是中断的问题时会用到。如果模块默认不是 I2C 模式,需要把 I0/I1 都拉低后再上电。
CubeMX 里把 I2C1 打开,速度设 100kHz,其他保持默认。生成代码后,在 main.c 里加上 PN532 驱动。
3.2 I2C 模式核心代码拆解
先写一个最底层的 I2C 写帧函数。这个函数的作用是把前面说的命令帧打包好,然后通过 I2C 发出去。我一般这样做:
void PN532_I2C_WriteCommand(uint8_t *cmd, uint8_t len) { uint8_t frame[64]; uint8_t i = 0; uint8_t checksum; frame[i++] = 0x00; // Preamble frame[i++] = 0x00; // Start Code frame[i++] = 0xFF; // Start Code frame[i++] = len + 1; // LEN: TFI + 命令数据 checksum = 0x00 - (uint8_t)(len + 1); // LCS frame[i++] = checksum; frame[i++] = 0xD4; // TFI,主机到 PN532 for (uint8_t j = 0; j < len; j++) { frame[i++] = cmd[j]; } checksum = 0x00 - 0xD4; for (uint8_t j = 0; j < len; j++) { checksum = (uint8_t)(checksum - cmd[j]); } frame[i++] = checksum; // DCS frame[i++] = 0x00; // Postamble HAL_I2C_Master_Transmit(&hi2c1, 0x48, frame, i, 100); }然后写读取 ACK 和响应帧的函数。PN532 收到命令后,I2C 从机地址 0x48 是写方向,0x49 是读方向。HAL 库里面 HAL_I2C_Master_Receive 传入的地址是 8 位地址,0x49 就代表读方向。
uint8_t PN532_I2C_ReadAck(void) { uint8_t ack[6]; HAL_I2C_Master_Receive(&hi2c1, 0x49, ack, 6, 100); if (ack[0] == 0x00 && ack[1] == 0x00 && ack[2] == 0xFF && ack[3] == 0x00 && ack[4] == 0xFF && ack[5] == 0x00) { return 1; } return 0; }获取固件版本命令是 PD0 = 0x02,无参数,所以 cmd 数组只需要一个字节。完整调用流程是:
uint8_t cmd = 0x02; PN532_I2C_WriteCommand(&cmd, 1); // 稍作延时,等 ACK HAL_Delay(10); if (PN532_I2C_ReadAck()) { // 等待响应就绪 HAL_Delay(10); uint8_t buf[16]; HAL_I2C_Master_Receive(&hi2c1, 0x49, buf, sizeof(buf), 100); // buf 里就是响应帧 }响应帧的格式和命令帧类似,开头也是00 00 FF,然后是长度、TFI=0xD5、PD0=0x03(响应标识)、后面跟 4 个版本信息字节。打印出来就能看到类似 IC 型号、固件版本号这些信息。
提示:HAL_Delay 等待 ACK 和响应是一种简单粗暴但有效的方式,实际项目中我更建议用 IRQ 引脚做中断触发读取。PN532 在没有准备好数据时,IRQ 保持高电平;数据准备完成后 IRQ 拉低,通知主机可以读取。这样既省 CPU,也不会因为延时不够导致读不到数据。
3.3 SPI 与 HSU 模式的移植要点
如果要用 SPI 模式,CubeMX 里把 SPI1 打开,配置为全双工主机、模式 0,速率先设 1MHz。代码框架跟 I2C 基本一样,差别只在底层收发函数。
SPI 写帧时先拉低 CS,然后 HAL_SPI_Transmit,发完后等待芯片回 ACK。这里有个细节,PN532 的 SPI 是全双工,主机发送一个字节的同时会收到一个字节,所以读 ACK 时要用 HAL_SPI_Receive 而不是只读 MISO。很多主控在 SPI 通信时是边发边收,接收的字节可能都是 0xFF 或者乱值,需要根据时序在正确的时间点采样。
如果你想让 SPI 跑得更快,可以开 DMA,HAL_SPI_Transmit_DMA 和 HAL_SPI_Receive_DMA。但要注意 DMA 的缓冲区长度和数据长度要匹配,还有片选信号要自己控制好。我在一个项目里用 STM32F103 的 SPI1 + DMA 读取 PN532,速率提到 2MHz,数据刷新率明显比 I2C 高。
HSU 模式更直接,就是普通的串口收发。CubeMX 里把 USART1 配成 115200、8N1,然后用 HAL_UART_Transmit 发送帧,HAL_UART_Receive 接收数据。代码和 I2C 的差异只在于底层接口函数不一样,帧结构完全复用。比较方便的是,HSU 模式不需要关心 ACK 和响应是否在同一条总线上,因为只要往串口发数据,串口那边就会把芯片返回的内容原样送回来。
有一点要注意:PN532 的 HSU 默认波特率是 115200,但它的 UART 设备在长时间不用时可能会进入低功耗模式,这时候你需要先唤醒它。最简单的方法是在发送正式命令之前,先拉低或拉高某个唤醒引脚(看具体模块定义),或者连续发送几个 0x55 之类的唤醒字节。不同模块实现不太一样,我买过一个模块说明书里专门提到要先发 0x55 唤醒,另一个则不用,直接发指令就行。
3.4 逻辑分析仪怎么看时序
如果你在调试过程中一头雾水,代码怎么改都不通,最有效的手段就是用逻辑分析仪抓波形。一个几十块钱的 8 通道逻辑分析仪,配合电脑软件,可以清晰看到 I2C 的起始条件、地址、数据帧,也能看到 SPI 的时钟和数据是否对齐。
抓 I2C 时要注意,逻辑分析仪探头并联在 SDA 和 SCL 上,本身会给总线增加一点电容,如果上拉电阻比较大,波形边缘可能会变缓。这时候可以先确认软件波形是不是符合预期,再判断是硬件问题还是软件问题。
抓 SPI 时重点看 CS 拉低后到第一个时钟上升沿之间,数据是否已经稳定;再看 MISO 上芯片返回的数据是不是在正确的时钟沿被采样。大多数 SPI 排查到最后发现都是模式配错了,或者速率太高导致信号失真。
实际板子上的波形未必像教科书那样完美,但只要关键转换点正确,逻辑分析仪能解码出正确的数据,通信就是通的。我见过有人过度纠结波形上升沿不够陡,其实 400kHz 的 I2C 在 10cm 杜邦线下有一点缓边沿完全能正常工作。
4. 常见问题与排查技巧实录
4.1 I2C 不通信,八成是上拉和地址问题
I2C 模式最常见的问题就是完全不响应,或者读回来的数据全是 0xFF。这类问题我总结下来,原因集中在三个地方。
第一个是上拉电阻。I2C 是开漏输出,必须有外部上拉电阻才会产生高电平。如果你的开发板上没有上拉,或者上拉电阻太大,比如 10kΩ 以上,再叠加杜邦线和逻辑分析仪的寄生电容,波形高电平可能抬不上去,通信直接失败。我可以负责任地说, 4.7kΩ 是 I2C 最稳妥的起点,如果总线线长超过 20cm 或者挂载多个设备,可以适当减小到 2.2kΩ。
第二个是地址。PN532 默认 I2C 地址是 0x24(7 位地址),但实际代码里写地址时,不同库和平台要求不一样。HAL 库的 HAL_I2C_Master_Transmit 需要传 8 位地址,所以写 0x48;Adafruit 库内部用 7 位地址,写 0x24。我见过有人把两个地址混用,对着 0x48 的库函数写 0x24,结果收发全部错位。
第三个是 SCL 和 SDA 接反了。这种错误在排线比较杂乱时特别容易发生。最好用万用表量一下,确认主控 SCL 对应模块 SCL,主控 SDA 对应模块 SDA,不要想当然。
4.2 SPI 通信不生效,先查模式和片选
SPI 的坑主要在两个方面。
一个是时钟极性 CPOL 和相位 CPHA。PN532 要求模式 0,如果主控配置成模式 3,通信不会完全失败,但数据位会整体偏一位或者半位,解析出来全是乱码。这个很难通过代码层面看出来,只能用逻辑分析仪对比。
另一个是片选信号的处理。我调试 SPI 时发现,有些库会在发送完整个命令帧后立刻释放 CS,但如果 PN532 处理命令需要时间,主机再想读响应时 CS 已经被释放了。正确做法是:先发送命令帧,保持 CS 拉低,等待芯片返回完 ACK 和响应帧,再拉高 CS。如果使用硬件 SPI 的自动片选功能,反而容易因为时序不可控而失败,我宁可用普通 GPIO 软件控制 CS。
还有一点是 SPI 速率。PN532 数据手册给出的最高 SPI 频率有限制,实际应用从 1MHz 起步比较稳妥。如果你的主控在 18MHz SPI 下通信正常,但 PN532 不稳定,那大概率是分频配置不合理,降到 4MHz 以内再试。
4.3 HSU 乱码和唤醒问题
HSU 的典型故障是收到大量乱码,或者一条指令发出去,芯片理都不理。
乱码第一怀疑对象是波特率。确认代码里是 115200,而不是 9600 或者 38400。有些模块板载了电平转换或者固定了别的波特率,看模块说明书里有没有标注。如果模块是通过 USB 转串口和电脑连接,还要确认串口工具本身没有开额外的流控。
第二是接线。TX 要和 RX 交叉连接,这是串口最容易犯的错。模块 TX 接主控 RX,模块 RX 接主控 TX,两边 GND 共地。第三是唤醒问题,前面说过,PN532 在 HSU 模式下可能会进入休眠,需要先发唤醒字节。
另外还有一个容易被忽略的点:HSU 模式下,PN532 的 IRQ 引脚同样会指示数据是否就绪,但有些模块没有把 IRQ 引出来。如果你发现发完命令后直接读串口,经常读不到完整响应,可以试试在发送命令后加一个 10ms 到 20ms 的延时,等芯片处理完再读,比一直死等要靠谱。
4.4 排查问题速查表
| 现象 | 可能原因 | 对应手段 |
|---|---|---|
| I2C 发送后无 ACK | 上拉电阻太大/太小、地址错误、接线反 | 检查上拉,确认地址 0x48/0x49,核对接线 |
| I2C 读到全 0xFF | 设备未响应、总线被占用 | 逻辑分析仪看波形,确认 SCL/SDA |
| SPI 通信数据错位 | CPOL/CPHA 配置错误 | 检查是否为模式 0 |
| SPI 读不到响应 | CS 时序不对、速率过高 | CS 全程保持拉低,降低速率 |
| HSU 乱码 | 波特率不匹配 | 确认 115200 8N1 |
| HSU 无响应 | TX/RX 接反、芯片休眠 | 交叉接线,发送唤醒字节 |
| 所有接口都不通 | 供电不足或电平不匹配 | 独立稳压供电,加电平转换 |
我在实际测试中还有一个小经验:如果没有逻辑分析仪,可以用一个简单方法验证硬件通路是否正常。对于 I2C,扫描总线上所有地址,看能不能扫到 0x48(或者 0x24);对于 SPI,发一个获取固件版本命令,看 MISO 上有没有非 0xFF 的数据;对于 HSU,直接把模块 TX 和 RX 短接,发什么回什么就说明串口通路没问题。这个方法不复杂,但能快速缩小问题范围。
调完这些,PN532 的读取基本就稳定了。如果你准备做更复杂的功能,比如卡模拟,注意 PN532 不是所有型号都支持完整的卡模拟功能,和具体固件版本有关系,先确认固件版本号会少走很多弯路。我自己后来做产品时,优先用 SPI 接口拿数据,再用 HSU 做调试输出,这样开发阶段和生产阶段互不干扰。
PN532 这套多协议通信思路其实很值得借鉴,一颗芯片用多种方式接入主机,既照顾了资源紧张的小主控,也满足了大吞吐量的需求。你在自己项目里选型时,不用太纠结说哪种接口最好,先把你手上主控的资源盘点清楚,再对照我前面的对比表,基本就能确定方案了。后面不管是做门禁、读卡器还是 NFC 开发板,通信层的框架都是同一套,熟悉了一种接口,再切到另一种也只是改底层收发函数的事。