
简介面向STM32微控制器的PN532 NFC串口驱动资源包旨在解决嵌入式平台上近场通信功能的快速集成问题适用于物联网节点、移动支付终端、门禁识别等场景。压缩包内共有211个文件整体体积约3.7MB以41个C源文件、43个头文件作为驱动核心同时包含Keil工程文件.uvprojx、可执行映像.axf、烧写文件.hex、链接映射.map以及编译生成的.o、.crf、.lst等辅助文件可直接打开工程查看代码并重新编译。驱动实现从PN532与STM32的UART硬件连接入手覆盖串口参数配置、协议帧构建与解析、InDataExchange命令调用、NDEF卡片读写并考虑中断收发优化与错误处理机制形成一套较为完整的参考实现。资源内还提供STM32F103C8T6的具体示例工程便于开发者对照移植到其他型号。该资源已有1804人浏览学习适合具备串口编程基础、希望减少NFC协议适配工作量的嵌入式工程师。1. stm32-PN532串口驱动把NFC读卡做成一个可以直接调用的库拿到这份 stm32-PN532串口驱动如果只是对着示例代码抄很容易在调通串口之后卡在读卡超时上。驱动真正的价值不在初始化代码而在把 PN532 的 HSU 串口协议封装成一套稳定可复用的状态机上层只要调用读卡函数就能拿到 UID帧头、长度、校验和、ACK 时序都交给底层处理。适合刚接触 NFC 的嵌入式开发者也适合要把读卡能力快速集成进现有 STM32 工程的人。文中会把协议格式、状态机实现、常见踩坑逐个展开跟着走完就能把一张 MIFARE 卡的 UID 读出来。2. 硬件与协议基础PN532 HSU串口模式下的帧格式和数据结构NFC 通信听起来玄学但落到驱动层面就是“字节的收发”。PN532 支持 SPI、I2C、HSU 三种接口本项目用的是 HSU也就是高速串口。在接线和帧格式没确认之前跑什么上层代码都是白搭。这一章先把模块的硬件接法和协议帧拆清楚后面读卡流程才不会莫名其妙失败。2.1 串口硬件接线与电平标准PN532 模块的硬件模式选择通常由 SDO、NSS、SCL 三个引脚的电平决定。以常见的模块为例HSU 模式要求 SDO 接高电平NSS 接低电平SCL 接低电平也有模块用拨码开关直接切换接口模式。如果模块上标注不清最好用万用表量一下默认电平或者查模块丝印不要照搬开发板的配置。确认模式后接线其实很简单PN532 的 TX 接 STM32 的 USART_RXPN532 的 RX 接 STM32 的 USART_TXGND 共地。很多模块支持 3.3V 或 5V 供电但逻辑电平一般是 3.3V。如果 STM32 的引脚不是 5V 容忍中间必须加电平转换。我见过有人把 PN532 的 TX 直接接到 5V 单片机上单片机没坏但信号高电平被拉高后偶尔误码读卡成功率只有一半。另一个容易忽略的是电源瞬间电流PN532 打开射频场时电流可能到几百毫安如果供电是电脑 USB 口或某个 LDO 余量不够帧就会丢字节。我习惯在模块 VCC 引脚并联一个 100uF 电容必要时再加一个 1uF 和 0.1uF 去耦效果会明显改善。HSU 模式下 PN532 默认波特率通常是 1152008bit 数据、无校验、1bit 停止位。如果模块被别人改过固件波特率也可能不同。接线完成后可以先发一帧 GetFirmwareVersion 命令看有没有响应再进入协议调试。这一步能排查大部分硬件问题比直接读卡来得快。2.2 PN532 HSU协议帧格式PN532 的串口帧格式和普通 AT 指令完全不同。一个标准数据帧由固定帧头、长度、校验和后置码组成具体字段如下表字段字节内容说明起始码0x00 0x00 0xFF3 字节固定帧头LEN从 TFI 到 PDU 结束的总字节数一个字节LCS0x100 - LENLEN 的补码校验TFI0xD4 或 0xD5主机发送是 0xD4PN532 应答是 0xD5PDU命令或响应数据长度为 LEN - 1DCS0x100 - (TFI PDU 求和)数据校验和后置码0x00帧结束这里容易写错的有两点。第一LEN 是指从 TFI 开始到 PDU 结束的字节数不是 PDU 长度。如果命令带 3 个参数LEN 就是 4TFI 也算进去。第二LCS 和 DCS 都是补码也就是每个字节和的模 256 取补例如和是 0x1F校验和就是 0xE1。用 C 语言写就是(uint8_t)(0x100 - sum)千万别写成按位取反或者异或。除了数据帧PN532 还定义了三种控制帧ACK 帧00 00 FF 00 FF 00表示帧接收成功NACK 帧00 00 FF 01 FF 00表示校验出错错误帧00 00 FF 03 FF 00表示模块当前无法处理。很多驱动把 ACK 当成完整响应这是最常见的翻车点。PN532 收到主机命令后会先回 ACK然后执行命令再回一个数据帧两次响应之间可能有几百毫秒延迟。所以驱动层必须区分“等待 ACK”和“等待命令响应”两个状态。2.3 驱动层的关键数据结构驱动里需要管理发送、接收、状态所以数据结构不能太零散。我一般定义一个实例结构体方便多串口或多模块复用typedef struct { UART_HandleTypeDef *huart; uint8_t tx_buf[256]; uint8_t rx_buf[256]; volatile uint8_t rx_len; volatile uint8_t expect_frame; uint8_t state; } pn532_uart_dev_t;expect_frame用来标记当前等待的是 ACK 还是数据帧state是状态机的当前状态rx_buf存放解析出的完整帧rx_len是有效长度。这里没有用动态内存因为嵌入式环境不允许。tx_buf用来组装将要发送的帧容量写 256 对 NFC 命令来说足够因为 PN532 的单个 PDU 很少超过几十字节。上层 API 需要一种方式来区分“收到的是哪条命令的响应”。我习惯让pn532_uart_wait_response(timeout)返回一个指向rx_buf的指针并且把rx_buf[0]设为 TFIrx_buf[1]设为命令回显。这样上层拿到响应后直接比对resp[1]就能确认是不是自己等的命令。要注意响应帧在复制到rx_buf时不要包含帧头、LEN、LCS 和后置码否则上层解析会很痛苦。2.4 常用命令与应答时序项目中用到的 PN532 命令不多最核心的是 GetFirmwareVersion0x02和 InListPassiveTarget0x4A。以 GetFirmwareVersion 为例主机发送的命令 PDU 只有一个字节02完整帧是00 00 FF 02 FE D4 02 2A 00其中02是 LENFE是 LCSD4是主机 TFI02是命令码2A是 DCS。PN532 收到后会先回 ACK紧接着回一个数据帧。响应帧的 LEN 一般是 0x05内容开头是D5 02后面跟着 3 个字节的固件版本号。读卡命令 InListPassiveTarget 的 PDU 是4A 01 00其中4A是命令码01表示最多检测 1 张卡00表示使用 106kbps 的 Type A 速率。发送后PN532 先回 ACK然后打开射频场等待卡片靠近检测到卡片后返回数据帧。如果一直没有卡靠近串口助手上可能只有 ACK。如果主程序是同步阻塞调用必须给wait_response设一个合理的超时比如 500ms否则程序会一直卡在读卡函数里其他外设全部停摆这也是很多“刷一次卡就死机”的根源。3. 驱动架构拆解从串口字节到NFC命令应答的完整链路这一章开始进入代码。整套驱动从底层到上层分三层串口收发、帧解析状态机、NFC 命令封装。这样分层以后上层写业务逻辑的人不用关心校验和底层换接口也不需要动上层的调用关系。3.1 源码目录结构与模块划分一个典型的驱动工程目录如下stm32-pn532-driver/ ├── Inc/ │ ├── pn532.h │ ├── pn532_uart.h │ └── main.h └── Src/ ├── main.c ├── pn532.c ├── pn532_uart.c └── hal_msp.cpn532_uart.c负责串口底层和帧解析pn532.c负责上层命令封装比如读卡、读固件版本main.c里调用上层 API。hal_msp.c由 CubeMX 生成主要处理引脚映射和时钟配置。这种分层的好处是如果后续要换成 I2C 或 SPI只需要替换pn532_uart.c这一层pn532.c可以原封不动。模块间的调用关系大概是main.c调用pn532_get_firmware_version()函数内部先组装命令行命令包通过pn532_uart_write()写入串口再调用pn532_uart_wait_response()等待响应帧最后从响应缓冲区解析出固件版本号。这个流程覆盖了从指令到字节再到应答的完整链路。3.2 核心代码串口初始化与接收状态机底层初始化用 CubeMX 生成 UART 配置即可关键是要开启 UART 全局中断并配置逐字节接收。代码结构如下UART_HandleTypeDef *pn532_huart; volatile uint8_t rx_byte; void pn532_uart_init(UART_HandleTypeDef *huart) { pn532_huart huart; HAL_UART_Receive_IT(pn532_huart, rx_byte, 1); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart pn532_huart) { pn532_uart_process_byte(rx_byte); HAL_UART_Receive_IT(pn532_huart, rx_byte, 1); } }这里pn532_uart_process_byte是状态机的入口。注意在中断回调里重新调用HAL_UART_Receive_IT是必须的否则只收到一个字节后接收就停了。我见过有人漏掉这一行表现是只能收到 ACK 帧的第一个字节后续全部超时。状态机实现可以用switch-case逐字节转移核心判断是帧头、长度和校验void pn532_uart_process_byte(uint8_t byte) { static uint8_t state 0; static uint8_t frame_buf[256]; static uint8_t idx 0; static uint8_t frame_len 0; static uint8_t sum 0; switch (state) { case 0: if (byte 0x00) state 1; break; case 1: state (byte 0x00) ? 2 : 0; break; case 2: if (byte 0xFF) { state 3; idx 0; } else state 0; break; case 3: frame_len byte; state 4; break; case 4: if ((uint8_t)(frame_len byte) ! 0) state 0; else { state 5; sum 0; idx 0; } break; case 5: frame_buf[idx] byte; sum byte; if (idx frame_len) state 6; break; case 6: if ((uint8_t)(sum byte) ! 0) state 0; else state 7; break; case 7: if (byte 0x00) pn532_uart_frame_ready(frame_buf, idx); state 0; break; default: state 0; break; } }状态 0 到 2 用来找00 00 FF帧头状态 3 读 LEN状态 4 验证LEN LCS是否为 0状态 5 收集 TFI 和 PDU状态 6 验证 DCS状态 7 确认帧尾。frame_len表示 LEN 字段也就是 TFIPDU 的字节数所以状态 5 里收集到frame_len个字节后就去校验 DCS。这里要特别说明frame_len是 LEN 字段的值不是 PDU 长度。如果命令带 3 个参数LEN 就是 4状态 5 会依次收下 TFI 和 3 个参数。实际工程里在状态 3 读到 LEN 后应该立刻判断frame_len 250就丢弃避免一帧损坏数据把缓冲区写穿。上面的代码没有做这个保护严格来说需要补上。3.3 数据帧封装与校验和计算发送侧和接收侧对称。组装一帧数据的函数如下uint16_t pn532_uart_build_frame(uint8_t tfi, const uint8_t *pdu, uint8_t pdu_len, uint8_t *buf) { buf[0] 0x00; buf[1] 0x00; buf[2] 0xFF; uint8_t len pdu_len 1; buf[3] len; buf[4] 0x100 - len; buf[5] tfi; uint8_t sum tfi; for (uint8_t i 0; i pdu_len; i) { buf[6 i] pdu[i]; sum pdu[i]; } buf[6 pdu_len] 0x100 - sum; buf[7 pdu_len] 0x00; return 8 pdu_len; }参数说明tfi是方向标识主机发命令传0xD4pdu是命令参数区pdu_len是参数长度buf是输出缓冲区返回值是帧总长度。注意校验和用了0x100 - sum等于补码和 LCS 一致。有了发送和接收基础上层 API 就简单很多。比如读固件版本uint8_t pn532_get_firmware_version(uint8_t *ver) { uint8_t pdu[1] {0x02}; uint8_t frame[20]; uint16_t len pn532_uart_build_frame(0xD4, pdu, 1, frame); pn532_uart_write(frame, len); uint8_t *resp pn532_uart_wait_response(1000); if (resp NULL || resp[0] ! 0xD5 || resp[1] ! 0x02) return 1; ver[0] resp[3]; ver[1] resp[4]; ver[2] resp[5]; return 0; }pn532_uart_wait_response会阻塞等待一帧完整数据底层状态机收到后复制到响应缓冲区。这里需要给超时参数一般 500~1000ms。PN532 在进入射频场后响应时间可能变长超时太短会误判为通信失败。4. 工程配置与运行让PN532在STM32上真正读出一张卡代码能编译只是第一步真正跑通读卡需要把工程配置、硬件连接和调试链路都对齐。这一章从 CubeMX 配置开始到烧录后用读卡流程验证给出完整的落地步骤。4.1 STM32CubeMX配置要点不管用哪种 STM32首次搭建都要在 CubeMX 里做三件事配置一个 USART 作为 PN532 通信口配置一个 LED 引脚用于状态指示配置系统时钟。通信口波特率选 115200、8 位数据、无校验、1 位停止位这个配置要和 PN532 的 HSU 默认波特率一致。我一般用 USART1 接 PN532引脚是 PA9 和 PA10这组引脚在开发板上很常见。开启 USART1 全局中断优先级不要设太高比系统滴答定时器低一档即可。生成代码后检查MX_USART1_UART_Init()中波特率和引脚是否正确然后在pn532_uart_init里调用HAL_UART_Receive_IT确保上电就能进入接收状态。4.2 编译烧录与调试链路推荐用 CubeIDE 或 Keil 编译整个工程。下载到板子后用 USB 转串口的 RX 接 PN532 的 TX注意接的是 PN532 的 TX不是 STM32 的 TX。打开串口调试助手波特率 115200如果 PN532 上电后主动发数据会看到00或FF之类的字节。不过很多模块上电不会主动发要等主机发命令才回。更靠谱的验证方式是让 STM32 上电主动发一条 GetFirmwareVersion 命令然后抓 PN532 的 TX 有没有回帧。没有逻辑分析仪时我就在pn532_uart_frame_ready回调里把接收到的每个字节通过另一个调试串口打印出来。这里要特别注意调试串口打印 hex 值时不要按 ASCII 输出否则0x00会被吞掉帧结构完全看不出来。这一步失败时的表现是串口助手一直空白。先查模块模式选择引脚再查 TX/RX 是否接反最后看供电是否稳定。大部分“完全不回”的问题都出在这三处。4.3 读卡流程验证驱动正确性当 GetFirmwareVersion 能正常返回时就可以测读卡了。PN532 读卡的核心命令是 InListPassiveTargetPDU 参数为4A 01 00。完整帧发送后PN532 会先回 ACK然后在射频场检测到卡片后返回数据帧。代码里我封装成uint8_t pn532_read_card(uint8_t *uid, uint8_t *uid_len) { uint8_t pdu[] {0x4A, 0x01, 0x00}; uint8_t frame[32]; uint16_t len pn532_uart_build_frame(0xD4, pdu, sizeof(pdu), frame); pn532_uart_write(frame, len); uint8_t *resp pn532_uart_wait_response(500); if (resp NULL || resp[0] ! 0xD5 || resp[1] ! 0x4B) return 1; *uid_len resp[4]; memcpy(uid, resp[5], *uid_len); return 0; }响应结构中resp[0]为0xD5表示 PN532 应答resp[1]为0x4B是 InListPassiveTarget 的命令回显resp[4]是卡片 UID 长度resp[5]起是 UID 数据。如果你用的是 MIFARE Classic 卡UID 通常是 4 字节所以uid_len一般是 4。验证时把一张卡片贴在模块天线上观察是否每次都能稳定返回相同的 UID。如果偶尔超时先检查供电和天线区域是否有金属遮挡。如果完全无响应用串口抓包看有没有 ACK有 ACK 但没有后续数据帧说明命令被接受了但射频场没有起来重点查天线匹配和模块供电。5. 避坑与排查串口通信和帧解析里的高频问题调试 NFC 驱动不是一蹴而就的事串口助手上看到的现象往往不是真实原因。我把最常见的问题整理成五条每一条都是可以复现的坑按“现象、原因、解决”的顺序写方便照着查。5.1 串口通信类问题坑 1上电后 STM32 发命令PN532 完全不回串口助手上没有任何字节。现象发送函数正常返回但 PN532 没有响应。原因PN532 并没有进入 HSU 模式很多模块出厂默认可能是 SPI 或 I2C。解决查模块上的模式选择引脚通常通过 SDO、NSS、SCL 电平组合切换按模块丝印配置为 HSU。同时确认 GND 已经共地模块 VCC 电压在正常范围。坑 2命令发出去后收到00 00 FF 00 FF 00的 ACK 帧但就是没有后续数据帧。现象ACK 能收到说明帧被接收了但 PN532 没有执行命令。原因ACK 只表示帧接收成功不表示命令执行成功。命令可能因为参数错误、校验错误被丢弃或者卡片未被激活。解决检查 LEN、LCS、DCS 是否计算正确特别确认 DCS 是 TFIPDU 的和的补码而不是异或再确认 InListPassiveTarget 中的波特率和卡片类型是否匹配。坑 3用串口调试助手直接读 PN532 的 TX看到一堆00、FF但拼不成一个完整帧。现象数据流看起来杂乱无章帧头反复出现。原因可能是供电过低或者时钟精度不够导致串口信号误码。解决给模块单独供电并在模块电源引脚处加电容尝试降低波特率测试看帧结构是否稳定。如果降低波特率后帧正常说明原来的时钟配置有偏差需要检查主时钟频率。5.2 帧解析与状态机问题坑 4程序运行时偶尔死机卡在状态机的某个循环里无法跳出。现象读卡功能时好时坏有时一帧数据就把系统搞死。原因状态机没有对长度字段做上下界约束收到一帧损坏数据时frame_len可能变成 255导致后续缓冲区越界。解决在状态 3 读到 LEN 时立即判断frame_len 250就丢弃并回到 IDLE同时检查idx是否超过frame_buf容量。这一步看起来小但能避免大部分内存踩踏问题。坑 5调用HAL_UART_Transmit发送命令时程序跑飞或者发送后收不到 ACK。现象发送阻塞在某个地方或者发送完没反应。原因发送函数在中断上下文或状态机处理流程中调用导致发送被嵌套中断打断或者发送完没有等待 TC 标志就立刻切换为接收最后几个字节还停在移位寄存器里。解决发送使用阻塞发送时确保不在 UART 接收中断回调内部调用如果必须用改用 DMA 发送并等待 DMA 完成回调。传输完成后清空接收缓冲再启动接收避免残留字节干扰下一帧解析。这些坑我基本都踩过尤其是长度校验和 ACK 混淆前者让程序跑飞后者让读卡永远失败。调试时最好把发出去的帧和收回来的帧都完整打印出来逐字节对照比对着代码猜要高效得多。6. 二次开发把驱动改造成支持I2C和SPI的通用适配层整套驱动目前是串口专用如果想换成 I2C 或者 SPI 接口的 PN532最省事的办法是把底层收发抽象成一组 IO 回调。在pn532_io.h里定义接口typedef struct { int (*write)(const uint8_t *buf, uint16_t len); int (*read)(uint8_t *buf, uint16_t len); void (*delay_ms)(uint32_t ms); } pn532_io_t;UART 实现里write就是HAL_UART_Transmitread依赖前面状态机填充的缓冲区I2C 实现里write需要先发送 PN532 的 I2C 地址再按 I2C 协议写入帧数据read要处理 NACK 和重试。上层命令完全不变只把pn532_uart_build_frame的输出交给不同的 IO 层。这种分层我在另一个项目里用过移植到 SPI 只改一个文件。验证改造是否成功不要直接跑读卡先做一个回环测试在底层的read函数前加一个调试宏把收到的每一帧打印出来确认帧头、LEN、DCS 都正确。然后用 GetFirmwareVersion 做冒烟测试再进入读卡流程。改接口时要特别注意延迟函数I2C 和 SPI 模式下PN532 的时序要求更严格delay_ms最好用systick实现不要用空循环数数否则不同编译优化等级下延时差异会直接导致通信失败。从那以后我每次接手一个新的 NFC 驱动都会强制走一遍“模式引脚确认、帧格式抓包、校验和逐字节核对”这三步再开始动代码。希望帮到你。本文还有配套的精品资源点击获取