如何向 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 213 | 180 字节 | 137 字节 | 名片、网址贴纸 |
| NTAG 215 | 540 字节 | 492 字节 | 通用场景(Amiibo 同款) |
| NTAG 216 | 924 字节 | 872 字节 | 长文本、vCard |
| MIFARE Classic 1K | 1024 字节 | 716 字节* | 旧款手机兼容性差 |
*Classic 需要 NDEF 应用目录(MAD)与扇区授权,部分手机 NDEF 栈直接拒绝访问;做 NDEF 用途优先选 NTAG 系列,出厂即 Type 2 格式化、兼容性最好。
三、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)覆盖了最常用的两类:
- 快捷指令 → 自动化 → 新建 → "轻碰 NFC" → 扫描标签;
- 若标签为空白,系统会提示"写入通知",确认后把该标签绑定到自动化;
- 自动化动作里可挂:打开网页、发送信息、运行家庭场景、执行任意快捷指令。
限制说明: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 前缀缩写、缩短语言码或分块。写后验证:写完立刻用手机碰一次确认弹出预期内容。
锁定时机很重要——先写入内容、验证无误,最后再锁:
| 保护方式 | NTAG | MIFARE Classic |
|---|---|---|
| 内容锁定(只读) | 写动态锁字节(锁定已用页) | 数据块配 010 只读 |
| 永久锁死 | 锁字节本身置 0(不可逆) | 控制位配为不可写 |
| 密码保护 | PWD_AUTH + PACK | 扇区密钥 A/B |
八、写入失败排查表
| 现象 | 原因 | 处理 |
|---|---|---|
| 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 完全解读》。
卡智通