PN532实战:I2C、SPI、HSU三接口通信与调试全攻略
最近把一个基于 PN532 的 NFC 读卡项目调完板子拿回来第一件事就是把 I2C、SPI、HSU 三种主机接口挨个跑了一遍。PN532 这块 NXP 的 NFC 控制器本身支持 ISO14443A/B、Mifare 系列、FeliCa 等常见卡种但真正让它“好用”的其实是它对外提供了三种不同的主机通信接口可以根据主控资源、速率需求、调试习惯灵活切换。这篇文章就围绕 PN532 的多协议通信实战把我调 I2C、SPI、HSU 三种接口时的接线、时序、代码框架和踩坑记录都整理出来。适合正在用 STM32 或其他 MCU 做 NFC 读卡器、门禁、支付终端、近场交互设备的工程师参考也适合刚接触 PN532 想快速把协议层跑通的新手。1. 为什么一块 NFC 芯片要做三种通信接口接口选型背后的逻辑1.1 I2C、SPI、UART 三种协议的本质区别很多朋友第一次接触 PN532 时会有一个疑问既然芯片已经内置了 NFC 协议栈主机只需要发命令、收响应为什么还要提供 I2C、SPI、HSU 三套接口这不是徒增选择困难吗其实这三套接口各有适用场景选错了后期调试成本会明显上升。先看一个直观对比接口引脚需求典型速率总线形态差错机制调试难度I2CSCL SDA加 IRQ100kHz / 400kHz多设备共享总线有 ACK/NACK链路错误易发现中需关注上拉电阻和时序SPISCK MISO MOSI CS加 IRQ可到数 MHz通常 1~8MHz 够用点对点主从为主无内建应答错帧靠协议层校验中需关注极性和片选HSUTX RX可加 RTS/CTS 流控加 IRQ9600~115200 常见点对点无内建应答需帧校验低串口打印最直接从表格能看出I2C 的优势是引脚少而且总线上可以挂多个设备SDA 低电平代表“占用”天然支持一主多从SPI 的优势是速度上限高配合 DMA 传输时 CPU 开销极小适合高频轮询卡状态、批量读扇区数据的场景HSU 本质是高速串口只要主控有 UART 就能接调试时用 USB 转串口插到电脑上就能看数据流非常直观。PN532 之所以保留三种接口本质上是为了适配不同行业的不同主控。有些家电主控只有 I2C有些工控板 SPI 资源富余有些项目干脆就是想用串口透传三种接口都支持意味着同一颗 NFC 芯片可以覆盖更多产品形态。实际项目中我不会盲目追求“最快”而是先看主控侧哪个接口最顺手再反过来定 PN532 的接口模式。1.2 PN532 接口模式切换与引脚配置PN532 的接口模式不是软件寄存器配置而是通过硬件引脚在上电时锁定的。这一点很多人第一次用会忽略以为像普通外设一样在代码里切一下就完事实际不行。我手头常用模块的 SEL0、SEL1 与接口模式的对应关系如下SEL0SEL1接口模式01I2C默认10SPI11HSU部分成品模块会把 SEL0、SEL1 做成拨码开关或者跳线焊盘比如我最早用的一款红色小板就是拨码开关板子丝印上直接标了 I2C、SPI、HSU 三个档位。裸片方案则需要自己拉高拉低这两个引脚而且要保证复位期间电平稳定不能悬空。注意切换模式之后必须对 PN532 重新上电或者把 RSTPD 引脚拉低再释放否则芯片仍然沿用旧接口工作主机侧怎么发数据都收不到响应这种“玄学故障”我遇到过不止一次。还要提醒一点PN532 某些引脚在不同接口模式下是有复用的。比如 I2C 模式下有些引脚用于配置地址SPI 模式下则用作 CS 或 IRQ。裸片画板时要仔细对照数据手册的引脚功能表不要想当然。成品模块一般已经处理好了直接用就行但如果是自己画的 Layout这一点一定要提前确认。2. I2C 模式实战上拉电阻怎么选、地址与帧怎么对2.1 先搞清楚 I2C 为什么用开漏输出和上拉电阻I2C 调不通十有八九是上拉电阻的问题。要理解这个问题得先明白 I2C 的物理层设计。I2C 的 SDA 和 SCL 都是开漏结构意思是设备只能主动把线拉低不能主动输出高电平。高电平靠外部上拉电阻提供。这样设计有两个直接好处一是多个设备可以“线与”任何一方拉低总线整个总线就是低电平多主机仲裁时不会出现一个设备输出高、一个输出低导致短路二是方向切换方便主机发送完数据后释放 SDA 交给从机回 ACK只要把引脚配置成输入即可不会有驱动冲突。那么上拉电阻阻值怎么选这里有一个简单的估算逻辑。阻值太小低电平时灌入的电流偏大比如 3.3V 下用 1k 上拉低电平灌电流就有 3.3mA多个设备并联时更严重可能导致引脚拉不低或者低电平电压偏高这正好对应了很多朋友遇到的“上拉电阻小了不通信”现象。阻值太大RC 充电时间常数变大上升沿变缓在 400kHz 快速模式下可能还没到达高电平阈值就被采样了同样会误码。我实际使用时的经验值100kHz 标准模式用 4.7k 上拉到 3.3V400kHz 快速模式用 2.2k 或 1k如果 PCB 走线较长或者总线上挂了多个设备总线电容变大就要适当降低阻值但低于 1k 后要格外留意低电平灌电流。调试时我习惯先用 4.7k 把功能跑通再用示波器看波形的上升沿和低电平电压最后决定要不要调整。还有一个高频问题外部已经接了上拉电阻MCU 内部的上拉还需不需要开答案是需要关掉。内部上拉一般是几十 k 欧的弱上拉和外部上拉并联后等效阻值会被拉低看似影响不大但内部上拉的源阻抗并不精确在快速通信时会干扰信号完整性而且内部上拉本质上也是驱动能力弱的上拉源容易出现分压问题。我在工程上统一的做法是外部有上拉就不开内部上拉保持 I2C 引脚为开漏输出模式。2.2 PN532 的 I2C 地址与通信帧格式PN532 在 I2C 模式下的设备地址由硬件引脚决定默认常见的 7 位地址是 0x24换算成 8 位写地址是 0x48读地址是 0x49。注意STM32 的 HAL 库函数里传的 DevAddress 参数需要的是左移一位后的 8 位地址也就是直接填 0x48很多新手这里容易填成 0x24导致通信失败。PN532 的协议帧格式非常统一无论是 I2C、SPI 还是 HSU帧结构都是一样的前置码长度校验方向数据校验后置码00 00 FFLENLCSTFIPDU 数据DCS00其中 LCS 是 LEN 的补码计算公式为 LCS 0x100 - LENDCS 是 TFI 加上 PDU 所有字节求和的补码计算公式为 DCS 0x100 - (TFI sum(PDU))。TFI 的取值有规定主机发给 PN532 时是 D4PN532 回给主机时是 D5。举个例子发一条最常用的 GetFirmwareVersion 命令完整帧是00 00 FF 02 FE D4 02 2A 00拆开看00 00 FF 是前置码LEN 是 02LCS 是 FETFI 是 D4PDU 是 02GetFirmwareVersion 指令DCS 是 2A最后 00 是后置码。PN532 收到后如果没有异常会先回一个 ACK 帧00 00 FF 00 FF 00然后等模块把真正的数据帧准备好IRQ 引脚拉低主机再去读。这里有一个关键操作经验发送命令后不要立刻去 I2C 读数据必须先等 PN532 的 IRQ 引脚变成低电平这表示模块已经有数据要发给主机了。如果不等 IRQ 直接读大概率读到的是空帧或者状态字节不全的数据。I2C 读取时模块通常会先回一个状态字节再回正式帧数据具体状态字节的含义不同固件可能有差异我习惯先把整段数据抓回来再根据帧头 00 00 FF 找位置解析。2.3 用逻辑分析仪读 I2C 时序从 START 到 ACK逻辑分析仪是我调 I2C 时最依赖的工具。很多朋友买了逻辑分析仪却不知道怎么用这里说点实操经验。先设置采样率。通常建议采样率至少是总线频率的 4 倍以上100kHz 的 I2C 最少用 1MHz 采样率400kHz 则建议 4MHz 或更高。采样率太低解码出来的波形会丢失细节有时连 START 条件都识别不了。抓信号时主要看这几个点START 条件是 SCL 高电平期间 SDA 由高变低STOP 条件是 SCL 高电平期间 SDA 由低变高数据位在 SCL 高电平期间保持稳定SCL 低电平期间允许变化第 9 个时钟是 ACK/NACK 位SDA 被从机拉低表示 ACK保持高则表示 NACK。有些朋友只看原始波形不看协议解码这样效率很低。我建议在逻辑分析软件里直接挂 I2C 协议解码器让它把地址、读写方向、每个字节的内容标出来。调 PN532 时如果看到地址 0x48 后面跟的 ACK 丢失就要先怀疑上拉电阻太小导致 SDA 拉不低如果数据能发出去但 PN532 一直不响应就重点看主机发送完帧后有没有等 IRQ以及 IRQ 有没有接到外部中断。逻辑分析仪一次抓完整段操作基本能定位八九成的问题。3. SPI 模式实战时钟极性、片选策略与 DMA 读取3.1 SPI 主从通信的关键参数CPOL、CPHA 与片选SPI 调不通最先怀疑的就是 CPOL 和 CPHA。CPOL 决定时钟空闲时的电平CPOL0 表示空闲时 SCK 为低CPOL1 表示空闲时 SCK 为高CPHA 决定数据在哪个边沿采样CPHA0 表示第一个边沿采样CPHA1 表示第二个边沿采样。组合起来就是常用的四种模式。PN532 作为 SPI 从机我实际用的配置是 CPOL0、CPHA1也就是 SPI 模式 1。这个不一定在所有模块固件上都一样所以我的建议是先把 SCK 频率降到 1MHz 附近把 CPOL 和 CPHA 的四组组合都试一遍读一次 GetFirmwareVersion能收到正常响应的那组就是对的。比起翻手册确认每个边沿这种实测排除法在项目里更高效也更能暴露极性问题。另一个容易出问题的是片选。STM32 有硬件 NSS 和软件片选两种方式。硬件片选在单主机模式下由外设硬件自动控制看似方便但实际用起来不够灵活尤其 PN532 这种从机片选低电平期间 MISO 才会输出数据一旦时序没控好硬件 NSS 的自动翻转反而碍事。我统一用软件片选GPIO 拉低选中 PN532操作完再拉高逻辑非常清晰。SPI 读 PN532 还有一个容易忽略的细节这是全双工总线主机要收数据就必须同时发数据。读取 PN532 响应时主机的 MOSI 可以不发有效数据但要持续发送 0x00 来产生 SCK 时钟否则从机的 MISO 数据根本不会移动。所以配置 SPI 时不要选“只接收”模式直接用全双工模式最省心。3.2 PN532 作为 SPI 从机的特殊行为与 IRQ 配合PN532 在 SPI 模式下有两个行为特性和 I2C 不太一样一定要先摸清楚。第一CS 引脚拉低后MISO 上吐出来的第一个字节通常是状态字节。比如模块有数据准备好时这个状态字节可能是 0x00表示可以读后续帧数据。因此我读数据时会先接收一个较长缓冲再在缓冲里搜索 00 00 FF 这个帧头来定位真正的响应帧而不是默认第一个字节就是帧的第一字节。第二IRQ 引脚仍然有效。和 I2C 一样主机发送命令帧之后要等待 IRQ 拉低再拉低 CS 去读响应。如果不等 IRQ 就拉低 CS 读可能读到的是上一帧残留数据或者状态未就绪的数据。我见过有人 SPI 读了一堆 FF排查半天发现是 CS 一直拉低MISO 被持续占用IRQ 也一直没恢复整个状态机就乱了。所以我的标准操作顺序是拉低 CS发送命令帧拉高 CS等待 IRQ 下降沿拉低 CS用 SPI 连续读 N 个字节拉高 CS。整个流程里CS 每帧只拉低一次读完立即释放IRQ 会在主机读完数据后自动恢复高电平。保持这个节奏SPI 模式基本不会出幺蛾子。3.3 用 STM32CubeMX 把 SPIDMA 配出来并读取 PN532SPI 的好处是速度快配 DMA 后几乎不占 CPU。下面以 STM32F103 的 SPI1 为例把 Cubemx 配置过程完整走一遍。在 Cubemx 里SPI1 模式选 Full-Duplex Master数据宽度 8 bitMSB First预分频器一开始选大一点比如 64 分频先把 SCK 降到 1MHz 左右调通之后再逐步提高。NSS 选 SoftwareCPOL 和 CPHA 按上一节说的模式 1 来配即 CPOLLOW、CPHA2EDGE。如果实测不对再切其他组合。关键一步是 DMA。在 Cubemx 的 DMA Settings 里给 SPI1_RX 添加一个 DMA 通道Mode 选 NormalData Width 选 Byte。注意如果只配 RX DMA主机不会自己产生 SCK所以实际读取时要调用全双工的 DMA 收发函数同时送 TX 缓冲才能持续产生时钟。这一点非常关键很多人只开了 RX DMA结果数据全停在那不动。初始化代码大致如下hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_2EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_64; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1);发送命令帧时用软件片选先拉低 CS调用 HAL_SPI_Transmit 发完整帧然后拉高 CSHAL_GPIO_WritePin(PN532_CS_GPIO_Port, PN532_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmdBuf, cmdLen, 100); HAL_GPIO_WritePin(PN532_CS_GPIO_Port, PN532_CS_Pin, GPIO_PIN_SET);等到 IRQ 下降沿触发后再拉低 CS用 DMA 方式接收一段定长数据HAL_GPIO_WritePin(PN532_CS_GPIO_Port, PN532_CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive_DMA(hspi1, txBuf, rxBuf, PN532_RX_LEN);DMA 完成回调里再拉高 CSvoid HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { HAL_GPIO_WritePin(PN532_CS_GPIO_Port, PN532_CS_Pin, GPIO_PIN_SET); } }这里我多说一句 DMA 循环模式。之前有人想用 HAL 库的 SPI DMA 循环模式做连续轮询省去每次重新启动 DMA 的麻烦但循环模式下 RX 缓冲会被反复覆盖PN532 返回帧长度不固定很容易出现半包错位。工程上我仍然推荐 Normal 模式配合 IRQ 触发每帧重新启动一次 DMA虽然多几行代码但逻辑清晰、数据可靠。调通 DMA 之后我实测 SCK 在 4MHz 左右跑 PN532 也很稳不过第一版调试还是建议从低速开始。4. HSU 模式实战高速串口与波特率适配4.1 HSU 和普通 UART 到底差在哪HSU 的全称是 High Speed UART本质上还是串口但 PN532 把这套串口接口按高速场景做了优化。相比普通 UARTHSU 支持的波特率范围更高引脚上还带了 RTS/CTS 流控信号方便和主控做硬件握手。实际项目中HSU 最大的价值在于调试友好。SPI 和 I2C 都必须用逻辑分析仪或者示波器才能看到波形而 HSU 只需要一根 USB 转串口线就能把 PN532 和 PC 直接连起来用串口助手发命令、收响应对协议理解和验证非常有帮助。所以我个人的建议是如果主控侧有闲置的 UART 资源第一次接触 PN532 可以先从 HSU 模式开始跑把协议帧结构吃透再切到 I2C 或 SPI 做量产方案。接线方面PN532 的 TX 接主控 RXPN532 的 RX 接主控 TX注意交叉共地。IRQ 引脚的用法和另外两种接口一样发送命令后等待 IRQ 拉低再读取。如果 MCU 支持 RTS/CTS可以接上流控如果只是普通串口不接流控也能跑但前提是严格按照“发帧、等 IRQ、读响应”的节奏来不要在模块还没准备好时连续发送数据。4.2 波特率自适应的初始化手法PN532 的 HSU 模式有一个和普通 UART 很大的区别它支持波特率自动检测。也就是说模块上电后并不知道主机用多少波特率通信需要在初始化阶段先进行一次同步。我常用的做法是在发正式命令之前先按目标波特率发送一段同步字节例如连发几个 0x55。0x55 的二进制是 01010101高低电平交替模块通过测量位宽就能反推出主机波特率。不同库和固件对同步序列的要求不完全一样有的发 0x55 0x55 0x00有的发更多字节我建议按你使用的 SDK 或参考手册来。如果跨过同步直接发命令常见的现象是模块不响应、响应乱码或者干脆一直超时。同步完成之后再发 GetFirmwareVersion 确认链路是否正常。如果收到的帧头不是 00 00 FF而是 FF 00 FF 之类先不要怀疑硬件优先检查波特率是否匹配、同步序列是否足够。常用波特率 9600、19200、38400、57600、115200 我都试过只要同步这一步做好后续数据收发都正常。考虑到 STM32 的 APB 分频和误差问题工程上更推荐 115200波特率越高单个 bit 的时间越短但误差容忍度反而越低所以要确保主控串口波特率计算误差在允许范围内。有流控的板子还要注意发送前要等 CTS 信号允许接收时如果 RTS 被拉高说明缓冲区满了暂时不要继续发。没有流控时靠 IRQ 等待就是唯一的保护手段所以我总强调 IRQ 脚一定要接不能省。4.3 如何设计一套可切换三种接口的驱动框架三种接口都调通之后如果每个应用都写一套业务代码那就太浪费了。我的做法是把 PN532 的协议层和物理接口层分离定义一组统一的接口操作函数上层业务只依赖这组函数。typedef struct { void (*init)(void); uint8_t (*writeCommand)(const uint8_t *buf, uint16_t len); uint8_t (*readResponse)(uint8_t *buf, uint16_t maxLen); uint8_t (*waitIRQ)(uint32_t timeoutMs); } pn532_io_t;然后为 I2C、SPI、HSU 分别实现这套函数。比如 I2C 版的 readResponse 内部是 HAL_I2C_Master_ReceiveSPI 版内部是拉 CS HAL_SPI_TransmitReceive_DMAHSU 版内部是 HAL_UART_Receive。上层代码完全不用感知当前跑在哪个接口上换接口时只要改一下初始化和板级配置业务逻辑一行不动。这么做还有一个好处不同接口的差异被隔离在一个很薄的文件里测试时可以用同一套业务代码分别验证三种接口的收发正确性。我之前就靠这个框架做过一次三接口回归测试发现某个模块的 SPI 在高速下偶发丢字节而 I2C 和 HSU 都正常问题一下就定位到了 SPI 的时序参数上。5. 实战中遇到的常见问题与排查实录5.1 I2C 上拉电阻小了不通信现象与解决有一个字面意义上很典型的问题I2C 上拉电阻太小导致 PN532 不通信。现象是发送命令后模块无响应用逻辑分析仪看SDA 低电平只有 0.5V 左右甚至接近 1V已经高于 I2C 标准的低电平阈值从机根本无法正确识别。这背后的原因就是 2.1 节说的灌电流问题。上拉电阻过小外部上拉和内部开漏管脚形成分压低电平被抬高了。解决思路很简单换回 4.7k 上拉或者把总线上所有设备的上拉都查一遍因为多设备并联后等效上拉阻值会变小。我还遇到过一种隐蔽情况板子上原来为其他传感器设计了一组 1k 上拉PN532 直接挂在同一组总线上结果怎么调都不对后来把 PN532 单独用一组 4.7k 上拉才稳定。排查 I2C 问题我还会顺手看一眼 IRQ 引脚配置。有些工程上的 IRQ 没开外部中断而是在主循环里轮询电平这也能用但要注意轮询周期不能太长否则读响应慢得像死机一样。最好还是用下降沿触发外部中断IRQ 一低就立刻进入读流程响应时延最小。5.2 SPI 通信不生效极性、片选、时序三板斧SPI 调不通我总结了三板斧排查顺序。第一板斧查极性。如果读回来的数据全是 FF 或者全是 00大概率是 CPOL/CPHA 组合不对。这个用逻辑分析仪看 SCK 和数据信号的采样点最直观看不到就让程序把四组模式都跑一遍。第二板斧查片选。软件片选时CS 拉低后要延时一小段再开始收发尤其对 PN532 这类从机CS 刚拉低时 MISO 还没有稳定进入输出状态收发结束后也要确保 CS 拉高。如果 CS 一直拉低不释放PN532 的 SPI 状态机可能卡死后续所有通信失效。第三板斧查时钟速率。PN532 是 SPI 从机但不同模块的固件对 SPI 时钟上限要求不同不要一上来就推到十几 MHz。先用低速把链路打通再逐步提高。我之前在某个 8MHz 主频的 MCU 上跑 SPI 预分频 2SCK 接近 4MHz一切正常换到另一块板子同样的配置却频繁丢字节最后发现是 PCB 走线过长、信号完整性变差降到 2MHz 就稳定了。高速 SPI 要关注走线和负载电容不能只看 MCU 支持多快。5.3 切换接口后模块无响应复位与配置顺序这个问题非常常见尤其是同一块板子反复测试三种接口时。现象是从 I2C 切到 SPI代码改完了硬件跳线也改了但模块就是不响应。原因基本就是 1.2 节说的PN532 的接口模式是上电锁定的只改跳线不重新上电没用。具体操作时我会在切换硬件跳线之后先断开主控和模块的供电等几秒再上电如果模块有 RSTPD 引脚也可以用 GPIO 控制复位复位期间保证 SEL0、SEL1 电平已经到位然后释放复位。这样模块才会按新接口启动。还有一个更隐蔽的坑PN532 内部有一些配置是可以被主机命令改写的比如 I2C 地址偏移、波特率设定、SAM 配置等。如果在某个接口下调用了改配置的命令切到另一个接口后可能仍然继承旧配置导致行为异常。遇到这种情况要么发一次恢复出厂配置的命令要么给模块重新刷固件再要么就是换一颗芯片做对照实验。我倾向于在应用代码里每次上电都显式做一次接口初始化而不是依赖模块的默认状态。5.4 问题排查速查表把上面这些经验整理成一张速查表调试时对着看能省不少时间。现象排查方向常用解法I2C 发命令后无响应上拉电阻、地址、IRQ 等待确认上拉 4.7k 左右关闭内部上拉核对 8 位写地址 0x48等待 IRQ 下降沿I2C 读回全 FF 或乱码总线冲突、上拉过小用逻辑分析仪看 ACK检查低电平电压换 2.2k~4.7k 上拉SPI 读回全 FF极性错、CS 没拉低、MISO 虚焊轮换 CPOL/CPHA确认软件片选时序检查硬件连接SPI 偶发丢数据时钟过快、DMA 缓冲覆盖降到 1MHz 验证改用 Normal 模式每帧重启 DMAHSU 响应乱码波特率不匹配、未做同步先发同步序列让 PN532 锁定波特率再发 GetFirmwareVersion切换接口后无响应未复位、硬件跳线没生效重新上电复位确认 SEL0/SEL1 电平稳定IRQ 一直为高命令没发出去、模块没识别接口用逻辑分析仪抓主机发送帧确认帧校验 LCS/DCS 正确我个人踩过几次坑之后养成的习惯是拿到任何一块 PN532 板子先不写业务代码直接用逻辑分析仪架在总线上跑一次 GetFirmwareVersion确认链路层波形正常再开始写应用。这个习惯帮我避开了至少三次“代码没问题但硬件没通”的僵局。如果让我给刚接触 PN532 多协议通信的朋友一个建议我会说第一遍尽量用 I2C 把流程跑通因为 I2C 有 ACK最容易定位问题是主机还是从机跑通之后再根据实际项目需求切 SPI 或 HSU。协议切换本身不难真正难的是把每一条总线的物理层特性摸清这也是我想通过这篇实战记录把这三种接口放在一起讲的原因。