资讯动态

STM32驱动PN532串口读卡完整指南:从HSU协议到UID解析

发布时间:2026/10/20 3:06:22 来源:尧图企业网站定制
简介面向STM32嵌入式开发者的NFC驱动资源包以PN532串口驱动为核心讲解在STM32上实现NFC标签读写的方法。内容涵盖UART引脚选择如PA9/PA10、115200bps波特率配置、PN532初始化命令、标准帧结构解析、NDEF数据读写流程以及中断驱动优化并配有基于STM32F103C8T6与HAL库的示例工程可直接移植到物联网、移动支付、智能门禁等场景。资源共211个文件以43个h头文件、41个c源文件及32个s汇编文件为主体另含工程配置文件、链接脚本、编译产物和少量文档压缩包约3.7MB目录结构清晰便于按模块查看驱动实现与协议封装。已有1804人学习下载。读者可获得可编译的驱动模板、关键数据帧说明及排错思路减少底层协议开发与调试时间。1. 这个驱动解决什么问题串口接PN532为什么不能直接读卡刚接触STM32和PN532的时候几乎所有人都会干一件“理所当然”的事把模块的TXD、RXD直接接到串口上然后往串口丢一串ASCII命令指望卡贴上去就能读到卡号。结果往往是串口助手一片空白或者收到一堆乱码。原因是PN532的串口不是那种“发AT指令”的普通串口它跑的是HSU协议——所有数据都要按专用帧格式打包还要先给芯片做SAMConfig配置它才愿意进入寻卡模式。这套STM32的PN532串口驱动就是把“组帧、校验、发命令、等待ACK、解析响应”这些脏活全封装好的落地工程。它的核心能力一句话概括把PN532接到STM32的任意一个USART上调用几个API就能完成初始化、寻卡、读UID并且留好了往Mifare数据块读写、NDEF文本解析方向扩展的接口。适合三类人正在做门禁/读卡器/智能硬件原型的嵌入式开发者、手头有PN532模块但一直卡在协议细节上的初学者、以及想在项目里快速集成NFC但不想从零啃数据手册的人。2. 协议与架构PN532 HSU帧格式与驱动分层设计2.1 HSU串口协议每个字节都是算出来的PN532在HSU模式下默认波特率为1152008N1空闲时TXD输出高电平。看起来和普通串口没有任何区别但通信内容完全不同。主机发给PN532的任何一条指令都必须按下面的帧结构组织字节位置名称说明0前导码固定0x001起始码固定0xFF2LEN数据区长度含TFI、指令码和参数不含校验字节3LCS长度校验码0x100 - LEN 取低8位4TFI主机发0xD4PN532回0xD55..NData指令码 参数N1DCS数据校验码0x100 - (TFI 所有数据字节之和) 取低8位举个例子读固件版本命令GET_FIRMWARE_VERSION的指令码是0x02没有参数。完整发送帧是00 00 FF 03 FD D4 02 FC。其中LEN0x03是因为数据区包含D4、02两个字节但LEN本身不算自身只算它后面的数据字节数所以是2个数据字节加1个TFI这里要特别注意PN532数据手册里的LEN指的是“从TFI开始到最后一个数据字节的字节数”也就是说D4、指令、参数都在内共3个字节所以LEN0x03LCS0xFD。DCS则是对0xD4和0x02求和取补码0x100-(0xD40x02)0x100-0xD60x2A不对我重新算一下。0xD40x020xD60x100-0xD60x2A。所以完整帧应该是00 00 FF 03 FD D4 02 2A。但是网上很多参考帧写的是00 00 FF 03 FD D4 02 2A有一部分人写错了写成了FC原因就是把LEN错算成2。按这个帧发出去PN532也会回ACK但它会认为帧校验失败而丢弃命令。要验证你的计算对不对最直接的办法是把帧扔进串口调试助手发给PN532看它回什么——只要回的是00 00 FF 00 FF 00这种ACK帧说明帧格式对了一半。PN532的通信流程是固定的两步主机发命令帧 → PN532回一个ACK帧表示“收到” → 再回真正的响应帧。ACK帧是固定的6个字节00 00 FF 00 FF 00没有长度校验和数据校验因为它是个特殊帧。真正的响应帧结构和请求帧一样只是TFI变成了0xD5。这个“先ACK再响应”的时序是新手最容易翻车的地方很多人发了命令就去读串口结果读到了ACK就以为收到响应了然后解析出错。2.2 驱动分层把协议细节和业务逻辑分开这套工程在结构上明确分成了三层目的很实际以后换了I2C或SPI接口的PN532或者从HAL库换成标准外设库都只需要改最底下那层上面的命令层和应用层一行不动。层级文件职责物理层pn532_uart.c串口收发字节、复位引脚控制、延时函数适配命令层pn532.c组帧、校验、发送命令、接收响应、解析数据应用层app_nfc.c业务逻辑寻卡回调、读UID、上层状态机命令层是整个驱动的核心它不关心命令拿去干什么只负责保证“发出去的帧一定合法收回来的帧一定被校验过”。应用层则是在命令层基础上做场景封装比如“上电后初始化NFC”“循环寻卡并把UID打印到串口”这种代码普通人一读就懂。完整目录结构如下project ├─ Core/ │ ├─ main.c │ └─ stm32f1xx_it.c ├─ Drivers/ │ └─ STM32F1xx_HAL_Driver/ ├─ BSP/ │ ├─ pn532.h │ ├─ pn532.c │ ├─ pn532_uart.h │ └─ pn532_uart.c └─ App/ ├─ app_nfc.h ├─ app_nfc.c └─ ndef_parser.c从依赖关系看app_nfc.c只调用pn532.h里暴露的APIpn532.c只调用pn532_uart.h里的串口收发函数。这样各层之间是单向依赖不会出现应用层直接操作寄存器的情况。对于想二次开发的人来说只需要看懂pn532.c里的3个核心函数整条NFC链路就通了。3. 核心代码拆解从串口发帧到读回UID的完整链路3.1 发帧封装组帧、校验、发送一气呵成所有发给PN532的命令最终都汇聚到这一个函数。它的输入是命令码和参数数组函数内部完成LEN、LCS、DCS的计算最后通过HAL串口发出去。代码如下uint8_t PN532_SendCommand(uint8_t cmd, uint8_t *params, uint8_t param_len) { uint8_t frame[258]; uint8_t data_len param_len 2; // TFI cmd params uint8_t total_len data_len 5; // 前导 起始 LEN LCS DCS uint8_t dcs 0xD4 cmd; frame[0] 0x00; // 前导码 frame[1] 0xFF; // 起始码 frame[2] data_len; // 数据区长度 frame[3] 0x100 - data_len; // 长度校验 frame[4] 0xD4; // 主机发往PN532的TFI frame[5] cmd; // 指令码 if (params ! NULL param_len 0) { memcpy(frame[6], params, param_len); for (uint8_t i 0; i param_len; i) { dcs params[i]; } } frame[total_len - 1] 0x100 - dcs; // 数据校验 HAL_UART_Transmit(huart1, frame, total_len, 100); return PN532_STATUS_OK; }注意data_len param_len 2这2个字节就是TFI和指令码。LCS不是对整个帧求和而是对LEN本身求补所以即使前面帧头写错了只要LEN和LCS不匹配PN532就会把整帧丢弃。DCS是对TFI、指令码、所有参数加起来求补注意它不包含LEN和LCS很多参考代码在这里有误导。这个函数的参数很好理解cmd是PN532的指令宏比如PN532_CMD_SAMCONFIG是0x14PN532_CMD_INLISTPASSIVETARGET是0x4Aparams是命令参数的首地址param_len是参数个数。使用时的常见坑是忘记把数组长度和实际参数个数对齐多传一个0x00或少传一个0x00都会导致LEN变长或变短PN532直接不回帧。3.2 接收响应先吃ACK再吃响应帧发送只是前半程接收才是判断命令是否成功的依据。接收函数的处理逻辑是发送完成后先收6字节的ACK帧跳过它然后再收真正的响应帧头接着根据帧头里的LEN收完剩余数据最后做一遍DCS校验。代码如下static uint8_t PN532_ReadResponse(uint8_t *resp, uint8_t *resp_len) { uint8_t header[6]; uint8_t frame[260]; uint8_t data_len; uint8_t sum 0; // 第一步收ACK帧 00 00 FF 00 FF 00 HAL_UART_Receive(huart1, header, 6, 1000); if (header[0] ! 0x00 || header[1] ! 0x00 || header[2] ! 0xFF || header[3] ! 0x00) { return PN532_STATUS_ERR_ACK; } // 第二步收响应帧头 00 00 FF LEN LCS HAL_UART_Receive(huart1, header, 5, 1000); if (header[0] ! 0x00 || header[1] ! 0x00 || header[2] ! 0xFF) { return PN532_STATUS_ERR_FRAME; } data_len header[3]; if (header[4] ! (0x100 - data_len)) { return PN532_STATUS_ERR_CHKSUM; } // 第三步收数据区 DCS HAL_UART_Receive(huart1, frame, data_len 1, 1000); for (uint8_t i 0; i data_len; i) { sum frame[i]; } sum frame[data_len]; // 加上DCS if ((sum 0xFF) ! 0x00) { return PN532_STATUS_ERR_CHKSUM; } memcpy(resp, frame, data_len); *resp_len data_len; return PN532_STATUS_OK; }这个函数里有几个值得细看的点。第一ACK帧的校验只检查前4个字节因为后面FF 00是固定的确认序列如果前4字节匹配基本可以认定是ACK第二响应帧头收了5字节也就是00 00 FF LEN LCS这里LEN才是真正要紧的它决定了后面还要收多少数据第三DCS的验算用了一个技巧——把数据区所有字节和DCS求和正确结果应该等于0x100的整数倍也就是低8位为0x00不用再做减法。有人会问如果PN532发命令后板子响应太慢HAL_UART_Receive超时1000ms够不够实际测试下来PN532对绝大多数指令的响应时间在10ms以内只有寻卡命令在卡片不在场时会等到自己内部超时才回帧这个超时时间由SAMConfig的timeout参数决定默认1秒。所以接收超时设在1000ms是合理的短时间内不会误判。3.3 SAMConfig初始化让PN532进入正常工作模式PN532上电后默认处于待机状态必须发送SAMConfig命令它才进入配置好的工作模式。SAMConfig的指令码是0x14参数固定为3个字节。代码如下uint8_t PN532_SAMConfig(uint8_t mode, uint8_t timeout, uint8_t irq_en) { uint8_t params[3]; uint8_t resp[32]; uint8_t resp_len; params[0] mode; // 0x01: 正常模式, 0x02: 虚拟卡模式 params[1] timeout; // 超时时间, 单位 50ms params[2] irq_en; // IRQ引脚中断使能 if (PN532_SendCommand(PN532_CMD_SAMCONFIG, params, 3) ! PN532_STATUS_OK) { return PN532_STATUS_ERR_SEND; } if (PN532_ReadResponse(resp, resp_len) ! PN532_STATUS_OK) { return PN532_STATUS_ERR_RECV; } // 响应数据区第一个字节是0x15表示成功 if (resp[0] ! PN532_CMD_SAMCONFIG 1) { return PN532_STATUS_ERR_NACK; } return PN532_STATUS_OK; }很多人在这个环节把mode填成0x02然后发现PN532可以被手机读到、却读不到自己的卡——因为0x02是让PN532模拟一张NFC卡而不是读卡。正常读卡场景必须填0x01。timeout填0x14即20个50ms总共1秒含义是“寻卡等待超过1秒就返回超时”。IRQ使能这里填0x01让PN532通过IRQ引脚告诉主机“有响应了”如果没接IRQ引脚填0x00也行但要注意读卡响应只能靠串口数据判断。3.4 寻卡与读UIDInListPassiveTarget完整流程寻卡指令InListPassiveTarget的命令码是0x4A参数通常是两个字节MaxTg1和BaudRate0。其中BaudRate为0表示106kbps的NFC-A协议也就是Mifare Classic和Mifare Ultralight这类最常见的卡。完整代码如下uint8_t PN532_InListPassiveTarget(uint8_t *uid, uint8_t *uid_len) { uint8_t params[2] {0x01, 0x00}; uint8_t resp[64]; uint8_t resp_len; if (PN532_SendCommand(PN532_CMD_INLISTPASSIVETARGET, params, 2) ! PN532_STATUS_OK) { return PN532_STATUS_ERR_SEND; } if (PN532_ReadResponse(resp, resp_len) ! PN532_STATUS_OK) { return PN532_STATUS_ERR_RECV; } // 响应格式D5 4B nbTg Tg ATQA_H ATQA_L SAK UIDlen UID... if (resp[0] ! 0xD5 || resp[1] ! 0x4B) { return PN532_STATUS_ERR_NACK; } if (resp[2] 0x00) { return PN532_STATUS_ERR_NOTAG; // 没读到卡 } *uid_len resp[7]; memcpy(uid, resp[8], *uid_len); return PN532_STATUS_OK; }响应帧的解析是整个驱动里最容易算错偏移的地方。响应数据区从resp[0]开始依次是D5 4B nbTg Tg ATQA_H ATQA_L SAK UIDlen UID...。其中resp[2]是找到的目标数量为0说明没有卡resp[7]是UID长度通常是4字节或7字节UID本体从resp[8]开始。如果代码里写的是resp[6]或者resp[5]拿到的就是错的UID——这个坑相当隐蔽因为有时候UID恰好只有4字节错一位可能也能对上但换了7字节的卡就全乱了。4. 环境搭建CubeMX接线、配置与串口验证4.1 硬件接线与电平匹配PN532模块的引脚功能在串口模式下非常简单但有一个地方必须注意模块的TXD、RXD是3.3V TTL电平和STM32的USART引脚电平是一致的可以直接互联。不过市面上存在两种常见的PN532模块红色模块带5V稳压芯片白色模块是3.3V直供两者串口逻辑电平都是3.3V直接接STM32没有风险。真正需要警惕的是那些把电平转换芯片焊在板子上的模块如果模块上有SP232E之类的芯片串口引脚可能是RS232电平就不能直连。以下是标准接线表PN532引脚STM32引脚说明VCC3.3V模块供电注意电流约100mAGNDGND必须共地否则串口会乱码TXDPA10 (USART1_RX)模块发、主机收RXDPA9 (USART1_TX)主机发、模块收RSTOPB0复位控制拉低复位、拉高运行CTSPB1流控引脚拉低表示主机就绪重点说下CTS这根线。PN532的HSU模式默认启用硬件流控CTS输入为低时芯片才允许发送数据。如果模块上的CTS悬空内部上拉会让它处于高电平PN532就一直不往外发数据表现就是“主机发命令没反应但模块好像也活着”。解决方法是把CTS接到STM32任意一个GPIO配置为推挽输出并置低或者直接在模块侧把CTS和GND短接。RTS是输出信号可以留着不接。4.2 CubeMX串口配置要点STM32CubeMX里只需要配置一个USART参数按下面设置模式Asynchronous波特率115200字长8 Bit校验None停止位1 Bit方向TX和RX都使能PB0和PB1配置为GPIO_Output初始化时PB0输出高电平让PN532退出复位PB1输出低电平让PN532的CTS有效。生成代码后在main函数里加初始化调用int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); // 释放PN532复位拉高RSTO HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_Delay(500); // NFC初始化进入正常读卡模式 PN532_SAMConfig(0x01, 0x14, 0x01); while (1) { uint8_t uid[8]; uint8_t uid_len 0; if (PN532_InListPassiveTarget(uid, uid_len) PN532_STATUS_OK) { printf(UID[%d]: , uid_len); for (uint8_t i 0; i uid_len; i) { printf(%02X , uid[i]); } printf(\r\n); HAL_Delay(500); // 防止连续读同一张卡刷屏 } HAL_Delay(100); } }这段代码就是整个工程的骨架上电先把PN532从复位状态释放等500ms让它内部固件跑起来然后SAMConfig进入正常模式主循环里反复寻卡。HAL_Delay(500)放500ms是为了避免卡一直贴在读写器上时程序疯狂刷日志这个时间按业务需求随意调。4.3 用PC串口助手验证模块好坏如果你发现板子读卡没反应第一步不是查代码而是先确认PN532模块本身是好的。把模块的TXD、RXD用USB转串口接到PC打开串口助手波特率115200用HEX发送模式先发送一帧唤醒帧55 55 00 00 00 00。注意PN532在HSU模式下有个特殊行为连续收到4个以上的0x00会唤醒它。但这帧不是标准命令发送后PC端不需要期待ACK。真正的验证是发固件版本命令发送HEX帧00 00 FF 03 FD D4 02 2A如果模块正常PC串口助手会先收到00 00 FF 00 FF 00的ACK帧然后收到响应帧00 00 FF 05 FB D5 03 02 00 06 07。响应帧里D5 03表示“响应的是GET_FIRMWARE_VERSION命令”后面02 00 06 07是固件信息。如果你收到的是00 00 FF 01 FF D5 03 73之类的NACK帧说明命令没被正确解析多半是帧校验码算错了。用PC验证的好处是把问题边界划清楚模块没问题再回头看STM32的串口配置模块没响应基本就是接线或供电问题。这套排查顺序能让调试时间省一半。5. 踩坑排查5个导致读不到卡的真问题5.1 上电就发命令模块完全没有反应现象STM32上电后立刻调用PN532_SAMConfig回包超时模块指示灯不亮或闪烁异常。原因PN532的复位脚RSTO如果悬空芯片可能一直处于复位态或者刚从复位释放但内部LDO还没稳定此时任何串口命令都会被丢弃。很多模块的原理图上RSTO是有上拉电阻的但如果你用杜邦线连接RSTO到PB0且PB0初始化时输出低电平等于把模块按在复位状态。解决PB0在初始化阶段先输出低电平等STM32和串口外设都初始化完成后再把PB0拉高并延时至少500ms。我一般会拉到1秒让模块的晶振和内部锁相环完全稳定。从那以后我接所有NFC模块第一件事就是确认复位引脚的GPIO初值。5.2 能发命令但收不到ACK串口示波器能看到波形现象用逻辑分析仪抓TXD引脚能抓到STM32发出的完整帧但RX引脚一直收不到任何数据。原因CTS引脚悬空导致PN532的发送被硬件流控卡死。PN532的HSU默认启用流控它的CTS输入悬空时内部下拉实际上很多模块的CTS脚内部没有上下拉悬空状态不确定PN532会认为主机未就绪。解决把CTS引脚用杜邦线接到GND或者在CubeMX里配一个GPIO输出低电平接到CTS。如果你用的模块是成品板板上已经把CTS和GND短接那这个坑就不会出现。判断方法很简单用万用表量CTS对GND的电压如果接近0V就是被拉低了如果悬空就要手动处理。5.3 寻卡响应一直是0个目标但PC端验证模块是好的现象固件版本命令能正常响应SAMConfig也返回成功但InListPassiveTarget永远返回PN532_STATUS_ERR_NOTAG。原因最可能的情况是SAMConfig的mode被填成了0x02。0x02是虚拟卡模式PN532把自己模拟成一张卡所以它不会主动寻卡。另一个可能原因是天线线圈没有调谐卡贴得再近也耦合不到能量。解决确认SAMConfig的第一个参数是0x01。如果参数没问题换一张Mifare Ultralight或者Mifare Classic测试因为InListPassiveTarget的BaudRate0只覆盖Type A协议如果你拿的是Type B的身份证卡它当然读不到。5.4 能读到UID但偶尔读到的UID是乱的现象同一张卡反复放上去大部分时间读到正确UID偶尔读出来少一个字节或者多两个字节有时是FF FF FF FF这种非法值。原因多半是串口接收和命令发送的竞态问题。当前代码是阻塞式收发如果主循环里寻卡指令刚发出去PN532的ACK还没回来这时候系统又发了一条新指令会把PN532的状态机搞乱。解决给接收加上帧同步逻辑读到非法帧头就丢弃并重新等待而不是直接把错误帧返回给上层。更彻底的办法是给PN532的命令发送加一个互斥锁在响应超时之前不允许再次发送。我在这个工程里用了一个简单的busy标志位发送前检查响应完成后才清除。5.5 连续读卡几次后模块彻底死机必须断电恢复现象读卡前几次正常第5次或第10次开始无响应看门狗也没用只能断电重启。原因NFC协议规定主机在读完卡数据后要主动释放目标。如果每次寻卡成功但没有对卡片做释放操作PN532会把目标保持在激活状态内部缓冲区占满后状态机崩溃。解决在读完UID或数据后调用InRelease命令指令码0x52释放目标uint8_t PN532_InRelease(uint8_t tg) { uint8_t params[1] {tg}; // 目标编号通常为0x01 uint8_t resp[16]; uint8_t resp_len; PN532_SendCommand(0x52, params, 1); PN532_ReadResponse(resp, resp_len); return resp[0] 0x53 ? PN532_STATUS_OK : PN532_STATUS_ERR_NACK; }这段代码的坑在于NFC-A目标释放后PN532返回的响应数据区是D5 53如果你的判断条件写错会以为释放失败但实际已经释放了。另外如果卡片是被强制拿开的PN532内部也会自动检测到场强消失并清理目标所以这个释放不是每次都必须调用但养成习惯能避免上面的死机问题。6. 二次开发思路从读卡号到读写卡片数据块6.1 把命令层从读UID扩展到Mifare数据读写读UID只是NFC的最表层很多实际场景需要进一步读写卡内的数据块。Mifare Classic的数据读写走的是InDataExchange命令指令码0x40。它和寻卡命令的区别是必须先通过InListPassiveTarget找到一个目标然后针对这个目标交换数据。Mifare Classic的认证命令是uint8_t PN532_MifareAuth(uint8_t block, uint8_t key_type, uint8_t *key) { uint8_t params[16]; uint8_t resp[32]; uint8_t resp_len; params[0] 0x01; // 目标编号 params[1] key_type ? 0x61 : 0x60; // 0x60A密钥, 0x61B密钥 params[2] block; // 要认证的块号 memcpy(params[3], key, 6); // 6字节密钥 PN532_SendCommand(0x40, params, 9); PN532_ReadResponse(resp, resp_len); return resp[2] 0x00 ? PN532_STATUS_OK : PN532_STATUS_ERR_AUTH; }注意这里InDataExchange的数据区比之前的命令多了目标编号Tg而且响应数据区的前两个字节是D5 41第三个字节才是状态字。这个状态字等于0x00才表示认证通过常见值是0x01表示密钥错误0x02表示块号不存在。认证通过后读块数据就是把参数换成{0x01, 0x30, block}响应区从第三个字节开始就是16字节的块数据。6.2 NDEF文本解析往应用层挂一个轻量解析器Mifare Classic的NDEF文本通常从块4开始块的前4字节或第1字节是TLV结构标记。解析逻辑很简单读块4检查0x03开头的TLV标志位接着读长度字节最后把后面的内容按Text记录格式拆出来。我通常会在ndef_parser.c里放一个专门处理NDEF文本的轻量函数只提取第一条Text Record的UTF-8内容不做通用NDEF框架。因为电子的项目里百分之八十的需求只是“刷一下卡把字符串读出来”通用解析器反而增加代码体积和调试成本。6.3 把阻塞式收发换成中断加状态机当前驱动用HAL_UART_Transmit和HAL_UART_Receive在单任务场景下完全够用。但如果你要同时驱动蜂鸣器、数码管甚至蓝牙模块阻塞式串口会把CPU卡死。我一般的改造套路发送仍然用阻塞式命令很短微秒级接收改成HAL_UART_Receive_IT配合一个环形缓冲区在串口中断里把字节塞进队列命令层从队列里按帧同步解析。这样寻卡期间主循环还能做别的事响应速度快不少卡顿感几乎消失。给新手一个忠告别一上来就追求非阻塞先把阻塞式整个链路跑通再动接收方式。我当年就是直接上手DMA结果帧边界怎么都对齐不了最后反过头来用阻塞式逐字节调试才找到问题——从那以后每次换新NFC模块我都先用最笨的方式验证硬件链路再考虑性能优化。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑