PN532 NFC模块串口通信全解析:从硬件连接到MIFARE卡读写实战
1. 项目缘起:为什么是PN532与串口?
如果你在物联网、智能门禁或者DIY硬件项目里打过滚,大概率听说过PN532这颗NFC读写芯片。它几乎是业余爱好者和中小型项目接触13.56MHz NFC(近场通信)技术的“敲门砖”。市面上很多所谓的“NFC模块”,核心就是它。但当你兴冲冲买回一个模块,准备让它和你的Arduino、树莓派或者单片机说上话时,第一个拦路虎往往就是通信方式的选择。
模块通常支持I2C、SPI和UART(串口)三种通信接口。我见过太多新手朋友,在教程里看到I2C接线简单就选了I2C,结果被上拉电阻、地址冲突和时序问题折腾得够呛;或者选了SPI,又被片选、时钟相位这些概念绕晕。其实,对于绝大多数初次接触、或者希望快速验证功能的应用场景,串口(UART)往往是那个“最不折腾”的选择。它接线极其简单(通常就RX、TX、GND、VCC四根线),在电脑上直接用串口助手就能调试,几乎所有的微控制器都原生支持,避开了总线协议的那些“潜规则”。所以,这篇内容我们就聚焦在“如何用串口驯服PN532”这件事上,我会把从硬件连接到指令调试,再到实际应用中的那些坑和技巧,掰开揉碎了讲清楚。
2. 硬件连接与电源:一切稳定通信的基础
拿到一个PN532 NFC模块(常见的是那种蓝色或红色的板子),第一步不是急着写代码,而是确保它能正确上电并和你的主控设备建立物理连接。这一步的疏忽,会导致后续所有调试都像是在黑暗中摸索。
2.1 模块引脚识别与串口选择
市面上最常见的PN532模块,板子上通常会有一排排针,并印有缩写。找到标有VCC、GND、SDA、SCL、MISO、MOSI、SCK、SS、RX、TX的这些引脚。我们的目标是使用串口,所以需要关注RX(接收)、TX(发送)、VCC和GND。
这里有一个关键细节:很多PN532模块的串口逻辑电平是3.3V的。这意味着它的RX和TX引脚只能耐受3.3V的信号。如果你直接把它连接到Arduino Uno这类工作电压为5V的板子的数字引脚(5V TTL电平),长期工作有可能损坏PN532芯片。因此,你有两个选择:
- 使用3.3V主控设备:如ESP8266、ESP32、树莓派GPIO(需注意配置)、STM32(3.3V系统)等。这是最省事、最安全的方式。
- 使用5V主控设备,但必须进行电平转换:在Arduino Uno的
TX(发送)引脚和PN532的RX引脚之间,串联一个1kΩ左右的电阻进行分压,可以作为一种简单的防护。但更可靠的做法是使用专用的电平转换模块(如TXS0108E等)。绝对不要直接将5V引脚连接到PN532的RX上。
接线关系是交叉连接:
- 主控设备的
TX接 PN532模块的RX。 - 主控设备的
RX接 PN532模块的TX。 VCC接 3.3V(或5V经转换后的3.3V)。GND接GND。
注意:有些模块可能通过跳线帽或焊点来选择通信模式。你需要确保模块被配置为“串口模式”。具体方法要看模块说明书,常见操作是断开I2C的跳线,或者焊接特定的电阻。
2.2 供电稳定性的隐形门槛
NFC读写,尤其是激活卡片和进行数据交换时,芯片的瞬时电流可能会有一个小的峰值。如果供电不足或不稳,会导致读卡距离变短、卡片识别不稳定甚至完全无法工作。我踩过的一个坑是:使用一根又细又长的USB线给ESP32开发板供电,同时ESP32给PN532供电。当读写卡片时,系统偶尔会重启。后来用示波器查看3.3V电源轨,发现读写瞬间有超过200mV的跌落。
给你的建议:
- 尽量使用模块上独立的3.3V稳压芯片供电,而不是直接从主控芯片的GPIO取电。
- 在PN532的
VCC和GND引脚之间,就近焊接一个10μF的电解电容和一个0.1μF的陶瓷电容。前者应对低频电流需求,后者滤除高频噪声,这是保证芯片稳定工作的经典做法。 - 如果使用移动电源或劣质USB适配器,优先考虑换用输出更稳定的电源。
3. 通信协议解析:PN532的“语言规则”
物理连接通了,只是硬件层准备好了。要让主控设备和PN532相互理解,必须遵循一套特定的软件协议。PN532的串口通信并非简单的“发送数据就能读卡”,它有一套完整的指令帧结构。
3.1 指令帧格式:每个字节都有意义
PN532使用基于数据包的通信。每一个从主机(你的单片机/电脑)发送到PN532的指令,都必须包装成以下格式的帧:
[Preamble] [Start Code] [Length] [Length Checksum] [TFI] [Data Bytes ...] [Data Checksum] [Postamble]看起来有点复杂,我们拆解一下常用部分:
- Preamble (前导码):
0x00。用于同步,可以忽略,现代解析中常不严格检查。 - Start Code (起始码):
0x00 0xFF。标志一个帧的开始。 - Length (长度 LEN):一个字节,表示TFI字段加上后续所有数据字节的数量。
- Length Checksum (长度校验和 LCS):一个字节,计算方式为
LCS + LEN = 0x100。例如,如果LEN = 0x04,那么LCS必须为0xFC,因为0x04 + 0xFC = 0x100(溢出后为0x00)。这是一种简单的校验。 - TFI (帧标识符):一个字节,标识数据流的方向。
0xD4:主机到PN532(命令)0xD5:PN532到主机(响应)
- Data Bytes (数据域):具体的命令代码和参数。这是核心内容。
- Data Checksum (数据校验和 DCS):一个字节,计算方式为从
TFI开始到最后一个数据字节,所有字节的补码和(即所有字节求和,取反,再加1)。等效于~(sum(TFI + DataBytes)) + 1,并取低8位。确保TFI + DataBytes + DCS = 0x00(溢出后)。 - Postamble (后导码):
0x00。帧结束标志,同样常不严格检查。
举个例子:我们要发送一个最基础的“获取固件版本”指令。这个指令的代码是0x02,没有参数。
- TFI是
0xD4(主机发送命令)。 - 数据域只有
0x02一个字节。 - 计算长度:TFI(
0xD4) + 数据(0x02) = 2个字节。所以LEN = 0x02。 - 计算LCS:
0x02 + LCS = 0x100=>LCS = 0xFE。 - 计算DCS:
TFI + Data = 0xD4 + 0x02 = 0xD6。0xD6的补码是0x2A(因为0xD6 + 0x2A = 0x100)。所以DCS = 0x2A。 - 最终组成的完整帧为:
00 00 FF 02 FE D4 02 2A 00
你不需要每次都手动计算,可以在代码里封装成函数。但理解这个过程至关重要,因为当通信无响应时,你需要能检查你发出的帧格式是否正确。
3.2 核心指令集与工作流程
PN532的指令很多,但对于基础读写,掌握以下几个就足够了:
SAMConfiguration(0x14):配置SAM(安全访问模块)。相当于初始化PN532的内部状态。通常参数设为0x01(正常模式)。InListPassiveTarget(0x4A):寻卡指令。这是让PN532开始扫描附近卡片的命令。你需要指定卡的类型(如0x00表示106kbps Type A,即MIFARE Classic卡)和最大检测数量。InDataExchange(0x40):数据交换指令。这是与已激活的卡片进行通信(读、写、认证等)的“万能”指令。你需要将具体的MIFARE命令(如读块命令0x30)作为参数通过这个指令发送给卡片。InCommunicateThru(0x42):透传指令。在某些模式下,可以直接通过它发送APDU(应用协议数据单元),用于更高级的操作。
一个典型的读卡流程(以读取MIFARE Classic卡的0区块为例):
- 发送
SAMConfiguration命令,等待成功响应(0xD5 0x14 0x00 ...)。 - 循环发送
InListPassiveTarget命令(0xD4 0x4A 0x01 0x00),直到返回数据中表示发现卡片,并获取卡片的UID(唯一标识符)。 - 进行密钥认证(可选,但读0区块通常需要)。通过
InDataExchange命令向卡片发送MIFARE的认证命令(0x60或0x61)和密钥。 - 认证成功后,通过
InDataExchange命令发送MIFARE读块命令(0x30+ 块地址),卡片返回该块的数据。
4. 从零开始的代码实战(以Arduino为例)
理论说再多,不如一行代码。我们以Arduino平台为例,手把手实现通过串口读取MIFARE Classic卡的UID。这里我们不依赖复杂的库,而是直接操作串口来理解底层过程,之后你再使用库(如Adafruit-PN532)时会更加得心应手。
4.1 环境搭建与底层串口操作
首先,按照第2章的说明连接硬件(假设使用3.3V Arduino兼容板)。在Arduino IDE中,我们使用SoftwareSerial库来创建一个软串口,避免占用硬件串口(它可能用于打印调试信息)。
#include <SoftwareSerial.h> // 定义PN532的连接引脚,假设接在D2(RX), D3(TX) SoftwareSerial nfcSerial(2, 3); // RX, TX void setup() { // 启动调试串口 Serial.begin(115200); // 启动与PN532通信的串口,波特率固定为115200 nfcSerial.begin(115200); Serial.println("PN532 NFC Reader Initializing..."); }PN532模块的串口波特率通常是固定的115200,8位数据位,无奇偶校验,1位停止位(8N1)。一般不可更改。
4.2 封装帧发送与接收函数
这是最核心的部分,我们将创建两个函数:一个用于发送封装好的指令帧,一个用于接收并解析响应帧。
// 发送一个完整的PN532指令帧 void sendPN532Frame(uint8_t *cmdData, uint8_t cmdLen) { // cmdData 指向 TFI + 实际指令数据 的数组 // cmdLen 是 TFI + 指令数据 的总长度 uint8_t frame[cmdLen + 7]; // 预留完整帧的空间 uint8_t LEN = cmdLen; uint8_t LCS = 0x100 - LEN; // 计算长度校验和 // 构建帧 frame[0] = 0x00; // Preamble frame[1] = 0x00; // Start Code 1 frame[2] = 0xFF; // Start Code 2 frame[3] = LEN; frame[4] = LCS; // 拷贝TFI和指令数据 for (uint8_t i=0; i<cmdLen; i++) { frame[5+i] = cmdData[i]; } // 计算数据校验和 DCS uint8_t sum = 0; for (uint8_t i=0; i<cmdLen; i++) { sum += cmdData[i]; } uint8_t DCS = 0x100 - sum; // 同样用补码方式计算 frame[5 + cmdLen] = DCS; frame[6 + cmdLen] = 0x00; // Postamble // 通过软串口发送整个帧 for (uint8_t i=0; i < (cmdLen + 7); i++) { nfcSerial.write(frame[i]); } } // 接收并解析PN532响应帧 bool receivePN532Frame(uint8_t *buffer, uint8_t *len, uint16_t timeout = 1000) { uint8_t state = 0; uint8_t length = 0; uint8_t checksum = 0; unsigned long startTime = millis(); while (millis() - startTime < timeout) { if (!nfcSerial.available()) continue; uint8_t c = nfcSerial.read(); switch (state) { case 0: // 等待起始码 0x00 if (c == 0x00) state = 1; break; case 1: // 等待起始码 0xFF if (c == 0xFF) state = 2; else state = 0; // 重置 break; case 2: // 读取长度 LEN length = c; state = 3; break; case 3: // 读取长度校验和 LCS,并验证 if ((length + c) != 0x100) { // 验证失败 state = 0; break; } state = 4; break; case 4: // 读取TFI,应为0xD5 if (c != 0xD5) { // 不是响应帧 state = 0; break; } buffer[0] = c; // 存入TFI checksum = c; state = 5; *len = 1; // 初始化数据长度 break; case 5: // 读取数据字节,直到达到指定长度 buffer[*len] = c; checksum += c; (*len)++; if (*len == length) { // 已读完所有数据字节 state = 6; } break; case 6: // 读取数据校验和 DCS if ((checksum + c) != 0x100) { // 数据校验失败 return false; } state = 7; break; case 7: // 等待后导码 0x00 (可选,可忽略) // if (c == 0x00) { return true; // 成功接收一帧 // } // return false; break; } } return false; // 超时 }这个接收函数实现了一个简单的状态机,逐字节解析帧结构,并验证了长度校验和与数据校验和,可靠性很高。
4.3 实现完整寻卡与读UID流程
现在,我们将指令和函数组合起来,完成寻卡操作。
void loop() { // 1. 配置SAM uint8_t samCmd[] = {0xD4, 0x14, 0x01, 0x00}; // TFI + 指令(0x14) + 模式(0x01) + 超时(0x00) sendPN532Frame(samCmd, 4); uint8_t response[32]; uint8_t responseLen; if (!receivePN532Frame(response, &responseLen, 200)) { Serial.println("SAM Config failed or timeout."); delay(1000); return; } // 响应格式: 0xD5 0x14 0x00 (成功) if (responseLen < 3 || response[1] != 0x14 || response[2] != 0x00) { Serial.println("SAM Config error."); return; } Serial.println("SAM Configured."); // 2. 寻卡 (InListPassiveTarget) uint8_t pollCmd[] = {0xD4, 0x4A, 0x01, 0x00}; // TFI + 指令(0x4A) + 最大卡数(1) + 波特率(106k Type A) sendPN532Frame(pollCmd, 4); if (!receivePN532Frame(response, &responseLen, 500)) { // 没读到卡是正常的,可以静默重试 delay(200); return; } // 成功响应格式: 0xD5 0x4A 0x01 0x00 0x04 0x08 0x04 UID... // response[1]=0x4A, response[2]=0x01(卡数量), response[3]=卡类型, response[4]=UID长度 if (responseLen > 6 && response[1] == 0x4A && response[2] > 0) { uint8_t uidLength = response[4]; Serial.print("Card detected! UID Length: "); Serial.println(uidLength); Serial.print("UID: "); for (uint8_t i=0; i<uidLength; i++) { Serial.print(response[5+i], HEX); Serial.print(" "); } Serial.println(); } delay(1000); // 每秒轮询一次 }将代码上传到开发板,打开串口监视器(波特率115200),当把一张MIFARE Classic卡(比如门禁卡)靠近PN532模块时,你应该能看到UID被打印出来。这是你与PN532通过串口对话成功的第一个里程碑。
5. 进阶操作:读/写MIFARE Classic卡数据块
读取UID只是第一步,更常见的需求是读取或修改卡片数据区的内容。MIFARE Classic 1K卡有16个扇区(Sector 0-15),每个扇区有4个块(Block 0-3),每个块16字节。其中,每个扇区的块3是“扇区尾块”,存储着该扇区的两个密钥(Key A, Key B)和访问控制位(Access Bits)。在读写任何数据块(块0,1,2)前,必须先用正确的密钥认证该扇区。
5.1 扇区认证流程详解
认证是读写操作的守门员。PN532通过InDataExchange命令来中转我们的认证请求给卡片。
假设我们要认证扇区0(默认密钥A通常是0xFF 0xFF 0xFF 0xFF 0xFF 0xFF),认证流程的指令帧构建如下:
构建MIFARE认证命令:这是一个6字节的卡片级命令。
- 认证命令字节:
0x60(用Key A认证)或0x61(用Key B认证)。这里用0x60。 - 块地址:要认证的扇区里的任何一个块的地址。通常用该扇区的第一个数据块,即块0(地址
0x00)。注意,这里不是扇区号。 - 密钥:6字节的密钥,例如
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF。 - 卡片UID:4字节或7字节(取决于卡类型),我们在寻卡步骤已经获取。
- 所以,完整的MIFARE认证命令数据为:
[0x60, 0x00, FF, FF, FF, FF, FF, FF, UID0, UID1, UID2, UID3](共12字节)。
- 认证命令字节:
通过PN532的InDataExchange发送:我们将上述12字节数据,作为
InDataExchange命令的数据域发送。InDataExchange的指令码是0x40。- 它需要一个参数:目标编号(
TargetID)。这个TargetID来自InListPassiveTarget命令的响应。在上面的寻卡例子里,响应帧中的response[3]就是TargetID(通常是0x01)。 - 因此,发送给PN532的完整数据是:
[0xD4, 0x40, TargetID, 0x60, 0x00, FF, FF, FF, FF, FF, FF, UID0, UID1, UID2, UID3]。
解析响应:如果认证成功,PN532会返回一个响应帧,其中数据部分(
0xD5 0x40之后)应该包含卡片的应答。对于MIFARE Classic,成功认证后即可进行后续读写。
5.2 读块与写块操作实现
认证成功后,读块和写块操作同样通过InDataExchange命令中转MIFARE命令。
读块操作:
- MIFARE读块命令:
0x30。 - 需要参数:块地址(1字节)。
- 例如,读扇区0的块1(地址
0x01):MIFARE命令为[0x30, 0x01]。 - 通过PN532发送:
[0xD4, 0x40, TargetID, 0x30, 0x01]。 - 成功响应中,数据部分会包含该块16字节的内容。
写块操作:
- MIFARE写块命令:
0xA0。 - 需要参数:块地址(1字节) + 16字节数据。
- 例如,向扇区0的块1写入16个字节
0x00:MIFARE命令为[0xA0, 0x01, 0x00, 0x00, ..., 0x00](共18字节)。 - 通过PN532发送:
[0xD4, 0x40, TargetID, 0xA0, 0x01, 16个数据字节...]。 - 成功响应通常很简单。
极其重要的警告:绝对不要在没有完全理解访问控制位(Access Bits)的情况下,尝试写入扇区尾块(每个扇区的块3)。错误的写入会导致整个扇区被永久锁死,无法再读写。在实验时,请仅使用扇区0的块1和块2,或者使用专门的测试卡。
下面是一个在认证后读取块1的代码片段示例(接在之前寻卡成功之后):
// 假设我们已经获得了卡的UID(uid[])和TargetID(targetId) // 并且已经将UID存储在数组uid[]中,长度为uidLen // 步骤1: 认证扇区0 (Key A: FF FF FF FF FF FF) uint8_t authCmd[12] = {0x60, 0x00, // MIFARE Auth with Key A, Block 0 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Key A uid[0], uid[1], uid[2], uid[3]}; // UID // 构建PN532的InDataExchange帧来发送认证命令 uint8_t pn532AuthFrame[14] = {0xD4, 0x40, targetId}; // TFI + InDataExchange + TargetID for (int i=0; i<12; i++) { pn532AuthFrame[3+i] = authCmd[i]; } sendPN532Frame(pn532AuthFrame, 15); // 3 + 12 = 15 bytes if (!receivePN532Frame(response, &responseLen) || response[1]!=0x40 || response[2]!=0x00) { Serial.println("Authentication failed."); return; } Serial.println("Authentication successful."); // 步骤2: 读取块1 (地址 0x01) uint8_t readCmd[2] = {0x30, 0x01}; // MIFARE Read, Block 1 uint8_t pn532ReadFrame[5] = {0xD4, 0x40, targetId, readCmd[0], readCmd[1]}; sendPN532Frame(pn532ReadFrame, 5); if (receivePN532Frame(response, &responseLen) && response[1]==0x40) { // 响应数据从 response[3] 开始,应该是16字节的块数据 Serial.print("Block 1 Data: "); for (uint8_t i=0; i<16; i++) { Serial.print(response[3+i], HEX); Serial.print(" "); } Serial.println(); }6. 调试技巧与常见问题排查
即使按照教程一步步来,你也可能会遇到模块没反应、读卡不稳定等问题。以下是基于大量实战总结的排查清单。
6.1 通信完全无响应
这是最让人头疼的情况。按以下顺序检查:
- 电源与电平:这是首犯。用万用表测量PN532的VCC引脚,确保是稳定的3.3V(或5V,如果你的模块支持)。测量主控TX引脚在发送数据时的电压,确保是3.3V电平(如果是5V系统,必须加电平转换)。
- 接线错误:再次确认TX-RX是交叉连接。一个快速测试法:将主控的TX暂时连接到主控的RX(自发自收),发送测试数据,如果自己能收到,说明主控串口是好的。
- 波特率:确认代码中
nfcSerial.begin(115200);的波特率与模块严格一致。有些劣质模块的晶振不准,可以尝试微调波特率,如111200或115200。 - 模式跳线:仔细检查模块上是否有选择I2C/SPI/UART的跳线帽或焊点,确保其设置在UART模式。
- 发送帧格式:利用串口监视器的“十六进制显示”功能,捕获你实际发送出去的字节序列,与第3章计算的正确帧格式逐字节对比。重点检查
LCS和DCS。 - 模块损坏:如果以上都无误,尝试给模块一个“硬重启”(断电再上电)。有时模块会进入奇怪的状态。如果还不行,可能模块已损坏。
6.2 能初始化但无法检测到卡片
- 天线问题:PN532模块的天线是板载的线圈。确保卡片放在天线线圈中心区域上方。金属物体靠近会严重干扰甚至屏蔽磁场。
- 供电不足:读卡瞬间功耗增大,如第2章所述,检查电源是否充足。尝试用外接电源(如稳压电源)直接给模块供电测试。
- 寻卡参数:检查
InListPassiveTarget命令的参数。第二个参数0x00代表只寻Type A卡。如果你手头是其他类型的卡(如Felica),需要更改。 - 卡片类型:确认你的卡片是MIFARE Classic(S50 1K或S70 4K)。一些新的CPU卡或带有特殊功能的卡可能不被简单寻卡指令识别。
- 环境干扰:附近有强射频源(如路由器、手机)、大功率电源或电机,会产生电磁干扰,大幅降低读卡距离。
6.3 认证或读写失败
- 密钥错误:这是最常见的原因。MIFARE Classic卡的每个扇区都有两个密钥。除非你知道确切的密钥,否则默认尝试
0xFF 0xFF 0xFF 0xFF 0xFF 0xFF(Key A)或0xFF 0xFF 0xFF 0xFF 0xFF 0xFF(Key B)。许多门禁卡、校园卡会修改默认密钥。 - 块地址错误:认证时使用的“块地址”是扇区内的块号,不是扇区号。认证扇区0,使用块地址
0x00,0x01,0x02中的任意一个都可以。但读写时,要明确指定目标块地址。 - 访问控制位:如果你尝试写入的块被访问控制位设置为“只读”或“不可访问”,写操作会失败。在尝试写操作前,最好先读取该扇区尾块(块3)的访问控制字节,理解其含义。
- 时序问题:认证或读写操作后,需要给卡片和模块一点处理时间。在发送下一个命令前,添加少量延迟(如
delay(10))可能会解决偶发性失败。
6.4 利用逻辑分析仪或示波器
如果条件允许,一个几十块钱的逻辑分析仪(配合PulseView软件)是调试串口通信的神器。你可以同时抓取主控TX和RX的波形,直观地看到:
- 你发送的指令帧是否完全正确(对比理论帧)。
- PN532是否有返回响应(即使响应是错误码)。
- 响应帧的内容是什么,从而精准定位问题所在(例如,响应码
0x01通常表示帧校验错误)。
通过串口打印日志结合逻辑分析仪抓包,几乎可以定位所有通信层面的问题。
7. 从裸机驱动到使用成熟库
我们花大力气从底层实现,是为了彻底理解原理。在实际项目中,为了提高开发效率和可靠性,强烈建议使用成熟的库。以Arduino社区最流行的Adafruit-PN532库为例,它封装了所有底层帧的构建、解析和错误处理。
使用库后,之前的复杂操作变得极其简单:
#include <Wire.h> #include <SPI.h> #include <Adafruit_PN532.h> // 如果是串口,使用以下构造函数 Adafruit_PN532 nfc(2, 3); // RX, TX 引脚 void setup() { Serial.begin(115200); nfc.begin(); uint32_t versiondata = nfc.getFirmwareVersion(); if (!versiondata) { Serial.println("Didn't find PN53x board"); while (1); } nfc.SAMConfig(); // 配置SAM Serial.println("Waiting for an ISO14443A Card ..."); } void loop() { uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; uint8_t uidLength; // 一行代码实现寻卡 if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength)) { // 打印UID // 认证和读卡也都有封装好的函数 nfc.mifareclassic_AuthenticateBlock(uid, uidLength, 1, 0, (uint8_t*)KEY_DEFAULT_KEY); // 认证块1 uint8_t data[16]; if (nfc.mifareclassic_ReadDataBlock(1, data)) { // 读块1 // 处理数据 } } delay(1000); }库的好处是显而易见的:代码简洁、经过充分测试、支持更多功能(如NTAG读写)。但了解底层协议让你在库函数出错时,有能力进行深度调试,甚至根据特殊需求修改库代码。这就是“知其然,亦知其所以然”的价值。
最后,关于PN532串口通信,一个很深的体会是:耐心和细致的检查比任何高级技巧都重要。九成的问题都出在硬件连接、电源和最初的指令帧格式上。当你成功点亮第一张卡,读出第一串数据时,那种对底层硬件通信的掌控感,是直接用库无法比拟的。这份教程希望能帮你跨过最初的那道门槛,剩下的探索,就交给你的项目和想象力了。
