简介:针对 SmartEverything 开发板的 NT3H1X01 近场通信芯片库,是一套用 C++ 编写的 Arduino 兼容库,面向需要为 NT3H1101/NTAG I2C 芯片增加非接触式与 I2C 通信能力的开发者。库封装了 NFC 读写与 I2C 对接逻辑,调用时无需额外引脚,既可直接用于 SmartEverything,也可迁移到多数 Arduino 认证板卡。整个 zip 包仅 19KB,共 21 个文件:8 个 .h 与 6 个 .cpp 构成核心源码,3 个 .ino 提供可直接运行的示例草图,属性配置文件便于库管理器自动识别,另有文本说明与 Markdown 文档,整体结构紧凑、上手路径清晰。库内文件分工明确,核心实现集中存放,示例由浅入深,方便对照阅读源码并快速跑通通信流程;使用者可先运行示例确认硬件连接,再按需裁剪源码以减少资源占用。已有 403 人浏览学习,适合想验证 NT3H1X01 通信、理解 NTAG I2C 非接触式与接触式接口如何协作,或将其移植到自研硬件的中高级嵌入式开发者。
1. NT3H1X01 近场通信芯片库:开发板和手机之间最短的数据链路
sme-nt3h1x01-library 这个库名,读起来像一串随机字符,但它解决的是一个很具体的嵌入式问题:让一块带近场通信能力的开发板,不依赖线缆、不依赖蓝牙和 Wi-Fi,直接和手机交换数据。这个库围绕 SmartEverything 板载的 NT3H1X01 近场通信芯片,把芯片的 I2C 读写、NFC 字段检测、SRAM 握手这些底层操作封装成几个稳定的接口,让上层应用只需要关心“往哪个地址写什么、从哪个地址读什么”。
我接触这个芯片的动机很实际:一次做设备配网功能时,不想让用户先装 App、再找热点、再输密码,而是希望手机往板子上一贴,配置就自动写进去。用 NT3H1X01 这类双接口 NFC 芯片是常见做法,手机通过射频读写,板子通过 I2C 读写,共用同一片存储。这个库做的就是把后者收拾利索。这篇笔记适合正在做 IoT 配网、离线参数下发、设备调试接口的工程师,也适合刚拿到带 NFC 芯片的开发板、想快速确认它能不能用的新手。
2. 先弄懂 NT3H1X01 的双口模型:RF 与 I2C 是怎样共用一片存储的
直接把芯片当普通 I2C EEPROM 用,是很多人踩坑的起点。它本质上是一个双口器件:一个口是外部的射频接口,手机或读卡器通过天线靠近来读写;另一个口是板子上的 I2C 数字接口,MCU 通过这两根线访问同一块存储。两个口访问的目标有重叠,但访问规则完全不同,驱动库的价值就在这里。
2.1 两个端口、一个仓库:射频接口与数字 I2C 接口的关系
芯片内部有一块 EEPROM 主存储区,射频接口和 I2C 接口都能访问它。射频侧遵循的是 NFC 的逻辑,读卡器把存储区看成若干个 4 字节的块,一次读一块、写一块;I2C 侧则更像传统的串行 EEPROM,MCU 发一个偏移地址,然后连续读或连续写若干字节。也就是说,同一个物理地址,两侧看到的数据单位是不同的,这是第一个需要建立的心智模型。
两个端口同时访问同一块区域时,芯片有仲裁机制,而且射频访问的优先级更高。手机正在读卡的过程中,MCU 发起 I2C 读写,轻则等待,重则超时。这个特性在芯片手册里叫法不一,但本质上就是“同一时刻只有一个端口能真正落笔”。驱动库要做的事情,不只是把 I2C 协议跑通,还要在射频会话期间管住 MCU 的手。
所以这个芯片不适合被当成一个纯粹的数据仓库,它更像一个带门卫的共享仓库。门卫的规则是:手机来了,MCU 让路;手机走了,MCU 再进场。后面要讲的状态轮询和 FD 中断,都是围绕这一条规则设计的。理解了这一点,库的封装思路就清楚了:对外提供读写接口,对内做仲裁判断。
2.2 存储器映射:用户区、系统区、SRAM 与寄存器不是一回事
芯片的 I2C 地址空间不是一整块干净的用户 EEPROM。按常见型号的分布,可以粗分成四类区域:用户数据区、系统配置区、SRAM 区和状态寄存器区。用户数据区是日常存配置的地方,手机和 MCU 都能读写;系统配置区存放出厂信息、CC(Capability Container)和密码相关参数,普通读写一般碰不到;SRAM 区是芯片里的一块易失性缓存,上电后内容不定,但读写速度比 EEPROM 快很多,而且不占用写周期;寄存器区则反映当前射频场状态、SRAM 状态等运行信息。
这四个区域在 I2C 偏移地址上是连续排布的,在射频侧看却不是同样顺序。也就是说,手机通过 NFC 读到的某个块号,和 MCU 从 I2C 偏移读到的字节,并不是简单的块号乘 4 的关系。有的型号两者基本对齐,有的中间会插入系统区或 SRAM 区。做驱动封装时,最安全的方式是先读一遍全地址空间,把各区域边界实测出来,而不是凭记忆写死。
我这里并不建议一上来就去翻手册里的完整内存表,太容易看晕。更实用的做法是写一个 dump 脚本,把 0x00 到 0xFF 全部读出来,连续 0xFF 的区域通常是未使用的用户区,内容稳定且规律的是出厂数据区,上电后每次读都不一样的是 SRAM 区,读数随射频场变化的是寄存器区。这样一轮扫描,芯片对你就没有黑匣子了。
// dump_memory.c —— 把芯片 I2C 地址空间全量读出来 #include <stdio.h> #include <stdint.h> // 底层 I2C 原语由开发环境提供,这里只做封装示意 extern int i2c_read_reg(uint8_t addr7, uint8_t reg, uint8_t *buf, uint8_t len); #define NT3H_ADDR 0x55 // NT3H1X01 7 位 I2C 地址 #define REGION_SIZE 0x100 // 先扫 256 字节,覆盖大多数型号的用户区与寄存器区 void dump_memory(void) { uint8_t buf[16]; for (uint16_t offset = 0; offset < REGION_SIZE; offset += 16) { if (i2c_read_reg(NT3H_ADDR, (uint8_t)offset, buf, 16) != 0) { printf("read failed at 0x%02X\n", offset); break; } printf("0x%02X: ", offset); for (int i = 0; i < 16; i++) printf("%02X ", buf[i]); printf("\n"); } }这段代码的核心是验证“能不能连续读一大块”。注意i2c_read_reg的第一个参数是 7 位地址,不是最终发到总线上的 8 位地址,0x55 左移一位后才是总线地址 0xAA/0xAB。连续读的边界是 16 字节一组,如果你发现某个偏移返回的都是 0xFF,说明该区域未初始化;如果某个区域读数每次上电都变,基本可以断定是 SRAM 或寄存器。这个步骤能把芯片的映射摸清楚,后续写驱动就不会对着错误地址乱猜。
2.3 把 I2C 当普通 EEPROM 用为什么不行:写周期与区域差异
普通 EEPROM 驱动的套路是:发地址、写数据、等几个毫秒、读回来验证。在 NT3H1X01 上,这个套路前半段能跑通,后半段会有两个意外。第一,EEPROM 区写入后确实有内部写周期,但芯片在写周期内的行为表现不像老式 EEPROM 那样稳定可预测,不同型号在“写完了没有”这个状态上暴露方式不一样,有的需要用延时,有的要靠读状态位。第二,SRAM 区和寄存器区根本没有写周期,写入是即时的,如果你用一套固定延时去应对所有区域,性能被拖慢还是小事,更麻烦的是你会误以为所有区域都必须延时。
还有一个容易被忽略的差异:普通 EEPROM 的 I2C 地址空间就是纯数据,而 NT3H1X01 的地址空间里混着寄存器。你在某个偏移写了一个字节,可能不是在写数据,而是在改芯片的运行状态。比如误写寄存器区,可能导致后续读写行为变化。这就是为什么这个库要把“读取”和“写入”按区域类型区分开,而不是暴露一个通用的 byte write 接口。
所以库的底层通常分成三组操作:读 EEPROM、写 EEPROM、读 SRAM/寄存器。每组操作的等待策略不一样。新手最容易犯的错,是把这三组操作全部套同一个模板,功能偶尔正常,偶尔翻车,最后只能靠加长延时来“压住问题”,这其实是把系统的实时性也一起牺牲掉了。
3. 在 SmartEverything 上跑通最小读写:接线、地址扫描和三组 I2C 原语
原理讲完就可以动手。这一章的目标只有一个:在板子上通过 I2C 把芯片读出来、写进去、再读回来。整个流程不依赖手机,先把本地链路打通,后面再让手机参与。
3.1 先让芯片现形:接线检查与 I2C 地址扫描
NT3H1X01 对外引出的关键引脚不多,常见是 SDA、SCL、VCC、GND,外加一个 FD 字段检测脚。在 SmartEverything 这类开发板上,芯片已经和 MCU 接好线,你不需要自己飞线,但要确认两件事:第一,I2C 总线上是否已经接上拉电阻,有些板子为了低功耗把上拉做成可切换的,忘了使能会导致通信时好时坏;第二,确认芯片的 I2C 地址,常见默认 7 位地址是 0x55,但如果有地址配置脚或者批次差异,最好用扫描确认。
// i2c_scan.c —— 扫描总线上的从机地址 // 扫描成功说明接线和上拉基本没问题,可以进入下一步 void i2c_scan(void) { for (uint8_t addr = 0x08; addr <= 0x77; addr++) { if (i2c_start(addr << 1) == 0) { // 7 位地址左移 1 位,最低位为写方向 printf("found device at 0x%02X\n", addr); i2c_stop(); } } i2c_stop(); }这里扫描范围从 0x08 开始,是为了避开广播地址和保留地址。addr << 1是把 7 位地址转换成总线上的 8 位地址,最低位是读写方向,这里先发写方向来探测设备是否存在。如果 0x55 在列表里,基本可以确定芯片在位。扫描不到的常见原因有三个:上拉没使能、SDA/SCL 接反、芯片没有上电。注意不要因为板子默认电路就直接跳过扫描,这条经验在多个 Demo 板上都帮过忙。
3.2 读用户内存和写 EEPROM:库底层的三组原语实现
扫描确认地址后,写一个读函数和一个写函数。读函数的关键是“重复起始”(repeated start):先发偏移地址,然后不停止总线,直接再发一个起始信号并切到读方向。这样芯片才能把偏移寄存器保留住,返回出正确的数据。如果先 stop 再 start,部分器件会丢失偏移地址,读到的永远是 0x00 或者随机内容。
// nt3h_rw.c —— 库底层的基础读写原语 #include <stdint.h> extern void i2c_start(uint8_t addr8); extern void i2c_rep_start(uint8_t addr8); extern void i2c_write(uint8_t byte); extern uint8_t i2c_read(int ack); extern void i2c_stop(void); #define NT3H_I2C_ADDR 0x55 // 读数据:先发偏移地址,再用重复起始切到读方向 int nt3h_read(uint8_t offset, uint8_t *buf, uint8_t len) { if (len == 0) return -1; i2c_start(NT3H_I2C_ADDR << 1); // 写方向 i2c_write(offset); // 设置偏移地址 i2c_rep_start((NT3H_I2C_ADDR << 1) | 1); // 重复起始,读方向 for (uint8_t i = 0; i < len; i++) { buf[i] = i2c_read(i < len - 1); // 最后一字节回 NACK } i2c_stop(); return 0; } // 写数据:发偏移地址后连续写,结束后等写周期 int nt3h_write(uint8_t offset, const uint8_t *buf, uint8_t len) { if (len == 0) return -1; i2c_start(NT3H_I2C_ADDR << 1); i2c_write(offset); for (uint8_t i = 0; i < len; i++) { i2c_write(buf[i]); } i2c_stop(); delay_write_cycle(); // 等待内部 EEPROM 写周期完成 return 0; }这两个函数是库的骨架。nt3h_read的解释重点在i2c_rep_start,重复起始可以保留芯片内部的偏移寄存器状态,避免多一次总线反转;nt3h_write的重点则在最后的delay_write_cycle(),这个延时是为了等 EEPROM 内部写周期结束,典型时间是几个毫秒,具体数值要以芯片手册为准。写 SRAM 或者寄存器时,这行延时是可以去掉的,因为这两个区域没有内部写周期。
使用这个库时,参数上有两个容易调错的地方:偏移地址是 8 位还是 16 位。NT3H1X01 这类器件常见的是 8 位偏移,但有些大容量型号支持 16 位,如果你的库函数里偏移参数是 uint16_t,多出来的高字节在 8 位器件上会被忽略,写错了也不会报错,但会读到错误地址。建议先把偏移类型固定为 uint8_t,等到确实需要跨越 256 字节边界时再扩展。
3.3 写周期与传输速率:几个毫秒的等待到底怎么处理
刚上手时最容易疑惑的就是这个“几个毫秒”。芯片手册上写得清清楚楚有内部写周期,但实际测试你会发现,写完一个字节后立刻读,有些时候能读到新值,有些时候读到旧值,有些时候读到 0xFF。这就是写周期没等够的表现。正确的做法是在写周期完成前,不要对该区域发起读操作。
delay_write_cycle()有两个实现方向。简单做法是固定延时,常见取 5 毫秒,缺点是如果每次写都等满 5 毫秒,批量写 32 字节就变成 160 毫秒,体验很差。更好一点的做法是只对 EEPROM 区做这个等待,SRAM 区直接跳过;再进一步,可以写完一块后做一个短轮询,直到读到非 0xFF 的内容确认写周期结束。下面这段是推荐的处理方式。
// nt3h_write_eeprom.c —— 写 EEPROM 后等待写周期完成 // 轮询比固定延时更稳,也更容易排查异常 int nt3h_write_eeprom_with_wait(uint8_t offset, const uint8_t *buf, uint8_t len) { nt3h_write(offset, buf, len); // 先写进去 uint8_t check = 0xFF; int timeout = 100; // 最多轮询 100 次,避免死等 while (timeout--) { nt3h_read(offset, &check, 1); if (check == buf[0]) return 0; // 读回的值与写入值一致,写周期结束 delay_ms(1); } return -1; // 超时,大概率总线或地址有问题 }这段代码的逻辑是:写入后反复读第一个字节,直到读到的内容和写入的第一个字节一致。注意这个方案只适合验证第一个字节,如果这一字节恰好是 0xFF,循环会立即误判成功,所以更完备的做法是写入一个非 0xFF 的标记字节来确认。实际项目中我习惯在写周期的等待上“先延时后轮询”:固定等 1 毫秒,再轮询标记位,这样既不会误判,也不用每次都傻等 5 毫秒。
I2C 速率方面,这个芯片在标准模式下能跑到 100kHz,快速模式一般也能到 400kHz。布线短、上拉合理时用 400kHz 没有问题;如果排线比较长或者板上还有其他慢速器件,建议统一降到 100kHz。低速不会影响 EEPROM 写入,因为写周期本身远大于传输时间,追求 400kHz 对写性能提升有限。
4. 让 MCU 感知手机靠近:FD 中断、SRAM 快速应答与状态轮询
打通 I2C 读写只是第一步。实际应用里,MCU 不可能一直轮询芯片等着手机靠近,它需要被“叫醒”。NT3H1X01 提供了 FD 字段检测引脚和状态寄存器,配合 SRAM 区,可以组成一套完整的事件驱动机制。
4.1 FD 引脚接入中断:把“手机靠近”变成事件
FD 引脚(Field Detect)在芯片检测到射频场时电平会变化。把 FD 接到 MCU 的外部中断引脚上,手机一靠近,MCU 就会收到中断,而不需要轮询。这个功能对低功耗设计尤其重要:MCU 平时可以睡大觉,只在手机靠近时醒来处理数据。
// fd_int.c —— FD 引脚中断配置示意 // 注意:FD 输出的极性和驱动方式因型号而异,实测为准 void fd_init(void) { fd_pin_set_input(); // 配置为输入 fd_pin_enable_pullup(); // 部分型号 FD 为开漏输出,必须加上拉 fd_pin_set_irq(FD_EDGE_RISING, on_fd_isr); // 先按上升沿触发配置 } void on_fd_isr(void) { // 中断里只放标志位,不要做 I2C 读写 phone_approached = 1; }这里有两个坑。一是 FD 引脚可能是开漏输出,必须加上拉电阻才能有确定的电平跳变;二是中断里不要直接做 I2C 读写,因为射频会话可能还在进行,I2C 访问会等待甚至超时,在中断上下文里等待是非常危险的行为。正确做法是中断里只置标志,主循环检测到标志后再去读写数据。这样的设计把“手机靠近”从硬件事件变成了软件事件,后续处理链路就清晰了。
4.2 SRAM 找出一片即写即读的区域,当握手缓冲区
SRAM 区是芯片上最容易被低估的部分。EEOROM 写入有延时,射频写入和 I2C 读取之间天然存在一个“看不见的写周期”。但 SRAM 区是易失性的,读写即时完成,没有内部写周期,非常适合做两个端口之间的握手缓冲区。
常见做法是乒乓结构:分配两个 SRAM 区域,手机通过射频写 A 区,MCU 通过 I2C 读 B 区;等手机写完后,通过一个状态标志通知 MCU 切换,MCU 去读 A 区,同时手机开始写 B 区。这样两个端口各写各的,不需要等对方。SRAM 区在读写性能上的优势非常明显,尤其是在手机连续往芯片写数据的场景里,用 SRAM 代替 EEPROM 做中转,能把整个交互时延从“毫秒级写周期”降到“微秒级传输”。
需要注意的是,SRAM 内容在上电时不保证有效,MCU 复位后不能直接拿 SRAM 里的数据当配置用。如果你把关键参数放 SRAM,上电后必须显式初始化。这也是为什么 SRAM 适合做握手缓冲区而不适合做持久配置存储的原因。
4.3 状态寄存器轮询:如何判断 RF 会话已经结束
FD 中断解决了“手机来了”的感知问题,但中断只发生在进场那一刻。手机读卡过程可能持续几百毫秒,中途可能有多条 APDU 命令交互,MCU 怎么知道“现在可以安全访问了”?答案是读状态寄存器。
芯片内部有一个反映射频场状态的寄存器,字段指示当前是否有射频场存在。库的正确做法是:在主循环里轮询这个字段,只有当射频场不活跃时才发起 I2C 读写。
// nt3h_status.c —— 判断射频会话是否结束 // 返回 0 表示射频空闲,可以安全访问 EEPROM int nt3h_wait_rf_idle(int timeout_ms) { uint8_t status = 0; int waited = 0; while (waited < timeout_ms) { nt3h_read(REG_STATUS, &status, 1); if ((status & STATUS_RF_FIELD) == 0) return 0; // 场不活跃 delay_ms(2); waited += 2; } return -1; }这段代码的逻辑不复杂,但它是整个库的“门卫”。REG_STATUS是状态寄存器的偏移,STATUS_RF_FIELD是其中的场指示位,这两个值需要按芯片手册确认。调用时机也很关键:在任何写 EEPROM 的操作之前调用,而不是在之后。因为如果你已经发起了写操作,射频场又恰好激活,轻则写入被延迟,重则总线超时。
状态轮询和 FD 中断要配合使用:FD 中断负责快速唤醒,状态轮询负责确认安全窗口。只靠中断不轮询,会在射频会话中间误操作;只靠轮询不中断,MCU 空耗电能,低功耗设计直接失败。两者缺一不可。
5. 避坑:NT3H1X01 驱动里最常见的五个翻车现场与排查方法
这个芯片的坑不算多,但每一个都能让调试浪费大半天。以下五条是从实际调试中沉淀出来的,按频率排序,每条都按“现象、原因、解决”来写,方便你直接对照。
5.1 写入后立刻回读全是 0xFF,新数据去哪了
现象:调用写函数后,紧接着读同一地址,读回来的字节是 0xFF,数据像丢了一样。多试几次,偶尔又能读到正确值,非常玄学。
原因:EEPROM 内部写周期没有结束。写入后立刻读,芯片还在处理写操作,读回的旧内容或空白内容被当成数据返回。固定延时不够长,或者延时只加了“写一个字节”的场景,批量写时翻车更明显。
解决:写 EEPROM 后必须等待写周期,推荐用“先延时 1 毫秒 + 轮询首个字节”的方式。如果写入的字节恰好是 0xFF,轮询会误判,建议在数据里放一个非 0xFF 的校验头。另外,批量写时不要每写一个字节等都一次,可以连续写完一整块再统一等待,这个芯片支持页写入,效率更高。
5.2 手机写进来的块,I2C 侧只读到一半
现象:手机通过 NFC 往芯片写了一段配置,MCU 通过 I2C 读的时候,读到一部分新数据、一部分旧数据,配置解析失败。
原因:射频侧的写入按块进行,块单位通常是 4 字节,而 I2C 侧是按字节连续读的。如果 MCU 在射频写块的过程中发起读操作,可能读到新旧数据混合的中间态。芯片本身不提供“写完成”的事务锁,两个端口同时操作同一块区域时,数据一致性要靠软件保证。
解决:不要并发读写同一块区域。要么用状态寄存器判断射频活动,等会话结束再读;要么用 SRAM 乒乓区,手机写 A 区、MCU 读 B 区,写完再切换。实际项目里,配置数据的场景建议优先用“手机写完后再让 MCU 读取”的顺序流程,避免乒乓结构的额外复杂度。
5.3 RF 会话期间 I2C 访问超时,驱动卡死
现象:手机在读卡器上操作时,MCU 的 I2C 读写突然超时,驱动进入错误分支,设备表现为“死机”。手机离开后,设备又恢复正常。
原因:芯片仲裁机制偏向射频侧,I2C 访问会等待射频操作完成。如果等待时间超过了 MCU 驱动里设置的超时上限,I2C 控制器报错,驱动没有做重试或恢复逻辑。
解决:在库的所有读写入口加“射频忙”检查,也就是前面第 4 章的状态轮询。如果检测到射频场活跃,先等待,不发起 I2C 操作。同时把驱动的 I2C 超时时间调大一点,至少覆盖一次 NFC 命令交互的时间,别用默认的几十毫秒量级。
5.4 FD 中断偶尔丢失,低功耗唤醒慢半拍
现象:MCU 进入休眠,手机靠近时大部分时间能被唤醒,但偶尔唤不醒,或者唤醒时间明显变慢。用示波器看 FD 引脚,发现波形有毛刺或者电平没有完全翻转。
原因:FD 引脚输出类型不是想当然的推挽,它可能是开漏,需要外部上拉。上拉没加或者上拉电阻太大,边沿变缓,MCU 的中断检测就可能漏掉。另一个原因是中断触发电平配置与 FD 实际电平极性不匹配。
解决:确认 FD 的电气特性,开漏输出必须加上拉,推荐 4.7k 到 10k 之间;触发电平先用示波器实测,再决定是上升沿还是下降沿。中断服务函数里只置标志位,主循环再兜底做一次状态寄存器轮询,即使中断丢了也能在下一次轮询周期发现手机靠近。这个兜底逻辑在低功耗设计里非常必要。
5.5 密码默认在全零状态,调试完忘了改
现象:功能一切正常,产品也快交付了,突然发现芯片的密码保护根本没用,任何读卡器都能改写配置区。
原因:芯片出厂时密码保护处于未激活状态,密码默认可能为全零。调试过程中没有人去配置密码,也没有在量产环节做初始化,安全问题被完全忽略。
解决:把“修改默认密码并开启保护”写进初始化流程,放在产品量产的第一步。修改密码要在下发任何敏感配置之前完成,否则密码保护的窗口期始终存在。另外,密码本身不建议明文放在源码里,应该由产线工具下发,每台设备可以有不同的密码,这样就算一台设备的密码泄露,影响面也是可控的。
6. 进阶用法:用这个库做手机配网,数据布局与校验都安排上
最后落地一个完整场景:手机通过 NFC 把 Wi-Fi 配置写进芯片,板子上电后通过 I2C 读出来,完成自动配网。这里不展开全部代码,重点讲数据布局和校验怎么设计,这是最容易在后期返工的部分。
6.1 配网数据布局:Magic、长度、校验和双份镜像
// cfg_layout.h —— 配网数据布局定义 #define CFG_MAGIC 0x4E31 // 'N1',标识这是一份有效配置 #define CFG_MAX_LEN 96 typedef struct { uint16_t magic; // 固定魔数,用于识别有效数据 uint16_t len; // 有效载荷长度 uint8_t crc8; // 载荷校验 } cfg_header_t;布局上的三个决定值得展开。第一,数据开头必须有魔数,用来区分“没写过”和“刚好写了全 0xFF”,否则每次上电都会把空白区域当配置解析。第二,要记录长度,不能靠读满整个存储区来猜配置边界。第三,要带校验,射频写入本身有误码可能,I2C 读取也可能因为时序问题读到部分数据,CRC 校验能挡住这两种情况。
6.2 启动时读取配置:校验函数与回退逻辑
// cfg_load.c —— 启动时读取配置并校验 int cfg_load(uint8_t *out, uint16_t *len) { cfg_header_t hdr; nt3h_read(CFG_SLOT_A, (uint8_t *)&hdr, sizeof(hdr)); if (hdr.magic != CFG_MAGIC) return -1; // 槽位 A 无效 if (hdr.len > CFG_MAX_LEN) return -1; nt3h_read(CFG_SLOT_A + sizeof(hdr), out, hdr.len); if (crc8(out, hdr.len) != hdr.crc8) return -1; // 校验失败 *len = hdr.len; return 0; }让我补充一下这段逻辑里的一个细节:按上面的代码,槽位 A 校验失败会直接返回 -1,没有回退逻辑,这个写法是不够完善的。实际项目里通常把它改成先读槽位 A,失败再读槽位 B。双份镜像的成本只是多占一块存储,但对“手机写了一半断电”这种场景非常值。
6.3 两个值得继续做的方向:密码保护与能量采集
配网流程跑通后,有两件事值得继续投入。一是把第 5 章提到的密码保护真正启用,让芯片只能被授权设备改写,这会让配网从“能用”变成“敢用”;二是研究芯片的能量采集能力——VOUT 引脚能从射频场取电,功率不大但足够给低功耗传感器供电,实现“手机贴近即供电、断电无需电池”的玩法,这个方向在 NFC 供电设备上特别有意思。
我自己的习惯是,每次拿到新 NFC 芯片,都会先用这篇笔记里的 dump 脚本扫描一遍地址映射,再跑一遍“写、等、读、验证”的最小闭环,然后才进业务逻辑。这套流程看着简单,但每次都能提前暴露接线、上拉、写周期这些底层问题,省下后面大量排错时间。希望帮到你。
本文还有配套的精品资源,点击获取