如何向 NFC 标签写入 NDEF:编码方法与手机实操

把一条网址、一段文字或一个自动化指令写进 NFC 贴纸,本质只有两步:第一步把内容编码成 NDEF 二进制报文第二步通过某个平台通道把字节写进标签。手机 App(如"NFC Tools")只是把这两步打包给你。本文先把两步原理讲透,再分别给出 Android、iOS、Web NFC 和 PC/SC 读卡器四条通道的实操方法,最后覆盖容量校验、写保护与常见写入失败排查。

一、写入的两步原理

第一步:编码。以写入网址 https://cupass.com 为例,需要构造一条 URI 记录:

D1 01 0C 55 04 63 75 70 61 73 73 2E 63 6F 6D
│  │  │  └"U"┘ └0x04=https:// └"cupass.com"─┘
│  │  └载荷长 12 字节
│  └类型长 1
└D1 = MB|ME|SR(单记录短报文),TNF=1(Well-Known)

编码规则细节(记录头标志位、TNF 取值、URI 前缀缩写表)在《NDEF 报文格式详解》中已逐位拆解,此处不再重复。第二步:加壳写入。Type 2 标签(NTAG、Ultralight)要求把 NDEF 报文包进 TLV 外壳再逐页写入:

TLV 外壳 = 03 [报文长度] [NDEF 报文] FE
示例:03 0F D1 01 0C 55 04 ... 63 6F 6D FE

二、选对标签:容量与格式对照

动手前先确认标签型号与可用容量——NDEF 可用空间比标称容量小,因为 TLV 外壳、锁定/配置页都占空间:

标签总容量NDEF 可用(约)典型用途
NTAG 213180 字节137 字节名片、网址贴纸
NTAG 215540 字节492 字节通用场景(Amiibo 同款)
NTAG 216924 字节872 字节长文本、vCard
MIFARE Classic 1K1024 字节716 字节*旧款手机兼容性差

*Classic 需要 NDEF 应用目录(MAD)与扇区授权,部分手机 NDEF 栈直接拒绝访问;做 NDEF 用途优先选 NTAG 系列,出厂即 Type 2 格式化、兼容性最好。

购买建议:通用玩法选 NTAG 215 贴纸;贴在手机壳、笔记本等金属表面必须买抗金属(Anti-metal)背胶款,普通贴纸贴金属上完全无法读取。

三、Android 手机:NFC Tools 或 App 内代码

普通用户用 NFC Tools(App)即可:添加记录(URL / 文本 / Wi-Fi)→ 写入 → 贴标签完成。需要集成进自己 App 的开发者,走系统 API:

// 已格式化的标签:Ndef 通道
Ndef ndef = Ndef.get(tag);
ndef.connect();
if (!ndef.isWritable()) { /* 只读标签,放弃 */ }
if (ndef.getMaxSize() < messageSize) { /* 容量不足 */ }
ndef.writeNdefMessage(ndefMessage);   // 直接写完整报文
ndef.close();

// 空白未格式化标签:NdefFormatable 通道
NdefFormatable fmt = NdefFormatable.get(tag);
if (fmt != null) {
    fmt.connect();
    fmt.format(ndefMessage);   // 格式化并写入首条报文
    fmt.close();
}

关键点:Ndef.get(tag) 返回 null 不代表标签坏了,只说明它还没被 NDEF 格式化——改走 NdefFormatable。MIFARE Classic 上若 format() 抛异常,通常是授权问题,需先用 MifareClassic 通道认证。

四、iPhone:iOS 快捷指令

iOS 不能自由写任意记录,但快捷指令(Shortcuts)覆盖了最常用的两类:

  1. 快捷指令 → 自动化 → 新建 → "轻碰 NFC" → 扫描标签;
  2. 若标签为空白,系统会提示"写入通知",确认后把该标签绑定到自动化;
  3. 自动化动作里可挂:打开网页、发送信息、运行家庭场景、执行任意快捷指令。

限制说明:iOS 的"写"实质是为空白标签初始化并绑定自动化,不能像 Android 那样写入任意 MIME/External 记录;要写复杂报文仍需 Android 手机或外接读卡器。读取方面,iPhone XS 及以后的后台识读已很完善,第三方 App 可用 Core NFC 读取 NDEF。

五、Web NFC:浏览器直接写标签

Chrome(Android)支持 Web NFC,网页里十几行代码即可写标签,适合做在线标签生成工具:

async function writeTag() {
  const ndef = new NDEFReader();
  await ndef.write({
    records: [
      { recordType: "url",  data: "https://cupass.com" },
      { recordType: "text", data: "资产编号 A-1024" }
    ]
  });
}
// 调用时机:必须由用户手势(点击按钮)触发,页面必须 HTTPS

浏览器会自动处理 TLV 外壳与分页,你只管给记录。注意兼容性:iOS Safari 与桌面 Chrome 均不支持,仅在部分 Android Chrome 上可用。

六、PC/SC 读卡器:逐页写入 NTAG

用 ACR122U 等读卡器 + Python(pyscard)可以对标签做最底层的控制,也是批量制卡的主要方式:

from smartcard.System import readers

def write_ntag_pages(ndef_bytes, conn):
    """把 TLV 包裹后的 NDEF 写进 NTAG,每页 4 字节,从第 4 页开始。"""
    tlv = bytes([0x03, len(ndef_bytes)]) + ndef_bytes + bytes([0xFE])
    for i in range(0, len(tlv), 4):
        page = 4 + i // 4
        data = tlv[i:i+4].ljust(4, b'\x00')
        # CC/特殊页跳过逻辑:NTAG 213 的页 3 是 Capability Container,
        # 实际写入时按型号跳过锁定页与配置页(10/11 页等)
        apdu = [0xFF, 0xD6, 0x00, page, 0x04] + list(data)
        resp, sw1, sw2 = conn.transmit(apdu)
        assert (sw1, sw2) == (0x90, 0x00), f"页 {page} 写入失败 {sw1:02X}{sw2:02X}"

写入前若标签已有旧数据,需先用 0x03 0x00 0xFE 写一个空 TLV 覆盖,否则新旧报文会粘连导致手机解析出乱码。MIFARE Classic 的写入要先对扇区认证(见《MIFARE Classic 存取控制位详解》),此处不展开。

七、写前校验与写后锁定

容量校验:报文总字节数(含 TLV 壳)不得超过标签可用空间,超限时优先启用 URI 前缀缩写、缩短语言码或分块。写后验证:写完立刻用手机碰一次确认弹出预期内容。

锁定时机很重要——先写入内容、验证无误,最后再锁:

保护方式NTAGMIFARE Classic
内容锁定(只读)写动态锁字节(锁定已用页)数据块配 010 只读
永久锁死锁字节本身置 0(不可逆)控制位配为不可写
密码保护PWD_AUTH + PACK扇区密钥 A/B
永久锁定不可逆:NTAG 的锁字节一旦置 0、Classic 的控制位一旦写成 deny-write,任何工具都无法再改。营业场景(如可更新的资产标签)只做"内容锁定",保留更新能力。

八、写入失败排查表

现象原因处理
App 提示"标签不支持写入"标签只读/锁死,或是无存储的卡(银行卡等)换空白 NTAG 标签
写入成功但手机读取为空TLV 外壳缺失或长度字段错误用 03 [len] [NDEF] FE 重新包壳
读取内容出现旧数据粘连未清空旧 TLV 就追加写入先写空 TLV(03 00 FE)再写新内容
NdefFormatable.format 抛异常Classic 扇区被密钥保护先认证或改用 Type 2 标签
写入后弹出错误网址URI 前缀代码与后缀不匹配对照前缀表检查 0x04 等代码
贴金属表面读不到普通标签在金属上失谐换抗金属背胶标签

九、相关工具与延伸阅读

编码完成后可在动手前先做纸上验证:编码转换工具箱 做 hex 与文本的逐字节对照;写 Classic 前先读《MIFARE Classic 存取控制位详解》避免锁卡;报文解析与格式细节见《NDEF 报文格式详解》;不确定手里标签是什么芯片,先看《NFC 标签类型识别:ATQA、SAK、ATS 完全解读》