NFC 核心的 HCI 后端¶
作者:Eric Lapuyade,Samuel Ortiz
概述¶
HCI 层实现了 ETSI TS 102 622 V10.2.0 规范的大部分内容。它使编写基于 HCI 的 NFC 驱动程序变得容易。HCI 层作为 NFC 核心后端运行,实现抽象的 nfc 设备,并将 NFC 核心 API 转换为 HCI 命令和事件。
HCI¶
HCI 向 NFC 核心注册为 nfc 设备。来自用户空间的请求通过 netlink 套接字路由到 NFC 核心,然后路由到 HCI。从这一点开始,它们被转换为发送到主机控制器(芯片)中 HCI 层的一系列 HCI 命令。命令可以同步执行(发送上下文阻塞等待响应)或异步执行(响应从 HCI Rx 上下文返回)。也可以从主机控制器接收 HCI 事件。将处理它们,并在需要时将转换转发到 NFC 核心。有一些钩子可以让 HCI 驱动程序处理专有事件或覆盖标准行为。HCI 使用 2 个执行上下文
一个用于执行命令:nfc_hci_msg_tx_work()。任何给定时刻只能执行一个命令。
一个用于分派接收到的事件和命令:nfc_hci_msg_rx_work()。
HCI 会话初始化¶
会话初始化是一个 HCI 标准,但遗憾的是必须支持专有网关。这就是驱动程序将传递必须成为会话一部分的专有网关列表的原因。当设置 hci 设备时,HCI 将确保所有这些网关都连接了管道。如果芯片支持预打开的网关和伪静态管道,则驱动程序可以将该信息传递给 HCI 核心。
HCI 网关和管道¶
网关定义了可以找到某些服务的“端口”。为了访问服务,必须创建到该网关的管道并打开它。在此实现中,管道是完全隐藏的。公共 API 只知道网关。这与驱动程序需要将命令发送到专有网关而不知道连接到它的管道是一致的。
驱动程序接口¶
驱动程序通常分为两部分编写:物理链路管理和 HCI 管理。这使得维护可以使用各种物理层(i2c、spi 等)连接的芯片的驱动程序变得更容易。
HCI 管理¶
驱动程序通常会向 HCI 注册自身并提供以下入口点
struct nfc_hci_ops {
int (*open)(struct nfc_hci_dev *hdev);
void (*close)(struct nfc_hci_dev *hdev);
int (*hci_ready) (struct nfc_hci_dev *hdev);
int (*xmit) (struct nfc_hci_dev *hdev, struct sk_buff *skb);
int (*start_poll) (struct nfc_hci_dev *hdev,
u32 im_protocols, u32 tm_protocols);
int (*dep_link_up)(struct nfc_hci_dev *hdev, struct nfc_target *target,
u8 comm_mode, u8 *gb, size_t gb_len);
int (*dep_link_down)(struct nfc_hci_dev *hdev);
int (*target_from_gate) (struct nfc_hci_dev *hdev, u8 gate,
struct nfc_target *target);
int (*complete_target_discovered) (struct nfc_hci_dev *hdev, u8 gate,
struct nfc_target *target);
int (*im_transceive) (struct nfc_hci_dev *hdev,
struct nfc_target *target, struct sk_buff *skb,
data_exchange_cb_t cb, void *cb_context);
int (*tm_send)(struct nfc_hci_dev *hdev, struct sk_buff *skb);
int (*check_presence)(struct nfc_hci_dev *hdev,
struct nfc_target *target);
int (*event_received)(struct nfc_hci_dev *hdev, u8 gate, u8 event,
struct sk_buff *skb);
};
open() 和 close() 应打开和关闭硬件。
hci_ready() 是一个可选的入口点,在设置 hci 会话后立即调用。驱动程序可以使用它来执行必须使用 HCI 命令执行的额外初始化。
xmit() 应简单地将帧写入物理链路。
start_poll() 是一个可选的入口点,应将硬件设置为轮询模式。仅当硬件使用专有网关或与 HCI 标准略有不同的机制时,才必须实现此功能。
dep_link_up() 在检测到 p2p 目标后调用,以使用需要传递回 nfc 核心的硬件参数完成 p2p 连接设置。
dep_link_down() 被调用以关闭 p2p 链路。
target_from_gate() 是一个可选的入口点,用于返回与专有网关对应的 nfc 协议。
complete_target_discovered() 是一个可选的入口点,允许驱动程序执行自动激活发现的目标所必需的额外专有处理。
im_transceive() 如果需要专有的 HCI 命令才能将数据发送到标签,则驱动程序必须实现此功能。某些标签类型将需要自定义命令,其他标签类型可以使用标准 HCI 命令写入。驱动程序可以检查标签类型,并执行专有处理,或返回 1 以请求标准处理。数据交换命令本身必须异步发送。
tm_send() 在 p2p 连接的情况下被调用以发送数据
check_presence() 是一个可选的入口点,核心将定期调用该入口点以检查激活的标签是否仍在场中。如果未实现此功能,核心将无法向用户空间推送 tag_lost 事件
event_received() 被调用以处理来自芯片的事件。驱动程序可以处理该事件,或返回 1 以允许 HCI 尝试标准处理。
在 rx 路径上,驱动程序负责使用 nfc_hci_recv_frame() 将传入的 HCP 帧推送到 HCI。HCI 将负责重新聚合和处理。这必须从可以休眠的上下文中完成。
PHY 管理¶
物理链路 (i2c, ...) 管理由以下结构定义
struct nfc_phy_ops {
int (*write)(void *dev_id, struct sk_buff *skb);
int (*enable)(void *dev_id);
void (*disable)(void *dev_id);
};
- enable()
打开物理层(通电),使其准备好传输数据
- disable()
关闭物理层
- write()
将数据帧发送到芯片。请注意,为了使诸如 llc 之类的高层能够存储该帧以进行重新发射,此函数不得更改 skb。它也不得返回正结果(成功返回 0,失败返回负值)。
来自芯片的数据应直接发送到 nfc_hci_recv_frame()。
LLC¶
CPU 和芯片之间的通信通常需要一些链路层协议。这些协议作为由 HCI 层管理的模块隔离。目前有两个模块:nop(原始传输)和 shdlc。新的 llc 必须实现以下函数
struct nfc_llc_ops {
void *(*init) (struct nfc_hci_dev *hdev, xmit_to_drv_t xmit_to_drv,
rcv_to_hci_t rcv_to_hci, int tx_headroom,
int tx_tailroom, int *rx_headroom, int *rx_tailroom,
llc_failure_t llc_failure);
void (*deinit) (struct nfc_llc *llc);
int (*start) (struct nfc_llc *llc);
int (*stop) (struct nfc_llc *llc);
void (*rcv_from_drv) (struct nfc_llc *llc, struct sk_buff *skb);
int (*xmit_from_hci) (struct nfc_llc *llc, struct sk_buff *skb);
};
- init()
分配和初始化您的私有存储
- deinit()
清理
- start()
建立逻辑连接
- stop()
终止逻辑连接
- rcv_from_drv()
处理来自芯片的、发送到 HCI 的数据
- xmit_from_hci()
处理由 HCI 发送的、发送到芯片的数据
llc 必须先向 nfc 注册,然后才能使用。通过调用来完成此操作
nfc_llc_register(const char *name, const struct nfc_llc_ops *ops);
再次注意,llc 不处理物理链路。因此,对于给定的芯片驱动程序,将任何物理链路与任何 llc 混合使用非常容易。
包含的驱动程序¶
包括一个用于 NXP PN544 的基于 HCI 的驱动程序,该驱动程序通过 I2C 总线连接,并使用 shdlc。
执行上下文¶
执行上下文如下:- IRQ 处理程序 (IRQH):速度快,无法休眠。将传入的帧发送到 HCI,在其中传递给当前 llc。在 shdlc 的情况下,该帧在 shdlc rx 队列中排队。
SHDLC 状态机工作程序 (SMW)
仅当使用 llc_shdlc 时:处理 shdlc rx 和 tx 队列。
分派 HCI cmd 响应。
HCI Tx Cmd 工作程序 (MSGTXWQ)
序列化 HCI 命令的执行。
在响应超时的情况下完成执行。
HCI Rx 工作程序 (MSGRXWQ)
分派传入的 HCI 命令或事件。
来自用户空间调用的系统调用上下文 (SYSCALL)
从 NFC 核心调用的 HCI 中的任何入口点
执行 HCI 命令的工作流程(使用 shdlc)¶
可以使用以下 API 轻松地同步执行 HCI 命令
int nfc_hci_send_cmd (struct nfc_hci_dev *hdev, u8 gate, u8 cmd,
const u8 *param, size_t param_len, struct sk_buff **skb)
必须从可以休眠的上下文中调用 API。大多数情况下,这将是系统调用上下文。skb 将返回在响应中收到的结果。
在内部,执行是异步的。因此,此 API 所做的只是将 HCI 命令入队,在堆栈上设置本地等待队列,并wait_event()
等待完成。等待是不可中断的,因为它保证命令在某些短超时后会完成。
然后将调度 MSGTXWQ 上下文并调用 nfc_hci_msg_tx_work()。此函数将下一个挂起的命令出队,并将其 HCP 片段发送到下层,即 shdlc。然后它将启动一个计时器,以便在没有收到响应时可以使用超时错误完成命令。
SMW 上下文被调度并调用 nfc_shdlc_sm_work()。此函数处理 shdlc 帧的传入和传出。它使用驱动程序的 xmit 发送帧,并在从驱动程序 IRQ 处理程序填充的 skb 队列中接收传入帧。SHDLC I(信息)帧的有效负载是 HCP 片段。它们被聚合以形成完整的 HCI 帧,该帧可以是响应、命令或事件。
HCI 响应会立即从此上下文中分派,以解除等待命令执行的阻塞。响应处理涉及调用 nfc_hci_msg_tx_work() 发送命令时提供的完成回调。完成回调然后将唤醒 syscall 上下文。
也可以使用此 API 异步执行命令。
static int nfc_hci_execute_cmd_async(struct nfc_hci_dev *hdev, u8 pipe, u8 cmd,
const u8 *param, size_t param_len,
data_exchange_cb_t cb, void *cb_context)
工作流程相同,只不过 API 调用会立即返回,并且回调将使用 SMW 上下文的结果进行调用。
接收 HCI 事件或命令的工作流程¶
HCI 命令或事件不是从 SMW 上下文中分派的。相反,它们被排队到 HCI rx_queue,并将从 HCI rx 工作线程上下文(MSGRXWQ)分派。这样做是为了允许 cmd 或事件处理程序也执行其他命令(例如,处理来自 PN544 的 NFC_HCI_EVT_TARGET_DISCOVERED 事件需要向读取器 A 门发出 ANY_GET_PARAMETER 以获取有关已发现目标的信息)。
通常,此类事件将从 MSGRXWQ 上下文传播到 NFC Core。
错误管理¶
与 NFC Core 请求执行同步发生的错误会简单地作为请求的执行结果返回。这些都很简单。
异步发生的错误(例如,在后台协议处理线程中)必须报告,以便上层不会忽视下面发生的事情,并且知道预期的事件可能永远不会发生。这些错误的处理方式如下
驱动程序 (pn544) 无法传递传入帧:它会存储错误,以便任何后续对驱动程序的调用都将导致此错误。然后它使用 NULL 参数调用标准 nfc_shdlc_recv_frame() 以向上报告问题。shdlc 存储 EREMOTEIO 粘性状态,这将触发 SMW 反过来向上报告。
SMW 基本上是一个后台线程,用于处理传入和传出的 shdlc 帧。当此线程发现由于 shdlc 或以下发生的不可恢复的错误而无法再运行时,它也会检查 shdlc 粘性状态并报告给 HCI。如果问题发生在 shdlc 连接期间,则错误会通过连接完成报告。
HCI:如果发生内部 HCI 错误(帧丢失),或者 HCI 从下层报告错误,则 HCI 要么使用该错误完成当前正在执行的命令,要么在没有命令正在执行时直接通知 NFC Core。
NFC Core:当 NFC Core 被通知来自下面的错误且轮询处于活动状态时,它将向用户空间发送一个带有空标签列表的标签发现事件,以告知用户轮询操作将永远无法检测到标签。如果轮询未激活并且错误是粘性的,则较低级别将在下次调用时返回该错误。