ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32驱动PN532实现NFC读写实战指南

STM32驱动PN532实现NFC读写实战指南 简介本资源是一套基于STM32平台的PN532 NFC/RFID近场通信模块驱动与应用Demo源码面向嵌入式初学者及硬件开发工程师解决NFC卡片识别、ID读取与基础通信协议对接等典型开发需求。压缩包共665个文件涵盖371个C源文件含主控逻辑、底层驱动与协议解析、145个头文件定义寄存器映射与API接口、71个汇编启动文件及48个IAR工程配置文件.icf另有Hex固件、调试脚本.bat、项目工程文件.uvprojx/.mxproject及原理图参考.jpg整体大小6.85MB。已有814人学习下载。代码结构清晰包含完整HAL库初始化流程、串口中断接收、PN532唤醒与被动寻卡InListPassiveTarget核心逻辑并提供多版本编译环境支持Keil/IAR/STM32CubeIDE附带详细函数注释与调试打印便于快速移植与协议层深入理解。1. PN532不是“读卡器插USB就能用”的黑盒——它需要STM32亲手握紧SPI/I2C时序、解析NFC协议帧、处理卡片防冲突与UID校验你拿到一个标着“STM32读写NFC_RFID模块PN532的DEMO例程源码.zip”的压缩包解压后看到main.c、pn532.h、stm32f10x_conf.h却连LED都不闪——这不是代码有bug而是你还没真正理解PN532在STM32系统里的角色它既不是即插即用的U盘也不是靠HAL库自动适配的外设。PN532是协议级协处理器必须由STM32主动发起命令、等待响应、校验CRC、解析TLV结构、管理卡片状态机。常见误区是直接调PN532_ReadCardID()就期待返回一串十六进制UID结果卡在WaitForDataReady()死循环里更隐蔽的问题是SPI模式下MISO引脚浮空导致接收数据错位或I2C地址写成0x24却忘了硬件跳线实际配置为0x48。本篇不讲抽象概念只聚焦真实开发链路从硬件接线定义开始到能稳定读出Mifare Classic 1K卡的4字节UID非伪UID再到写入指定扇区数据并验证回读——所有步骤均基于STM32F103C8T6最小系统PN532模块v3.1或v4.0实测验证命令参数、超时阈值、CRC校验逻辑全部公开可复现。1.1 硬件连接决定驱动层选型SPI比I2C快3倍但需额外4根线UART模式仅适合调试PN532支持三种通信接口SPI最高5MHz、I2C标准模式100kHz/快速模式400kHz、UART115200bps。在STM32嵌入式场景中SPI是首选——它规避了I2C总线仲裁冲突风险尤其当系统已挂载OLED、EEPROM等I2C设备时且传输速率足够支撑NFC命令交互典型命令帧64字节。但SPI接线必须严格对应PN532引脚STM32F103C8T6引脚说明VCC3.3V严禁接5VPN532芯片耐压仅3.6VGNDGND共地是通信前提SCKPA5 (SPI1_SCK)主机时钟输出MOSIPA7 (SPI1_MOSI)主机数据输出MISOPA6 (SPI1_MISO)主机数据输入PWDNPB0低电平使能高电平休眠可悬空默认使能IRQPB1中断请求PN532拉低表示就绪必须接上提示若使用I2C模式需将PN532模块上的JP1跳线帽移至I2C侧并确认SCL/SDA上拉电阻为4.7kΩSTM32内部上拉不可靠。UART模式仅推荐用于PC端调试工具如NFC Tools因STM32串口资源紧张且无硬件流控易丢帧。1.2 协议栈不是“拿来就跑”——PN532命令帧结构决定你必须手写CRC16校验与状态解析PN532通信基于固定帧格式0x00 0x00 0xFF LEN LCS CMD DATA DCS 0x00。其中LCS是LEN取反DCS是CMDDATA字节异或再取反任何一字节错误都会导致整帧被丢弃。DEMO例程常忽略这点直接发送原始命令结果PN532返回0x00 0x00 0xFF 0x00 0x00 0x00 0x00错误响应帧。正确做法是封装CRC计算函数// 计算PN532命令帧DCSData Checksum uint8_t pn532_calc_dcs(uint8_t *data, uint8_t len) { uint8_t dcs 0; for (uint8_t i 0; i len; i) { dcs ^ data[i]; } return dcs; } // 构建完整命令帧以GetFirmwareVersion为例 void pn532_build_cmd_frame(uint8_t *frame, uint8_t cmd, uint8_t *payload, uint8_t plen) { frame[0] 0x00; // Preamble frame[1] 0x00; frame[2] 0xFF; frame[3] plen 1; // LEN payload_len cmd_len frame[4] ~frame[3]; // LCS frame[5] cmd; for (uint8_t i 0; i plen; i) { frame[6 i] payload[i]; } frame[6 plen] pn532_calc_dcs(frame[5], plen 1); // DCS over CMDPAYLOAD frame[7 plen] 0x00; // Postamble }注意pn532_calc_dcs()必须对CMD和PAYLOAD所有字节异或而非仅payload。实测发现某DEMO例程漏算CMD字节导致InListPassiveTarget命令始终失败。2. 用SPI在STM32F103上跑通PN532最小命令——从初始化到读取Mifare卡UID的7步硬核操作2.1 初始化SPI外设时钟极性/相位必须匹配PN532硬件要求PN532 SPI接口要求CPOL0, CPHA0空闲时钟低电平采样沿为上升沿。若STM32配置为CPOL1/CPHA1MISO数据将全为0xFF。使用标准外设库初始化SPI1void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA5(SCK), PA6(MISO), PA7(MOSI) 配置为复用推挽 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // SPI1初始化CPOL0, CPHA0, 波特率分频4系统时钟72MHz→18MHzPN532支持最高5MHz SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // 关键必须为Low SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // 关键必须为1Edge SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 72MHz/418MHz 5MHz安全 SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }提示SPI_BaudRatePrescaler_4是保守值实测_89MHz仍稳定但_236MHz会导致PN532误码。务必用示波器抓SPI波形验证CLK/MOSI/MISO时序。2.2 实现PN532底层读写带超时的SPI收发与IRQ中断等待PN532要求主机在发送命令后必须等待IRQ引脚变低表示准备好响应再执行读操作。轮询IRQ比中断更可靠避免中断嵌套问题// 等待IRQ引脚变低超时100ms uint8_t pn532_wait_irq(void) { uint16_t timeout 10000; // 100ms 100kHz while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) timeout--) { Delay_us(10); // 短延时避免CPU满载 } return (timeout 0) ? SUCCESS : ERROR_TIMEOUT; } // SPI发送n字节 void pn532_spi_write(uint8_t *buf, uint8_t len) { for (uint8_t i 0; i len; i) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, buf[i]); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); } } // SPI接收n字节发送0x00填充 void pn532_spi_read(uint8_t *buf, uint8_t len) { for (uint8_t i 0; i len; i) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, 0x00); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); buf[i] SPI_I2S_ReceiveData(SPI1); } }2.3 执行GetFirmwareVersion命令验证硬件连通性的黄金标准这是诊断链路是否正常的第一个命令。发送0x02GetFirmwareVersion期望响应包含芯片型号0x32对应PN532和固件版本uint8_t pn532_get_firmware_version(uint8_t *version) { uint8_t cmd_frame[10] {0}; uint8_t rsp_frame[64] {0}; // 构建命令帧LEN1, LCS0xFE, CMD0x02, DCS0xFD cmd_frame[0] 0x00; cmd_frame[1] 0x00; cmd_frame[2] 0xFF; cmd_frame[3] 0x01; cmd_frame[4] 0xFE; // LEN1, LCS~LEN cmd_frame[5] 0x02; // CMDGetFirmwareVersion cmd_frame[6] 0xFD; // DCS 0x02 ^ 0xFF 0xFD cmd_frame[7] 0x00; // 发送命令 pn532_spi_write(cmd_frame, 8); // 等待IRQ if (pn532_wait_irq() ! SUCCESS) return ERROR_TIMEOUT; // 读取响应至少12字节前缀LENLCSSTATUSDATADCSPostamble pn532_spi_read(rsp_frame, 12); // 校验响应帧头0x00 0x00 0xFF - 正确 if (rsp_frame[0] ! 0x00 || rsp_frame[1] ! 0x00 || rsp_frame[2] ! 0xFF) { return ERROR_FRAME_HEADER; } // 检查状态字节rsp_frame[7]0x00表示成功 if (rsp_frame[7] ! 0x00) { return ERROR_STATUS_CODE; } // 提取版本信息rsp_frame[10]为IC型号0x32PN532rsp_frame[11]为主版本号 version[0] rsp_frame[10]; // IC Type version[1] rsp_frame[11]; // Version version[2] rsp_frame[12]; // Revision version[3] rsp_frame[13]; // Support return SUCCESS; }注意响应帧中rsp_frame[7]是状态字节Status Code非0值表示错误如0x01Timeout0x02Parity Error。DEMO例程常忽略此检查导致后续命令静默失败。3. 读取Mifare Classic卡UID的完整流程——绕过防冲突循环、解析ATQA/SAK、提取4/7字节UID3.1 InListPassiveTarget命令让PN532主动扫描并返回卡片信息Mifare卡识别分三步1) PN532发送InListPassiveTarget命令2) 卡片响应ATQAAnswer To Request和SAKSelect Acknowledge3) 解析响应帧提取UID。关键点在于命令参数设置参数值说明MaxTargets0x01最多返回1张卡多卡环境需设为0x02TargetTypes0x00 0x00表示支持所有类型Mifare Classic, Ultralight等// 发送InListPassiveTarget命令 uint8_t pn532_in_list_passive_target(uint8_t *uid, uint8_t *uid_len) { uint8_t cmd_frame[12] {0}; uint8_t rsp_frame[256] {0}; // 构建命令帧LEN3, LCS0xFC, CMD0x4A, PAYLOAD[0x01,0x00,0x00] cmd_frame[0] 0x00; cmd_frame[1] 0x00; cmd_frame[2] 0xFF; cmd_frame[3] 0x03; cmd_frame[4] 0xFC; // LEN3 cmd_frame[5] 0x4A; // CMDInListPassiveTarget cmd_frame[6] 0x01; // MaxTargets1 cmd_frame[7] 0x00; cmd_frame[8] 0x00; // TargetTypes0x0000 cmd_frame[9] 0x4A ^ 0x01 ^ 0x00 ^ 0x00; // DCS CMD^PAYLOAD cmd_frame[10] 0x00; pn532_spi_write(cmd_frame, 11); if (pn532_wait_irq() ! SUCCESS) return ERROR_TIMEOUT; // 读取响应最大长度256字节 pn532_spi_read(rsp_frame, 256); // 校验响应头与状态 if (rsp_frame[0] ! 0x00 || rsp_frame[1] ! 0x00 || rsp_frame[2] ! 0xFF || rsp_frame[7] ! 0x00) { return ERROR_STATUS_CODE; } // 解析响应rsp_frame[11]为目标数量rsp_frame[12]起为卡片数据 uint8_t target_num rsp_frame[11]; if (target_num 0) return ERROR_NO_CARD; // 提取UIDrsp_frame[17]为UID长度4或7rsp_frame[18]起为UID数据 *uid_len rsp_frame[17]; for (uint8_t i 0; i *uid_len; i) { uid[i] rsp_frame[18 i]; } return SUCCESS; }3.2 UID长度判断与校验4字节UID需补前导07字节UID含BCC校验Mifare Classic 1K卡UID为4字节如0x04 0x12 0x34 0x56但PN532返回的uid_len可能为4或7uid_len 4标准UID直接使用uid_len 7扩展UID第7字节为BCCBlock Check Character需校验uid[0]^uid[1]^uid[2]^uid[3]^uid[4]^uid[5] uid[6]。// 校验7字节UID的BCC uint8_t pn532_check_uid_bcc(uint8_t *uid, uint8_t len) { if (len ! 7) return SUCCESS; // 非7字节无需校验 uint8_t bcc 0; for (uint8_t i 0; i 6; i) { bcc ^ uid[i]; } return (bcc uid[6]) ? SUCCESS : ERROR_UID_BCC; } // 主函数调用示例 uint8_t uid_buf[7]; uint8_t uid_len; if (pn532_in_list_passive_target(uid_buf, uid_len) SUCCESS) { if (pn532_check_uid_bcc(uid_buf, uid_len) ! SUCCESS) { // BCC错误可能是读取干扰 return ERROR_UID_BCC; } // 输出UID按16进制格式 printf(UID: ); for (uint8_t i 0; i uid_len; i) { printf(%02X , uid_buf[i]); } printf(\r\n); }提示某些劣质Mifare卡UID为随机值非固化每次靠近读卡器都不同。验证真卡需用Mifare Classic Tool等工具读取Sector 0 Block 0的固定UID字段。4. 写入Mifare Classic卡指定扇区——密钥认证、块写入、回读验证三步闭环4.1 AuthA命令用默认密钥0xFFFFFFFFFFFF认证扇区0Mifare Classic卡每个扇区有独立密钥Key A/Key B。扇区0的默认密钥为0xFFFFFFFFFFFF12字节需先认证才能读写// 构建AuthA命令帧认证扇区0密钥0xFFFFFFFFFFFF uint8_t pn532_auth_a_sector0(uint8_t *key) { uint8_t cmd_frame[24] {0}; uint8_t rsp_frame[64] {0}; // LEN13: CMD(1)BlockNum(1)KeyType(1)TargetNum(1)Key(6)UID(3) cmd_frame[0] 0x00; cmd_frame[1] 0x00; cmd_frame[2] 0xFF; cmd_frame[3] 0x0D; cmd_frame[4] 0xF2; // LEN13, LCS~13 cmd_frame[5] 0x16; // CMDInDataExchange cmd_frame[6] 0x01; // TargetNum1 cmd_frame[7] 0x60; // AuthA cmd_frame[8] 0x00; // BlockNum0 (扇区0块0) // Key: 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF for (uint8_t i 0; i 6; i) { cmd_frame[9 i] key[i]; } // UID: 取前3字节扇区0块0的UID cmd_frame[15] uid_buf[0]; cmd_frame[16] uid_buf[1]; cmd_frame[17] uid_buf[2]; cmd_frame[18] pn532_calc_dcs(cmd_frame[5], 13); // DCS over CMDPAYLOAD cmd_frame[19] 0x00; pn532_spi_write(cmd_frame, 20); if (pn532_wait_irq() ! SUCCESS) return ERROR_TIMEOUT; pn532_spi_read(rsp_frame, 64); return (rsp_frame[7] 0x00) ? SUCCESS : ERROR_AUTH_FAILED; }4.2 WriteBlock命令向扇区0块0写入自定义数据并回读验证认证成功后向扇区0块0地址0x00写入6字节数据如0x11,0x22,0x33,0x44,0x55,0x66uint8_t pn532_write_block0(uint8_t *data) { uint8_t cmd_frame[24] {0}; uint8_t rsp_frame[64] {0}; // LEN11: CMD(1)BlockNum(1)Data(6)DCS(1)Post(1) cmd_frame[0] 0x00; cmd_frame[1] 0x00; cmd_frame[2] 0xFF; cmd_frame[3] 0x0B; cmd_frame[4] 0xF4; // LEN11 cmd_frame[5] 0x16; // CMDInDataExchange cmd_frame[6] 0x01; // TargetNum1 cmd_frame[7] 0xA0; // WriteBlock cmd_frame[8] 0x00; // BlockNum0 for (uint8_t i 0; i 6; i) { cmd_frame[9 i] data[i]; } cmd_frame[15] pn532_calc_dcs(cmd_frame[5], 10); // DCS over CMDBlockNumData cmd_frame[16] 0x00; pn532_spi_write(cmd_frame, 17); if (pn532_wait_irq() ! SUCCESS) return ERROR_TIMEOUT; pn532_spi_read(rsp_frame, 64); return (rsp_frame[7] 0x00) ? SUCCESS : ERROR_WRITE_FAILED; } // 回读验证 uint8_t pn532_read_block0(uint8_t *read_data) { uint8_t cmd_frame[12] {0}; uint8_t rsp_frame[64] {0}; cmd_frame[0] 0x00; cmd_frame[1] 0x00; cmd_frame[2] 0xFF; cmd_frame[3] 0x05; cmd_frame[4] 0xFA; // LEN5 cmd_frame[5] 0x16; // CMDInDataExchange cmd_frame[6] 0x01; // TargetNum1 cmd_frame[7] 0xB0; // ReadBlock cmd_frame[8] 0x00; // BlockNum0 cmd_frame[9] pn532_calc_dcs(cmd_frame[5], 4); // DCS over CMDTargetReadBlockBlockNum cmd_frame[10] 0x00; pn532_spi_write(cmd_frame, 11); if (pn532_wait_irq() ! SUCCESS) return ERROR_TIMEOUT; pn532_spi_read(rsp_frame, 64); if (rsp_frame[7] ! 0x00) return ERROR_READ_FAILED; // 数据在rsp_frame[13]起共16字节取前6字节 for (uint8_t i 0; i 6; i) { read_data[i] rsp_frame[13 i]; } return SUCCESS; }注意WriteBlock命令写入的是6字节有效数据但Mifare Classic块为16字节剩余10字节保持原值。回读时需解析完整16字节帧rsp_frame[13]开始才是数据区。5. PN532在STM32项目中的高频踩坑点与硬核排错技巧——从示波器抓波形到寄存器级定位5.1 IRQ引脚始终高电平检查这3个硬件级致命点当pn532_wait_irq()永远超时90%原因是IRQ信号未触发JP2跳线错误PN532模块JP2跳线必须置于IRQ侧非HSU或I2C否则IRQ引脚悬空PB1上拉缺失STM32 PB1需外接10kΩ上拉电阻模块内部无上拉否则浮空导致电平不定电源纹波过大用万用表测VCC是否稳定3.3V纹波100mV会导致PN532复位。实测某劣质LDO在电流突变时VCC跌至2.8VIRQ失效。提示用示波器探头直接测量PN532的IRQ引脚非STM32端确认卡片靠近时是否有清晰下降沿。若无问题在模块侧若有但STM32未检测到检查GPIO配置是否为GPIO_Mode_IN_FLOATING应为GPIO_Mode_IPU。5.2 读出UID全是0x00SPI MISO引脚接地不良的典型症状MISO线虚焊或接触电阻过大时STM32读到全0xFF发送时或全0x00接收时。验证方法发送GetFirmwareVersion命令用示波器抓MISO波形看是否为有效数据若MISO恒为高电平检查PN532模块MISO焊点及STM32 PA6引脚焊接替换SPI线缆屏蔽双绞线排除电磁干扰。5.3 AuthA返回0x14DEP_ERROR密钥长度或UID字节错位0x14错误码表示“DEPData Exchange Protocol错误”常见于密钥传入6字节但实际需12字节Mifare Classic密钥为6字节但PN532命令帧中Key字段占6字节UID字段占3字节顺序不能颠倒UID使用了7字节但只传入前3字节AuthA命令中UID字段必须为3字节取UID前3字节即可。// 正确UID取前3字节 cmd_frame[15] uid_buf[0]; // UID[0] cmd_frame[16] uid_buf[1]; // UID[1] cmd_frame[17] uid_buf[2]; // UID[2] // 错误传入7字节UID导致帧错位 // cmd_frame[15] uid_buf[0]; ... cmd_frame[21] uid_buf[6]; // 超出范围5.4 使用Keil MDK调试时无法单步进入SPI中断关闭优化等级并启用调试符号在Options for Target → C/C → Optimization中将优化等级设为-O0无优化否则编译器可能内联pn532_spi_write()导致断点失效。同时勾选Generate Debug Information确保.axf文件包含符号表。调试时重点关注SPI1-SR寄存器TXE发送缓冲区空和RXNE接收缓冲区非空标志位GPIOB-IDR实时查看PB1IRQ电平变化SPI1-DR观察发送/接收数据是否与预期一致。提示在pn532_wait_irq()中插入__NOP()指令防止编译器优化掉延时循环便于示波器抓取IRQ电平变化时刻。本文还有配套的精品资源点击获取
返回列表