简介面向STM32嵌入式开发者这份工程包演示了通过I2C接口驱动PN532 NFC/RFID模块并读取UID的完整流程适合需要实现非接触式刷卡、门禁或标签识别的中初级开发者。压缩包共272个文件约14.74MB包含大量.c/.h源码、I2C/USART驱动、FreeRTOS示例工程及MDK项目文件也有编译生成的.o/.axf/.hex文件和PDF/TXT说明文档直观覆盖从源码到烧录验证的各个环节。资源特别提供了STM32F103RB与FreeRTOS组合的示例以及新增的NFC UART驱动可作为I2C之外的备选通信方案PN532库一并打包省去自行移植的麻烦代码中对I2C初始化、UID读取命令发送与返回数据解析均有清晰实现便于迁移到门禁或点检场景。已有633人浏览学习适合想快速跑通STM32PN532读写标签的开发者参考。1. stm32-PN532-i2c-read-uid一个让新手绕半天、让老手十分钟搞定的NFC读卡项目先下个结论这个项目要做的就是用 STM32 的硬件 I2C 接口把 PN532 当做一个 I2C 从设备来访问发指令让它被动侦听非接触卡再从返回帧里把 UID 取出来。它解决的是嵌入式设备上做卡识别的最基础问题——门禁、消费机、开发板上的 IC 卡验证都是这个流程。标题里四个关键词卡住了不少新手I2C 从机地址到底填 0x24 还是 0x48、上电后要不要先唤醒、返回帧里第几个字节才是 UID、为什么每次读出来的数据是乱的。这篇文把四条线一次讲透照着敲一遍能通理解了能改踩过的坑也一并列出来。适读人群是有一定 STM32 基础、想在自己板子上加 NFC 读卡能力的开发者以及正在被 PN532 初始化时序折磨的调试者。2. 先定接口再接线PN532 的 I2C 选型理由与 STM32 侧的硬件准备2.1 三种主机接口怎么选I2C 不是性能最优但对 STM32 生态最省事PN532 这颗芯片本身支持 I2C、SPI、UART 三种主机接口硬件上通过 BS0 和 BS1 两个引脚的电平来切换。I2C 模式需要 BS01、BS10SPI 模式是 BS00、BS11UART 模式是 BS00、BS10。市面上大部分 PN532 模组已经把这两个脚通过跳线或电阻配置好你买到的模块默认是哪种模式看丝印或者卖家说明就能确定。从工程师的角度说选 I2C 的理由很现实只占 SCL、SDA 两根线外加一根 IRQ 中断脚比 UART 多一根信号线但比 SPI 少了 CS 和 MISO/MOSI 四根线引脚占用最少。而且 I2C 是 STM32 的硬件外设有现成的库函数支持不需要像模拟 SPI 那样自己用 GPIO 拼时序。I2C 标准模式是 100kHz快速模式是 400kHz对 PN532 这种一次收发几十个字节的读卡交互来说速度完全够用。如果你做的是地铁闸机那种对响应时间苛克到毫秒级的场景SPI 或 UART 的吞吐优势才会体现出价值。还有一个容易被忽略的选型点硬件 I2C 在 STM32 上可以用 DMA 搬运数据不占 CPU。软件模拟 I2C 虽然移植方便但每一 bit 都要等延时会阻塞主循环。对一个读卡器来说主控还要处理按键、显示、通信硬件 I2C 加 DMA 是更省心的方案。如果你的项目里 STM32 的 I2C1 被其他传感器占了用 I2C2 也一样只是引脚号和时钟使能要跟着改。2.2 电平匹配与上拉电阻通信不稳的隐患在这一步埋下PN532 的供电范围是 2.7V 到 5.5V但它的 IO 电平逻辑是以 VCC 为参考的。如果你手上的是 3.3V 版本的模块接到 STM32F103 这类 5V-tolerant 引脚上理论上可以直接连但我不建议赌这个。因为很多低价模组的板载电平处理做得很糊有的靠串阻有的靠二极管钳位有的干脆没有处理。稳妥做法是统一用 3.3V 给 PN532 供电SCL、SDA、IRQ 全部走 3.3V 逻辑电平STM32 的 I2C 引脚也配置成开漏模式由外部上拉到 3.3V。上拉电阻是 I2C 总线能不能稳定工作的命门。STM32 内部有上拉选项但在 I2C 400kHz 下不太可靠我一般都在板子上放 4.7kΩ 的外部上拉到 3.3V。这里有个常见误区PN532 模组板子上往往已经带了上拉电阻你再并一个 4.7kΩ等效阻值会变小上升沿变快是好事但如果两个上拉叠加后等效阻值低于 2kΩ挂载的设备一多总线负载过重反而会出错。建议先用万用表量一下 SCL 和 SDA 对 3.3V 的电阻值如果已经有 10kΩ 左右的阻值就不用再外加。判断上拉是否合适的土办法是把 I2C 时钟设到 400kHz然后用逻辑分析仪抓 SCL 上升沿。标准规定快速模式上升时间不超过 300ns如果你看到波形像斜坡一样缓上去那就是上拉太弱。换 2.2kΩ 试试如果波形还是不行再查是不是走线太长或者地平面有问题。这些在调试阶段花十分钟解决比量产之后批量返修划算得多。2.3 六线接线表与上电自检清单把 STM32 和 PN532 的连线列成一张表按这个接基本不会错。功能STM32 引脚PN532 引脚说明电源3.3VVCC按模块标注供电不建议直接上 5V地GNDGND必须共地I2C 时钟PB8I2C1_SCLSCL若用 I2C2 则改 PB10I2C 数据PB9I2C1_SDASDA若用 I2C2 则改 PB11中断PB0GPIO 输入IRQ等待响应就绪的关键引脚复位PB1GPIO 输出RSTO可选用于软件复位IRQ 这个引脚在实际调试中非常关键。PN532 在 I2C 模式下处理完一条命令后会把 IRQ 拉低表示响应数据已经准备好了。主机如果不等 IRQ 直接去读很可能读到空数据或者上一帧的残留。RSTO 引脚建议接一个 GPIO程序里留一个软件复位的接口遇到模块死锁时不需要手动按硬件复位。刚开始调通阶段只用开发板上的复位键也够但软件复位迟早要补上。接完线后上电之前先做两个检查第一用万用表确认 PN532 的 VCC 确实是 3.3V如果是 5V 版本就要改供电方案第二查一下模块上有没有 27.12MHz 晶振PN532 必须靠这颗晶振产生射频时钟和协议时序如果模块是拆机件且晶振缺失那 SDA 上永远不会出现 ACK。这两个检查做完再接 SCL 和 SDA能省掉后面大量的排查时间。3. 初始化链路I2C 外设配置、PN532 唤醒与固件版本验证3.1 STM32 侧 I2C 初始化三个必须留意的参数用 STM32 标准外设库初始化 I2C1重点看三个地方引脚模式、时钟频率、从机地址。引脚要配置为复用开漏GPIO_Mode_AF_OD这是 I2C 的标准接法因为总线需要外部上拉电阻来拉高电平。时钟速度建议第一次跑 100kHz不要一上来就 400kHz后面我会说为什么。PN532 的 7 位从机地址是 0x24这个地址在库函数里使用时需要左移一位变成 0x48 是写地址0x49 是读地址这个细节是经典的翻车点。void I2C_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD; /* 开漏输出I2C 标准接法 */ GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_ClockSpeed 100000; /* 先跑 100kHz稳定后再提速 */ I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_OwnAddress1 0x30; /* 主机模式下这个值不影响通信 */ I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }这段代码里的 IO 速度设为 50MHz 虽然是过配置但开漏模式下影响不大留这个值是为了避免某些标准库版本在低速设置下产生编译告警。I2C_AcknowledgedAddress_7bit 表示本机地址是 7 位模式主机模式下这个字段并不会主动使用但必须按规范初始化否则库函数可能不进入正确的状态机。I2C_OwnAddress1 填什么值都行只要不和总线上的从机地址冲突。如果你用的是 HAL 库对应关系是GPIO 初始化用 GPIO_InitStruct.Mode GPIO_MODE_AF_ODI2C 初始化用 hi2c1.Init.ClockSpeed 和 HAL_I2C_Init。后面发送数据的函数从 I2C_SendData 换成 HAL_I2C_Master_Transmit参数多一个超时时间。底层逻辑是一样的不影响本文的帧格式分析。3.2 从机地址与唤醒机制不唤醒就读不到任何东西PN532 上电后并不一定会立刻作为 I2C 从设备响应你。部分低成本模组为了省电上电后进入的是低功耗模式需要主机先向它写一个字节 0x00 来唤醒。这个操作的本质是让芯片从 Hibernate 状态退出进入正常的命令处理模式。如果你跳过了这一步后续发任何命令都会石沉大海——CRLF 不出现在总线上IRQ 也不会拉低。唤醒操作的具体代码很简单但地址是最容易搞错的地方。向 0x24 写 0x00 这句话在帖子里的意思是7 位设备地址是 0x24执行 I2C 写操作时总线上发出的地址字节是 (0x24 1) 0x48。如果你在库函数的 Send7bitAddress 里直接填 0x24实际发出去的就是 0x24PN532 根本不在这个地址上应答。我的建议是把宏定义直接写成两行#define PN532_I2C_ADDR_WRITE 0x48和#define PN532_I2C_ADDR_READ 0x49从源头避免混淆。唤醒之后不要急着发命令。PN532 内部有自己的启动流程从睡眠模式恢复到射频就绪需要一定时间。我实测下来唤醒后至少要延时 20ms 再发第一条命令才稳定。如果你用的是 GPIO 模拟的延时注意编译优化级别别开太高否则延时被优化掉是常有的事。延时 50ms 更保险对于读卡场景多这 30ms 根本感知不到。3.3 通用帧封装前导码、LCS、DCS 的计算模板PN532 的命令帧格式是固定的前导码 0x00、帧头 0xFF、长度字段、长度校验和 LCS、TFI0xD4 表示主机到 PN532、数据段、数据校验和 DCS、后帧 0x00。除了 TFI 和你的数据内容其余字段都需要计算或固定填入。如果校验和算错PN532 会直接丢弃整帧而且不会给你任何报错——主机只会发现 IRQ 一直不拉低。这类问题的排查特别费时所以封装一个通用发送函数是值得的。uint8_t pn532_send_command(uint8_t tfi, uint8_t *cmd, uint8_t len) { uint8_t frame[64]; uint8_t dcs 0; uint8_t i; frame[0] 0x00; /* 前导码 */ frame[1] 0xFF; /* 帧头固定 */ frame[2] len 1; /* 长度 数据长度 TFI 本身 */ frame[3] (~frame[2] 1) 0xFF; /* LCS 长度的二进制补码 */ frame[4] tfi; /* 0xD4 命令帧 / 0xD5 响应帧 */ dcs tfi; for (i 0; i len; i) { frame[5 i] cmd[i]; dcs cmd[i]; } frame[5 len] (~dcs 1) 0xFF; /* DCS 包含 TFI 和数据段 */ frame[6 len] 0x00; /* 后帧 */ for (i 0; i 7 len; i) { i2c_write_byte(PN532_I2C_ADDR_WRITE, frame[i]); } return 1; }DCS 的校验范围是从 TFI 开始包含 TFI 和所有数据字节但不包含前导码、长度和 LCS。这是最容易算错的地方——如果你把前导码也算进去了PN532 会认为校验不通过整体帧被丢弃。我这里写的 i2c_write_byte 是一个抽象函数实际项目里你可以把它替换成标准库的 I2C_SendData 加查标志位或者 HAL 库的 HAL_I2C_Master_Transmit。每次发送前记得等 I2C 总线不忙发送结束后要留出足够时间让 PN532 处理不要一条接一条地发。提个醒有些模组的固件版本较老或者被第三方改过对帧间延时特别敏感。如果你发现命令的响应时好时坏尝试在发送完一个字节后加一个微秒级小延时让 SCL 的每个字节之间有足够间隔。这类玄学问题通常都是电气时序问题可以用逻辑分析仪抓出来别把时间浪费在怀疑校验算法上。4. 读 UID 核心命令InListPassiveTarget 的帧构造与返回解析4.1 被动读卡原理为什么要引入 target 列表这个概念PN532 不是主动向周围发信号扫描卡片而是持续发射射频能量场有卡片进入场内它会感应到并响应。InListPassiveTarget 这条命令的作用是把当前场中出现的非接触卡加入一个列表然后返回每张卡的基本信息其中就包括 UID。为什么叫列表因为射频场内可以同时存在多张卡你可以设定最大目标数PN532 会把检测到的每张卡作为一个 target 依次列出来。这个命令的完整格式是TFI 0xD4、命令码 0x4A、然后两个参数——MaxTg最大目标数和 BaudRate波特率。MaxTg 我建议填 1对门禁这种一次刷一张卡的场景来说处理一个 target 比维护一个列表简单得多解析时也不用担心多卡返回带来的帧长度变化。BaudRate 决定你的读卡器支持哪些协议0x00 是 106kbps 的 ISO/IEC 14443A 型卡也就是最常见的 M1 卡、门禁卡0x01 是 106kbps 的 Felica 卡常见于部分手机 NFC 模拟0x02 是 106kbps 的 ISO/IEC 14443B 型卡身份证、部分校园卡走这个协议。有一个现象会困扰新手明明卡放在天线上了InListPassiveTarget 返回的 NbTg 却是 0。这时先别怀疑命令格式而是确认卡片类型是否在你的 BaudRate 设定范围内。很多卡片是 14443A 的但你如果因为参考代码用了 0x0214443B设置那自然读不到。我见过有人在 14443B 的设定下反复调天线匹配最后发现卡其实是 14443A白白浪费了一个晚上。4.2 构造 InList 命令并发送可直接抄进工程的函数下面的代码演示如何用前面封装好的 pn532_send_command 发送 InListPassiveTarget并等待 PN532 的响应。这里按裸机思路写如果你用的是 RTOS可以把最后的延时函数换成任务延时。uint8_t pn532_in_list_passive_target(void) { uint8_t cmd[3] {0x4A, 0x01, 0x00}; /* InList、MaxTg1、106kbps 14443A */ delay_ms(10); /* 等射频场稳定 */ if (pn532_send_command(0xD4, cmd, 3)) { return pn532_wait_response_timeout(500); /* 等 IRQ 或超时 */ } return 0; }延时 10ms 的原因在于上一次命令处理完后 PN532 需要时间重新稳定射频场太快发命令会导致命令进入内部缓冲区时旧状态还没清完响应帧合并或者丢失。pn532_wait_response_timeout 这个函数的内部逻辑是等待 IRQ 引脚拉低然后通过 I2C 读回响应帧。超时时间设 500ms对于一次刷卡操作来说是合理的——有人把卡放到天线上到响应返回通常不超过 100ms500ms 给 PCD 和 PICC 的防冲突流程留下足够余量。参数怎么改如果你的项目需要同时支持多种卡MaxTg 可以填 2 或 3但解析时要遍历每个 target 块如果你要支持 Felica 卡把第三个参数改成 0x01。我的建议是先固定 14443A 跑通全流程再扩展其他协议不要一开始就搞多协议并行。4.3 解析返回帧UID 在第几个字节一目了然响应帧的解析是读 UID 项目里最容易让新手发懵的部分。PN532 返回的一整串数据中各部分含义按顺序排列如下表所示。偏移长度内容说明010x00前导码110xFF帧头21L长度31LCS长度校验410xD5响应帧标识510x4BInListPassiveTarget 响应码61NbTg检测到的卡片数量71Tgtarget 编号通常为 181SENS_RES14443A 层应答91SEL_RES选择应答101NFCID 长度通常为 4 或 711NUID 内容从这开始才是卡号对应到代码里就是按偏移量取字节。注意如果 NbTg 是 0响应帧里就没有 target 块内容偏移 7 开始的数据含义完全不同所以代码里要先判断 NbTg。uint8_t pn532_read_uid(uint8_t *uid, uint8_t *uid_len) { uint8_t buf[64]; uint8_t i, len; if (!pn532_in_list_passive_target()) return 0; len pn532_read_response(buf, sizeof(buf)); if (len 14) return 0; /* 响应帧太短说明没有有效 target 块 */ if (buf[4] ! 0xD5 || buf[5] ! 0x4B) return 0; /* 协议层错误不是 InList 的响应 */ if (buf[6] 0) /* NbTg 0场上无卡 */ return 0; *uid_len buf[10]; /* NFCID 长度 */ for (i 0; i *uid_len; i) { uid[i] buf[11 i]; /* 复制 UID 内容 */ } return 1; }buf[10] 是 UID 长度buf[11] 开始才是真正的卡号这个结论只适用于标准长度的响应帧。什么情况下会变当响应帧中出现扩展长度字段长度超过 255 字节时偏移会整体往后挪但读 UID 的响应帧长度通常只有二十几个字节扩展帧不会在 14443A 单卡场景中出现。还有一个坑是 7 字节 UID 的卡片比如部分 NTAG213 或身份证模拟卡返回的 NFCID 长度是 7你申请缓冲区时至少要留 7 个字节别只留 4 个。我一般会在解析前加一段调试打印把 buf 的前 16 个字节通过串口以十六进制形式输出手动核对一遍偏移量之后再去写业务逻辑。很多现成的库把 target 块定义成结构体新手手抄代码时经常对不齐结构体边界反而搞不清楚到底该从哪取 UID。自己打印对照一次比看任何解析教程都管用。4.4 完整主流程从上电到打印 UID 的最短路径代码分装好之后主程序的调用顺序决定了你第一次能不能跑通。最小可用的流程是上电延时、唤醒 PN532、读取固件版本验证通信、循环读 UID 并串口打印。每一步都有可观测的输出这样如果通信有问题你能立刻判断是 I2C 没通还是卡片没识别到而不是在一堆堆栈日志里盲找。void main(void) { uint8_t uid[7] {0}; uint8_t uid_len 0; SystemInit(); UART_Configuration(); I2C_Configuration(); delay_ms(500); /* 等待 PN532 上电启动完成 */ pn532_wakeup(); /* 向 0x48 写 0x00唤醒模块 */ if (pn532_get_firmware_version() 0) { printf(PN532 not found.\n); while (1); /* 通信失败在此卡住 */ } printf(PN532 OK.\n); while (1) { if (pn532_read_uid(uid, uid_len)) { printf(UID: ); for (int i 0; i uid_len; i) printf(%02X , uid[i]); printf(\n); } else { printf(No card.\n); } delay_ms(1000); /* 读卡间隔至少 1 秒 */ } }delay_ms(500) 不是随便拍的——PN532 从 VCC 上电到固件完全初始化好的时间大约 200ms500ms 留足了余量。pn532_get_firmware_version 发的是 0x02 命令返回值的最后字节是固件版本号这个步骤的意义是确认整个 I2C 链路工作正常。如果版本都读不到后面读 UID 全是白费力气先回头查接线、上拉和唤醒时序。主循环里的 1 秒延时是防误报的手段。当同一张卡一直放在读卡区时PN532 会反复返回同一张 UID如果你的业务逻辑是每次变化才上报这 1 秒可以滤掉抖动。但如果你做的是连续读卡测试可以把延时改成 100ms注意别把模块搞到疲劳状态——长时间连续读卡会让天线驱动芯片发热读卡距离会下降这是射频器件通病。5. 读 UID 常见问题排查五个翻车现场与修复顺序5.1 上电后第一条命令必失败PN532 启动比 STM32 慢现象烧录程序后串口打印“PN532 not found”程序卡死在 while 循环里手动按复位键重新启动却一切正常。原因STM32 上电后几百微秒就执行到初始化代码而 PN532 的固件启动需要几十毫秒甚至更久取决于模块板载电容充电时间。在 PN532 还没进入可响应状态时发命令自然不会收到任何 ACK。解决在主程序最开始加至少 500ms 延时放在 I2C 初始化之后、发第一条命令之前。这个坑几乎人人都会遇到区别只是有人用复位键掩盖了它有人在代码里留了延时。我现在的习惯是开一个独立的上电标志位等延时结束后置位后续逻辑只有在标志位就绪后才执行避免调试时反复手动复位。5.2 从机地址填 0x24逻辑分析仪却看不到 ACK现象发送命令后 IRQ 一直不拉低逻辑分析仪抓 SCL/SDA 发现主机在发地址字节后收到一个 NACK通信直接中止。原因0x24 是 PN532 的 7 位设备地址I2C 协议规定地址字节由 7 位地址加 1 位读写标志组成。向设备写数据时总线上发出的字节是 0x24 1 0x48读数据时是 0x49。如果你把 0x24 直接传给库函数它原样发到总线上PN532 看到的是地址 0x12 xx自然不应答。解决写地址填 0x48读地址填 0x49并把这两个值定义成宏在代码里通过宏名引用不要随手填裸数字。用 HAL 库的 DevAddr 参数也一样都是左移后的地址。这个坑的可怕之处在于代码编译不报错逻辑分析仪能看到总线活动但设备就是没反应最容易让人误判为硬件损坏。5.3 SCL 频率设 400kHz 后偶尔读到 0xFF现象大部分时间读卡正常但在卡快速靠近天线或者环境温度变化时串口偶尔打印出 0xFFFF 或者 0x0000 这样明显错误的数据。原因400kHz 快速模式下I2C 信号的上升沿时间必须控制在 300ns 以内而 PN532 模组上的走线长度、上拉电阻值、寄生电容都会影响上升沿。上拉偏大或走线过长时400kHz 下信号完整性不足主控采样到错误电平PN532 收到的命令帧校验失败返回空响应或者乱码。解决把 I2C_ClockSpeed 降回 100000 标准模式重新测试。读一张卡的 UID 整个交互不超过 10ms100kHz 带来的性能影响完全可接受没必要为了数据手册上的理论最高速率增加调试难度。如果项目确实需要 400kHz至少把上拉电阻调整到 2.2k 到 4.7k 之间再用示波器量上升沿。5.4 IRQ 引脚悬空导致响应帧不完整现象程序能发送命令但读取响应时总是超时或者读回来的数据长度不稳定偶尔只有半截帧。原因PN532 在 I2C 模式下不是马上返回数据而是先把响应放到内部缓冲区然后拉低 IRQ 引脚通知主机。如果 IRQ 引脚悬空或者没有配置为输入你的程序只能靠固定延时时长去猜数据什么时候准备好猜早了读到的还是空猜晚了可能被下一帧覆盖。解决把 IRQ 接到 STM32 的 GPIO 输入引脚代码里在发送命令后等待 IRQ 变低再接 I2C 读操作。等待循环要设超时时间我一般用 300ms超过就返回失败。注意 IRQ 引脚不要开内部上拉因为 PN532 是开漏驱动需要外部上拉到 3.3V内部上拉阻值太大可能拉不低。5.5 读卡距离只有 1cm稍微拿远就没反应现象卡片必须贴着天线才能读取距离超过 1cm 就显示 No card和市面上成品读卡器的识别距离差距明显。原因天线匹配电路没有调到谐振点。PN532 的射频输出需要经过 LC 匹配网络到天线线圈匹配电容的容值、线圈匝数和尺寸都会影响谐振频率。如果你的模块是买的一体化成品出厂已经调好通常不会出现这个问题如果你自己绕天线、或者把模块天线拆开重新接过那大概率是匹配不准。解决检查天线线圈的匹配电容是否和 PN532 参考设计一致天线面积大时适量调大串联电容的容值。没有网络分析仪的情况下可以尝试稍微改变线圈匝数每次调整后测试读卡距离找到最优匝数。这个坑本质上是射频问题不是 I2C 问题但很多人因为在 I2C 上查了半天没结果最后才发现是天线的锅。6. 从读 UID 到量产滚动检测、调试保留与验证清单U 盘 UID 读出来了离一个能用的读卡器还差一步如何避免同一张卡放在读卡区时被重复上报。门禁机的正确行为是刷卡瞬间触发一次而不是每秒触发一次。做法是维护一个全局数组保存上一次的 UID 和长度每次读到新 UID 时先和旧值比较相同就忽略不同才上报。这里有个隐藏细节上一次读到 7 字节 UID这次只读到 4 字节前一次的数据后半段会残留在缓冲区里导致比较时永远不等。所以每次读卡前先把 UID 缓冲区清零再调用读函数逻辑才不会出错。另一个值得养成的习惯是保留调试打印口。很多人调通一次读 UID 后就把 printf 全注释掉等到换卡型、改天线、换模组版本后读不到又得重新打开打印。我的做法是在 pn532_read_uid 函数入口处放一个条件编译开关平时关闭遇到问题打开重编一版就能看到完整响应帧。提一句验证固件版本时0x02 命令的返回值里包含芯片型号、固件主版本、次版本不同批次模组的数值会有差异不要用固定值做相等判断改成打印出来人工确认即可。如果你准备把这个方案收到自己的产品里还有一个最常见的坑要提前堵住不同厂商的卡虽然都说兼容 14443A但 SENS_RES 和 SEL_RES 这两个字节在部分卡上会有细微差异PN532 在防冲突流程中依赖这两个字段做类型判断。换卡测试时要多准备几张不同品牌的卡至少在三种类型上验证过你用的 PN532 模组都能稳定返回 UID。希望这篇文能帮你把读 UID 这条路走得比我当年顺一点少踩那些不至于让项目失败、但足够浪费你一整天的坑。本文还有配套的精品资源点击获取