14.9 近场通信 (NFC)
如果你觉得蓝牙的几米通信距离还不够近,那我们再靠近一点。
近到什么程度?近到物理接触。
这就是 NFC(Near Field Communication)要干的事。它是一种短距离(通常小于 2 英寸)、低延迟的无线技术,设计初衷就是为了在两个设备「碰」在一起的瞬间,交换少量数据。它的速度上限是 424 kb/s,这在千兆网卡面前听起来像是史前时代的产物,但对于它的应用场景来说,绰绰有余。
NFC 承载的数据量通常不大——可能只是一个 URL、一段文本,或者是用来触发蓝牙/WiFi 连接切换的配对信息。它的核心逻辑是将「物理接近」与「立即动作」绑定。你拿着手机碰一下公交闸机、碰一下海报上的标签,或者碰一下另一个手机,数据就交换完成了,不需要像蓝牙那样先「发现设备」、再「配对」、再「连接」。
NFC 工作在 13.65MHz 频段,底层基于 RFID 的 ISO14443 和 Sony FeliCa 标准。这项技术的标准化工作主要由 NFC Forum(http://www.nfc-forum.org/)负责,他们定义了一整套规范,从底层的数字协议到上层的连接切换、医疗设备通信都有。好消息是,这些规范都是公开免费的。
NDEF:通用的「信封」
在 NFC 的标准体系中,最核心的是 NDEF(NFC Data Exchange Format)。你可以把它理解为 NFC 世界里的「通用语言」或「标准信封」。
无论是读取标签还是设备间点对点通信,数据载荷都被封装成 NDEF 格式。一个 NDEF 记录里包含两部分:
- 头部:元数据,告诉接收方「我是什么类型的数据」(比如是 URL 还是文本)。
- 载荷:真正的数据。
这种设计让应用程序无需关心底层协议细节,只需要解析 NDEF 记录,就能知道该触发什么动作——是打开浏览器,还是拨打电话。
NFC 设备与标签:谁供电,谁说话
在 NFC 的世界里,设备和标签有着根本的区别。
标签:沉默的石头
NFC 标签是极其简单的设备。它们通常只是一颗小小的闪存芯片加上一个感应天线,封装在贴纸、钥匙扣或者卡片里。它们没有电池,也不具备主动发射无线电场的能力。
它们怎么工作? 靠「偷」电。
当一个有源 NFC 设备(比如你的手机)靠近时,手机发出的无线电场会在标签的天线上产生感应电流,正是这点微薄的能量唤醒了芯片,让它能把存储的数据发回去。这种机制被称为被动通信。
NFC Forum 定义了四种标签类型,每一型背后都有深厚的历史包袱(RFID 和智能卡):
- Type 1:基于 Innovision/Broadcom 的 Topaz 和 Jewel 规格。容量很小(96 字节 ~ 2KB),速度只有 106 kb/s。便宜。
- Type 2:基于 NXP Mifare Ultralight。和 Type 1 类似,也是廉价的民用级标签。
- Type 3:基于 Sony FeliCa。这是日本那一套标准(比如 Suica 卡)。容量更大(可达 1MB),速度更快(212 或 424 kb/s),当然也贵一些。
- Type 4:基于 NXP DESFire。支持加密和更复杂的访问控制,容量可达 32KB,支持三种速度。
设备:主动的一方
NFC 设备(手机、读卡器)是主动方。它们能产生磁场,也能读回数据。但更重要的是,它们能干三件事:
- 读/写模式:读写标签。
- 卡模拟模式:假装自己是一张标签(比如手机刷公交卡)。
- 点对点模式(P2P):两个 NFC 设备互相对话。
三种操作模式
Linux 内核的 NFC 支持主要围绕这三种模式展开,我们需要区分清楚:
- 读/写:这是最简单的。手机去读标签,或者往标签里写数据。
- 点对点:这是 NFC 特有的。两个设备碰在一起,建立一条 LLCP(Logical Link Control Protocol) 链路。在这条链路上,可以跑各种服务(比如 SNEP 用来传 NDEF 数据,或者 Connection Handover 用来切换蓝牙连接)。
- 卡模拟:这是支付行业最看重的。手机假装成一张卡,读卡器(POS 机)来读它。为了安全,这通常涉及到「安全单元」,手机里的支付程序运行在一个受信任的环境里,直接控制 NFC 射频。
内核视角:从硬件到 Socket
与 Android 将整个 NFC 协议栈塞进用户空间不同,标准 Linux 内核承担了相当一部分工作。从 3.1 内核开始,Linux 引入了 AF_NFC 这个 Socket 协议族,以及一套用于控制 NFC 的 Generic Netlink 接口。
这种设计的思路是:内核负责抽象硬件和提供标准接口,用户空间负责复杂的业务逻辑。
驱动层:HCI vs NCI
NFC 的硬件生态曾经非常混乱。早期的控制器大多沿用 ETSI 制定的 HCI 标准,这本来是用来给 SIM 卡和非接触前端通信的,并不完全适配 NFC。结果就是,各家厂商(NXP, Broadcom 等)都在这个标准上加了无数的私有扩展。
为了收拾这个烂摊子,NFC Forum 定义了一套新的接口:NCI(NFC Controller Interface)。现在,NCI 已经成为主流,新出的芯片几乎都支持这个标准。
在内核里,驱动开发这这根据芯片支持的接口,选择注册到不同的子系统:
- 基于 NCI 的驱动(如
nfcwilink)注册到 NCI 层。 - 基于 ETSI HCI 的驱动(如
pn544,microread)注册到 HCI 层。 - 基于纯 USB 私有协议的驱动(如
pn533)直接注册到 NFC Core。
核心注册流程
不管走哪条路,最终的归宿都是 nfc_register_device()。
场景 A:直接注册到 Core(最底层)
如果你在写一个基于 pn533 这种用了奇怪 USB 协议的驱动,你需要:
- 调用
nfc_allocate_device()分配一个nfc_dev结构。 - 填充
nfc_ops回调函数(这是最累的,因为你要手动实现轮询、激活目标等底层逻辑)。start_poll: 开始轮询标签。stop_poll: 停止轮询。activate_target: 激活选中的目标。deactivate_target: 停用目标。im_transceive: 发送并接收数据。
- 调用
nfc_register_device()把设备注册进内核。
场景 B:注册到 HCI 层(稍微轻松点)
如果你用的是支持 HCI 的芯片(比如 pn544),HCI 层帮你屏蔽了很多细节:
- 调用
nfc_hci_allocate_device()。 - 调用
nfc_hci_register_device()。- 这里面会自动帮你调用
nfc_register_device()。 - HCI 层会提供一套默认的
hci_nfc_ops,你不需要自己实现全部五个回调。
- 这里面会自动帮你调用
场景 C:注册到 NCI 层(现代标准) 这是现在的推荐做法,流程和 HCI 类似:
nci_allocate_device()。nci_register_device()。- 同样会间接调用
nfc_register_device()。
- 同样会间接调用
这种分层架构让内核可以灵活应对各种历史遗留芯片和现代标准芯片。
用户空间接口:Socket 与 Netlink
内核把路铺好了,用户空间怎么用?主要有两条路:Socket 和 Netlink。
1. NFC Sockets (AF_NFC)
这是 NFC 通信的「高速公路」。内核实现了两种 Socket:
-
Raw Sockets (
SOCK_RAW):- 用途:用于读写器模式。
- 机制:你可以直接把标签特定的命令扔给驱动,驱动原封不动地发给标签,收到的数据也原封不动地还给你。内核不解析内容。
- 操作:
connect()用来选中并激活一个检测到的标签。send/recv用来收发数据。
-
LLCP Sockets (
SOCK_STREAM/SOCK_DGRAM):- 用途:用于点对点模式。
- 机制:内核帮你把复杂的 LLCP 链路层协议(连接建立、分片、重组)都处理好了,你只需要像写普通网络程序一样读写数据流。
- 操作:
connect():连接到对方设备上的某个服务(比如 SNEP)。bind():把某个服务注册到本地,让别人能连你。
2. NFC Netlink API
这是 NFC 的「控制台」。如果你想控制 NFC 适配器的行为(而不是收发业务数据),就得走 Netlink。
通过 Netlink,你可以:
- 列出所有的 NFC 控制器。
- 给控制器上电、断电。
- 开启/停止轮询(Polling)模式,去发现周围的标签或设备。
- 查询对方设备提供了哪些 LLCP 服务。
- 管理安全单元。
Netlink 同时也是一个事件通道。当内核检测到新标签、新设备,或者安全单元有交易发生时,它会通过 Netlink 广播事件给监听的应用程序。
初始化流程:nfc_init()
内核启动时,NFC 子系统的初始化顺序非常讲究,必须层层递进,不能乱。net/nfc/core.c 里的 nfc_init() 函数展示了这个装配过程:
static int __init nfc_init(void)
{
int rc;
// 1. 先把 Netlink 这个「控制台」搭起来
rc = nfc_genl_init();
if (rc)
goto err_genl;
// 2. 初始化 Raw Socket 支持(为了读写标签)
rc = rawsock_init();
if (rc)
goto err_rawsock;
// 3. 初始化 LLCP 协议栈(为了点对点通信)
rc = nfc_llcp_init();
if (rc)
goto err_llcp_sock;
// 4. 最后注册整个 AF_NFC 协议族
rc = af_nfc_init();
if (rc)
goto err_af_nfc;
return 0;
// ... 错误处理 ...
}
这里有一个依赖关系:LLCP 依赖于 Raw 机制,而 AF_NFC 协议族的注册依赖于前面所有的基础设施就绪。如果在 nfc_llcp_init() 失败了,整个 NFC 功能就不可用,必须回滚。
用户空间守护进程:neard
虽然内核提供了底层的 Socket 和 Netlink API,但直接拿这些 API 写应用太痛苦了。你需要处理各种协议细节、状态机。于是,Linux 社区搞了一个用户空间守护进程:neard。
neard 的角色就像是蓝牙领域的 BlueZ,它运行在内核 API 之上,通过 D-Bus 向上层应用提供统一、简洁的接口。
它实现了:
- 所有四种标签类型的读写逻辑。
- 所有 NFC Forum 规定的 P2P 协议(SNEP, Connection Handover 等)。
D-Bus API 的设计非常直观:
org.neard.Adapter:代表你的 NFC 适配器(比如手机里的芯片)。你可以用它来开启轮询。org.neard.Tag/org.neard.Device:代表检测到的实体。你可以调用Push方法发数据,或者Write方法写标签。org.neard.Record:代表 NDEF 记录。应用可以注册一个 Agent 来接收原始 NDEF 数据,或者直接解析成可读的文本/URL。
Android 的做法
Linux 内核 + neard 的组合是标准做法,但 Android 走了一条不同的路。
Android 把整个 NFC 协议栈(包括 NCI 实现、LLCP、SNEP 等)都搬到了用户空间,放在 HAL(Hardware Abstraction Layer)层里。内核里只留了一个极简的驱动桩,只负责把数据通过 I2C/SPI/UART 传给硬件。
Google 提供了一套基于 Broadcom NCI 规范的参考实现。对于手机厂商来说,如果你的芯片支持 NCI,移植起来相对容易;如果不是,那就得自己写适配层了。这也是为什么有些第三方 ROM 移植了 NFC 功能后,手机却读不出卡——HAL 层没匹配对。
本章回响
这一节我们完成了从「接触式」到「近场」的跨越。
表面上看,我们在讨论一种「碰一碰」的技术,实际上我们是在看Linux 如何处理一套极其碎片化的硬件生态。NFC 的历史包袱比蓝牙还要重——RFID 标准、ETSI HCI、厂商私有协议混杂在一起。Linux 内核通过引入 NCI、抽象 HCI、定义统一的 Socket 和 Netlink API,硬是在这片废墟上建立了一套可维护的架构。
还记得上一节蓝牙的类比吗?如果说蓝牙是架设「货运专线」,那么 NFC 就是建立了一个「握手协议」。它的重点不在于搬运大数据,而在于建立信任和触发上下文。内核在这里的角色,是确保无论底层的芯片是 NXP 的还是 Broadcom 的,上层应用都只需要关心「我要碰一下」这个动作,而不需要关心射频层发生了什么。
下一节,我们将退后一步,不再关注某一种具体的无线技术,而是去看支撑整个网络子系统运转的「神经系统」——通知链。它是网络设备状态变化(比如插拔网线、UP/DOWN)能够瞬间传导到整个系统的根本原因。