1. 项目概述当你的嵌入式设备需要一张“电子身份证”在物联网和智能设备遍地开花的今天你有没有想过如何让一个没有屏幕、没有键盘的嵌入式设备也能和智能手机“握个手”快速交换一点信息比如一个环境传感器把最新的温湿度读数“贴”在自己身上巡检人员用手机一碰就能读取或者一个智能门锁的控制板模拟成一张门禁卡让授权手机一碰即开。这背后的核心技术就是NFC的卡模拟模式。简单来说卡模拟就是让你的设备通常是单片机系统伪装成一个被动的NFC标签或智能卡。它本身不主动发射无线电波而是像一张真正的卡片一样等待读卡器比如手机的射频场来“唤醒”它然后通过负载调制的方式与读卡器通信。TRF7970A这颗芯片的强大之处在于它能同时模拟Type 4A和Type 4B两种主流标签平台相当于你的设备天生就掌握了两种“外语”能与市面上绝大多数NFC读卡器对话兼容性直接拉满。这次我们就以德州仪器的TRF7970A NFC/RFID收发器为核心手把手拆解如何从零构建一个Type 4标签的卡模拟系统。这不是一个简单的寄存器配置教程我会结合我过去在工控和消费电子领域调试NFC的经验把协议栈的“黑盒”打开把防碰撞的“握手”流程理清再把NDEF数据格式的“包装”说明白。你会看到从天线匹配到APDU命令响应每一个环节都有需要注意的“坑”。我们的目标是让你不仅能照着做出来更能理解为什么这么做以及下次遇到问题时知道该从哪里下手。2. 核心思路与方案选型为什么是TRF7970A和Type 4在决定用TRF7970A做Type 4卡模拟之前市面上其实有不少选择。有纯软件模拟低频RFID的有使用专用NFC标签芯片的也有用带NFC功能的单片机。但最终选择TRF7970A这套方案是基于几个非常实际的工程考量。2.1 硬件方案TRF7970A MCU的黄金组合TRF7970A是一颗完整的13.56MHz NFC/RFID收发器。它把射频前端、数据调制解调、协议处理部分都集成在了一颗芯片里。对我们开发者来说最大的好处是它极大地降低了射频设计的门槛。你不需要成为射频专家去调匹配网络当然基础匹配还是要做只需要通过SPI接口告诉它“现在进入Type A卡模拟模式”它就能帮你处理好底层的载波生成、负载调制和曼彻斯特/米勒编码。单片机MCU的角色是大脑。它通过SPI读取TRF7970A接收到的数据帧解析ISO 14443-4和ISO 7816-4的应用层命令如SELECT, READ BINARY, UPDATE BINARY然后组织好响应数据再通过SPI塞回TRF7970A的FIFO让它发送出去。TI的示例代码基于MSP430和MSP432但核心逻辑是通用的你可以移植到STM32、ESP32等任何带有SPI接口的MCU上。注意选择MCU时除了SPI还需注意中断响应速度。在卡模拟过程中从检测到场到回复防碰撞命令的时间要求非常严格通常是几个毫秒内。如果MCU正在处理其他高优先级任务可能会错过时间窗导致模拟失败。因此建议将TRF7970A的中断引脚连接到MCU的一个外部中断引脚并设置较高的中断优先级。2.2 为什么选择模拟Type 4标签NFC论坛定义了多种标签类型Type 1-5Type 4是基于ISO 14443 A/B标准的高阶标签。它的优势非常明显标准化程度高完全遵循ISO 14443-4 (传输协议) 和 ISO 7816-4 (命令集)与智能手机、读卡器的互操作性最好。功能完整支持标准的文件系统结构应用、文件、复杂的访问控制读/写权限和较大的存储空间理论可达64KB。灵活性数据内容采用NDEF格式这是一种通用的“信封”格式里面可以封装文本、URI、电话号码、蓝牙配对信息等多种类型的记录智能手机无需安装特定APP就能识别系统级支持。相比之下Type 1/2标签如MIFARE Classic功能简单且协议私有Type 3FeliCa主要在日韩流行。模拟Type 4意味着你的设备具备了最广泛的通用性。2.3 并发模拟Type 4A/4B的挑战与优势TRF7970A支持在固件层面同时监听Type A和Type B的轮询命令。这听起来很美好但实现上有讲究。Type A和Type B的物理层和防碰撞协议完全不同Type A (ISO 14443A):使用106kbps的米勒编码和ASK 100%调制。防碰撞采用位帧防碰撞算法。Type B (ISO 14443B):使用106kbps的NRZ编码和ASK 10%调制。防碰撞采用时隙ALOHA算法。芯片本身无法同时解调两种信号。因此所谓的“并发模拟”实际上是一种分时监听的策略。固件需要快速在两种模式间切换或者利用TRF7970A的“自动侦测”功能。TI的示例固件采用了前者即在一个循环中先配置为Type A模式监听几毫秒如果没有收到有效的REQA/WUPA命令再切换到Type B模式监听几毫秒如此往复。实操心得这种切换策略会引入额外的响应延迟。在实际测试中如果读卡器端如手机的轮询间隔设置得比较激进可能会在设备切换模式的空档期发起轮询导致首次触碰无反应需要第二次触碰。解决方法是优化切换时序或者根据你的主要应用场景比如主要面对安卓手机Type A居多将监听重心放在一种模式上另一种作为后备。3. 固件架构深度解析从状态机到数据结构拿到TI的示例代码你会发现它并不是一个简单的顺序执行程序而是一个由中断驱动、状态机管理的复杂系统。理解这个架构是进行二次开发和调试的基础。3.1 主循环与中断驱动模型卡模拟是一个强实时性的任务。读卡器的命令不知道何时会来因此不能采用轮询的方式去检查TRF7970A的接收状态那会带来不可接受的延迟。正确的方式是中断驱动。硬件连接将TRF7970A的IRQ引脚连接到MCU的一个外部中断引脚。中断服务程序ISR当TRF7970A接收到有效数据、发送完成或发生错误时会拉低IRQ引脚。在ISR中MCU需要快速读取TRF7970A的IRQ Status寄存器判断事件类型然后设置相应的软件标志位。主循环主循环不断检查这些标志位。例如如果RX_COMPLETE标志被置位主循环就调用相应的函数去FIFO里取数据、解析命令、组织响应、再触发发送。这种架构保证了MCU在大部分时间可以处理其他任务如传感器采样一旦有NFC通信事件能立即被响应。3.2 核心状态机防碰撞与数据交换卡模拟的协议流程本质上是一个状态机。TI的固件清晰地实现了这一点初始状态IDLE设备上电配置TRF7970A为卡模拟模式开始监听射频场。技术检测TECH_DETECTION检测到射频场后进入此状态交替监听Type A和Type B的轮询命令。防碰撞状态ANTICOLLISION收到SENS_REQ(Type A)或SENSB_REQ(Type B)后进入。在此状态下设备与读卡器进行防碰撞对话交换UID/PUPI最终被读卡器选中SELECTED。Type A防碰撞这是一个逐位比较的过程。读卡器发送ANTICOLLISION和SELECT命令携带一个位掩码。设备将自己的UID与掩码比较如果匹配则回复UID的剩余部分。这个过程可能需要多轮级联级别直到7字节或10字节UID全部交换完毕。这里有个关键点在防碰撞阶段Type A命令的CRC是不参与校验的因此需要将TRF7970A的ISO控制寄存器配置为“接收不带CRC”。防碰撞完成后切换到“接收带CRC”模式。Type B防碰撞读卡器发送REQB/WUPB命令指定时隙数(N)。设备在随机选择的一个时隙内回复ATQB响应其中包含一个4字节的伪随机PUPI。读卡器通过ATTRIB命令选中特定的PUPI。与Type A不同Type B的帧始终包含CRC。激活状态ACTIVATED防碰撞成功设备被选中。此时通信进入ISO-DEPISO 14443-4数据交换层。数据交换状态DATA_EXCHANGE在此状态下读卡器发送的是基于ISO 7816-4的APDU命令。对于Type 4标签核心命令只有三个SELECT(CLA00, INSA4)选择文件或应用。READ BINARY(CLA00, INSB0)从指定偏移量读取文件数据。UPDATE BINARY(CLA00, INSD6)向指定偏移量写入数据。释放状态DEACTIVATED通信结束设备回到初始状态等待下一次轮询。3.3 Type 4标签的数据结构设计这是实现功能的核心。TI的示例代码用一组精妙的C结构体定义了整个标签的“文件系统”。// 文件结构 typedef struct { uint16_t ui16FileId; // 文件ID如0xE103 uint8_t * pui8FileData; // 指向文件内容数据的指针 uint16_t ui16FileLen; // 文件数据实际长度 bool bReadOnly; // 文件是否只读 } tType4File; // 应用结构 typedef struct { uint8_t * pui8AppId; // 指向应用ID的指针 uint8_t ui8AppIdLen; // 应用ID长度 tType4File * ptFileArray;// 指向该应用下文件数组的指针 uint8_t ui8FileCount; // 文件数量 } tType4App; // 标签数据结构根 typedef struct { tType4App * ptAppArray; // 指向应用数组的指针 uint8_t ui8AppCount; // 应用数量 } tType4AppDS;这种设计的好处是动态和灵活。你不需要在编译时就把所有NDEF数据写死而是可以在运行时修改pui8FileData指向的缓冲区内容。例如你的设备测量到了一个温度值可以实时格式化成一个NDEF文本记录更新到这个缓冲区中。当手机来读时读到就是最新的数据。3.4 能力容器文件详解能力容器文件是Type 4标签的“目录”和“说明书”其文件ID固定为0xE103。读卡器必须首先读取这个文件才能知道标签里有什么、能干什么。 一个典型的CC文件数据如下示例uint8_t CC_File[] { 0x00, 0x0F, // CC长度15字节 (计算公式7 文件数*8。本例1个文件71*815) 0x20, // 映射版本2.0 0x00, 0xFB, // MLe最大读取长度251字节0xFB 0x00, 0xF9, // MLc最大写入长度249字节0xF9 0x04, // TLV类型0x04代表NDEF文件控制TLV 0x06, // TLV长度后续值域为6字节 0xE1, 0x04, // 文件IDNDEF文件的ID此处为0xE104 0x03, 0xE8, // 最大文件大小1000字节0x03E8 0x00, // 读访问条件0x00表示自由可读 0x00 // 写访问条件0x00表示自由可写0xFF表示只读 };MLe/MLc这两个值限制了单条READ BINARY/UPDATE BINARY命令能操作的最大数据量。设置成0x00FB和0x00F9是为了最大化利用协议允许的帧长减去协议开销。如果你的NDEF消息很小可以适当调小以节省每次通信的时间。访问条件这里的写访问条件0x00必须和tType4File结构体中的bReadOnly标志位逻辑一致。如果CC里声明可写(0x00)但bReadOnly被设为true固件在收到写命令时应返回错误状态字0x6982安全条件不满足。这是一个常见的配置错误点。4. 实操流程与核心代码实现理论讲完了我们动手把系统跑起来。这里以TI的MSP-EXP430F5529LP LaunchPad和DLP-7970ABP BoosterPack为例。4.1 硬件连接与初始化首先确保BoosterPack正确插在LaunchPad上。它们通过预定义的引脚连接。如果你用的是其他MCU需要根据TRF7970A的数据手册连接以下关键信号SPI接口MOSI,MISO,SCLK,CS(片选)EN(使能)高电平有效用于给芯片上电。IRQ(中断请求)开漏输出需上拉用于通知MCU事件。MOD(模式控制)用于选择芯片工作模式在卡模拟中通常置为高电平或根据手册配置。初始化步骤GPIO与SPI初始化配置MCU的SPI为主机模式时钟极性相位(CPOL/CPHA)通常为(0,0)或(1,1)需参考TRF7970A手册。配置EN、CS、MOD为输出IRQ为输入上拉并启用中断。TRF7970A初始化// 1. 硬件复位拉低EN引脚至少1ms再拉高。 NFC_EN_LOW(); delay_ms(2); NFC_EN_HIGH(); delay_ms(5); // 等待电源稳定 // 2. 软件复位发送直接命令0x0C TRF7970A_write_reg(TRF79X0_CHIP_STATUS_CTRL, 0x02); // 先进入Idle模式 TRF7970A_send_direct_command(SOFT_INIT_CMD); // 0x0C TRF7970A_send_direct_command(IDLE_CMD); // 0x00 // 3. 配置基本寄存器 TRF7970A_write_reg(TRF79X0_MODULATOR_SYS_CLK_CTRL, 0x21); // 27.12MHz晶振使能TX TRF7970A_write_reg(TRF79X0_REGULATOR_CONTROL, 0x01); // 使能稳压器输出 TRF7970A_write_reg(TRF79X0_IRQ_MASK, 0x7F); // 使能所有中断源进入卡模拟模式// 同时使能Type A和Type B侦听 TRF7970A_write_reg(TRF79X0_ISO_CONTROL, 0x21); // 使能14443A和14443B侦听 TRF7970A_write_reg(TRF79X0_NFC_TARGET_LEVEL, 0x80); // 设置为被动目标模式 TRF7970A_send_direct_command(RF_ON_CMD); // 0x03开启射频接收4.2 防碰撞流程的实现这是固件中最复杂的部分之一。我们以Type A为例看代码如何实现void handle_type_a_anticollision(uint8_t *p_rx_data, uint8_t rx_length) { static uint8_t cascade_level 1; static uint8_t uid_index 0; uint8_t sel_cmd p_rx_data[0]; uint8_t sel_par p_rx_data[1]; if (sel_cmd 0x93 cascade_level 1) { // SEL_CMD for CL1 // 检查SEL_PAR是否与我们的UID前几位匹配 if ((sel_par 0xE0) 0x20) { // 检查字节地址 // 计算需要回复的UID片段 uint8_t nvb sel_par 0x1F; // NVB (Number of Valid Bits) // 根据nvb从UID数组中提取需要发送的位并组织回复 uint8_t response[5]; // UID片段 BCC // ... 填充response ... // 关键在防碰撞阶段发送不带CRC TRF7970A_write_reg(TRF79X0_ISO_CONTROL, 0xA4); // RX without CRC send_response(response, response_len, false); // false表示不加CRC } else if (sel_par 0x70) { // SELECT命令 // 防碰撞完成发送SAK uint8_t sak_response[] {0x08}; // SAK值表示UID完整支持14443-4 TRF7970A_write_reg(TRF79X0_ISO_CONTROL, 0x24); // 切换回RX with CRC send_response(sak_response, 1, true); // true表示加CRC current_state STATE_ACTIVATED; // 进入激活状态 cascade_level 1; // 重置级联级别 } } // ... 处理其他级联级别 (0x95, 0x97) ... }注意事项在防碰撞阶段SEL_PAR参数的低5位(NVB)指示了本轮需要比较/发送的UID位数。固件需要根据NVB进行位操作从完整的UID中提取出正确的位进行回复。这是一个容易出错的位运算过程务必仔细调试。4.3 数据交换层APDU命令的解析与响应进入数据交换层后所有命令都是APDU格式。我们需要解析CLA,INS,P1,P2,Lc,Data,Le等字段。void handle_apdu_command(uint8_t *p_apdu, uint8_t apdu_len) { uint8_t cla p_apdu[0]; uint8_t ins p_apdu[1]; uint8_t p1 p_apdu[2]; uint8_t p2 p_apdu[3]; uint8_t lc 0; uint8_t *p_data NULL; uint8_t le 0; if (apdu_len 5) { // 可能包含Lc和Data if ((p_apdu[4] 0xFF) 0x00) { // 扩展长度略复杂此处简化 // 处理扩展长度 } else { lc p_apdu[4]; p_data p_apdu[5]; } } // 判断Le (期望返回的数据长度) 可能位于命令末尾 switch(ins) { case 0xA4: // SELECT handle_select_command(p1, p2, p_data, lc); break; case 0xB0: // READ BINARY handle_read_binary_command(p1, p2, le); break; case 0xD6: // UPDATE BINARY handle_update_binary_command(p1, p2, p_data, lc); break; default: // 不支持的指令返回6A 81 (功能不支持) send_apdu_response(SW_FUNC_NOT_SUPPORTED); break; } } void handle_read_binary_command(uint8_t p1, uint8_t p2, uint8_t le) { uint16_t offset (p1 8) | p2; // P1P2构成偏移地址 tType4File *p_current_file get_current_selected_file(); // 获取当前选中的文件 if (offset le p_current_file-ui16FileLen) { send_apdu_response(SW_WRONG_LENGTH); // 6C XX return; } if (p_current_file-bReadOnly false) { // 检查读权限通常都可读 // 从文件数据指针处根据偏移量复制数据到发送缓冲区 memcpy(tx_buffer, (p_current_file-pui8FileData[offset]), le); send_apdu_response_with_data(tx_buffer, le, SW_OK); // 90 00 } else { send_apdu_response(SW_SECURITY_STATUS_NOT_SATISFIED); // 69 82 } }关键点READ BINARY和UPDATE BINARY命令中的P1和P2两个字节共同组成了一个16位的文件偏移地址。这意味着单个文件的大小不能超过64KB。在组织响应时状态字SW1SW2如0x9000表示成功必须附加在数据后面如果有数据或单独发送。4.4 NDEF记录的构建与更新NDEF是数据的“包装纸”。一个最简单的文本类型NDEF记录在内存中的布局如下// 示例文本记录 Hello, NFC! uint8_t ndef_text_buffer[] { 0x00, 0x11, // NDEF消息长度不包括这2字节长度头 0xD1, // NDEF记录头MB1(消息开始), ME1(消息结束), CF0, SR1(短记录), IL0, TNF001(NDEF标准类型) 0x01, // 类型长度1字节 0x0E, // 载荷长度14字节 0x54, // 记录类型T (0x54) 表示文本类型 // 载荷开始 0x02, // 状态字节UTF-8编码语言代码长度2字节 0x65, 0x6E, // 语言代码en (英语) 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x4E, 0x46, 0x43, 0x21 // Hello, NFC! };当你的设备需要更新NDEF内容时比如传感器新数据只需按照NDEF规范重新生成这样一个字节数组然后更新到tType4File结构体所指向的缓冲区中。UPDATE BINARY命令处理函数需要实现对这个缓冲区的安全写入。5. 调试技巧与常见问题排查实录调试NFC卡模拟逻辑分析仪和一台支持NFC日志功能的安卓手机是你的最佳伙伴。5.1 工具准备逻辑分析仪连接MCU的SPI引脚SCLK, MOSI, MISO, CS和TRF7970A的IRQ引脚。可以清晰地看到命令/响应时序和数据流对排查防碰撞和数据交换问题至关重要。NFC调试APP如“NFC Tools”或“NFC TagInfo”。它们可以读取标签详细信息并显示原始的APDU通信日志。示波器用于观察天线两端的波形检查13.56MHz载波是否正常调制深度是否足够。5.2 典型问题与解决方案问题现象可能原因排查步骤与解决方案手机完全检测不到标签1. 射频场未激活。2. 天线匹配严重失调。3. TRF7970A未正确进入目标模式。1. 用示波器测天线两端应有~13.56MHz正弦波峰峰值约几伏到十几伏取决于读卡器功率和距离。2. 检查天线匹配电路通常是LC串联谐振确保谐振在13.56MHz。可用网络分析仪或通过测量天线的DC电阻应很小和感应电压初步判断。3. 检查TRF79X0_ISO_CONTROL和TRF79X0_NFC_TARGET_LEVEL寄存器配置是否正确并确认已发送RF_ON_CMD。手机能检测到标签但无法读取内容显示“标签类型不支持”1. 防碰撞流程失败。2. CC文件格式错误或内容为空。3. 未正确响应SELECT应用命令。1. 用逻辑分析仪抓取SPI数据对照ISO 14443-3标准看防碰撞命令REQA, ANTICOLLISION, SELECT和响应是否正确。重点检查Type A防碰撞时CRC模式的切换时机。2. 用手机APP的“读标签”功能看能否读到CC文件ID: 0xE103。如果读不到检查CC文件数据指针是否正确以及READ BINARY命令处理函数是否正常工作。3. 确认SELECT命令INS0xA4的响应是否正确返回了文件控制信息FCI或简单的成功状态0x9000。手机能读取但无法写入1. CC文件中该文件的“写访问条件”被设置为0xFF只读。2.tType4File结构中的bReadOnly标志被设为true。3.UPDATE BINARY命令处理函数有bug。1. 检查CC文件中对应文件的写访问字节是否为0x00。2. 检查代码中对应文件结构的bReadOnly成员是否为false。3. 用逻辑分析仪或手机APDU日志查看手机发送的UPDATE BINARY命令数据是否完整以及你的固件返回的状态字是什么。如果是0x6A82文件未找到检查文件选择逻辑如果是0x6982安全状态不满足检查写权限逻辑。通信不稳定时好时坏1. 天线Q值过高或过低导致带宽过窄或调制深度不足。2. MCU中断响应不及时。3. 电源噪声。1. 调整天线匹配电路中的并联阻尼电阻改变Q值。Q值太高带宽窄容易失谐Q值太低带宽宽则调制幅度小。通常将带宽设计在1MHz左右是个不错的起点。2. 确保NFC中断具有足够高的优先级。在中断服务程序ISR中只做标记繁重的解析工作放到主循环。3. 在TRF7970A的电源引脚就近放置足够大的去耦电容如10uF钽电容100nF陶瓷电容。模拟Type A正常Type B不正常或反之1. 技术检测切换时序不佳错过了轮询。2. Type B的PUPI生成或ATTRIB响应有问题。1. 增加每种模式的监听时间或根据你的主要使用场景暂时禁用另一种模式的侦听以作测试。2. Type B的PUPI通常是一个4字节随机数。确保在每次模拟会话开始时生成新的PUPI。检查ATTRIB命令的响应数据是否正确包含了PUPI和协议参数。5.3 一个真实的调试案例CRC校验导致的“幽灵”问题我曾遇到一个诡异的问题设备在大部分手机上工作正常但在某一款特定型号的安卓手机上十次有八次读不出来。用逻辑分析仪抓取SPI日志对比发现在出问题的手机上防碰撞阶段的SELECT命令后我的设备回复了SAK但手机紧接着发送了一个RATS命令而我的设备似乎没有正确响应。仔细对比协议栈发现在Type A防碰撞完成后从“防碰撞状态”切换到“激活状态”时我忘记将TRF7970A的ISO控制寄存器从“接收不带CRC”切换回“接收带CRC”。大部分手机的NFC栈容错性较好即使不带CRC的RATS命令也尝试处理了。但那款特定手机的NFC控制器非常严格对于不带CRC的RATS命令直接丢弃导致通信链路无法建立。教训协议切换的每一个细节都必须严格按照标准实现。在Type A流程中SENS_REQ和ANTICOLLISION命令不带CRC从SELECT命令开始包括RATS,PPS等的所有命令都带CRC。这个切换点必须精确。实现一个稳定可靠的NFC卡模拟系统是对嵌入式开发者协议理解、实时编程和硬件调试能力的综合考验。从天线匹配的硬件调校到防碰撞状态机的软件实现再到NDEF数据格式的灵活组织每一步都需要耐心和细致。TRF7970A提供了一个强大的硬件平台而理解其背后的原理和陷阱才能让你真正驾驭这项技术为你那些“沉默”的嵌入式设备赋予便捷的无线交互能力。当你看到手机轻轻一碰就读取到设备内部传感器实时数据的那一刻你会觉得这一切的折腾都是值得的。