NFC Core 的 HCI 后端

概述

HCI 层实现了 ETSI TS 102 622 V10.2.0 规范的大部分内容。 它使编写基于 HCI 的 NFC 驱动程序变得容易。 HCI 层作为 NFC Core 后端运行,实现抽象的 nfc 设备并将 NFC Core API 转换为 HCI 命令和事件。

HCI

HCI 向 NFC Core 注册为 nfc 设备。 来自用户空间的请求通过 netlink 套接字路由到 NFC Core,然后路由到 HCI。 从这一点开始,它们被转换为一系列 HCI 命令,发送到主机控制器(芯片)中的 HCI 层。 命令可以同步执行(发送上下文阻塞等待响应)或异步执行(响应从 HCI Rx 上下文返回)。 也可以从主机控制器接收 HCI 事件。 将对其进行处理,并根据需要将翻译转发到 NFC Core。 有一些钩子可以让 HCI 驱动程序处理专有事件或覆盖标准行为。 HCI 使用 2 个执行上下文

  • 一个用于执行命令:nfc_hci_msg_tx_work()。 在任何给定时刻只能执行一个命令。

  • 一个用于分派接收到的事件和命令:nfc_hci_msg_rx_work()。

HCI 会话初始化

会话初始化是一个 HCI 标准,不幸的是,它必须支持专有网关。 这就是驱动程序将传递一个专有网关列表的原因,该列表必须是会话的一部分。 当设置 hci 设备时,HCI 将确保所有这些网关都连接了管道。 如果芯片支持预先打开的网关和伪静态管道,则驱动程序可以将该信息传递给 HCI core。

HCI 网关和管道

网关定义了可以找到某些服务的“端口”。 为了访问服务,必须创建一个到该网关的管道并将其打开。 在此实现中,管道完全隐藏。 公共 API 仅知道网关。 这与驱动程序需要将命令发送到专有网关而不知道连接到它的管道是一致的。

驱动程序接口

驱动程序通常分为两部分编写:物理链路管理和 HCI 管理。 这使得维护一个可以通过各种 phy (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 目标后调用,以完成 p2p 连接设置,并将需要传递回 nfc core 的硬件参数传递给它。

  • dep_link_down() 被调用以关闭 p2p 链路。

  • target_from_gate() 是一个可选的入口点,用于返回与专有网关相对应的 nfc 协议。

  • complete_target_discovered() 是一个可选的入口点,允许驱动程序执行自动激活已发现目标所需的其他专有处理。

  • 如果需要专有 HCI 命令才能将数据发送到标签,则驱动程序必须实现 im_transceive()。 某些标签类型将需要自定义命令,其他标签类型可以使用标准 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()

打开 phy(电源),使其准备好传输数据

disable()

关闭 phy

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 混合在一起非常容易。

包含的驱动程序

包含一个基于 HCI 的 NXP PN544 驱动程序,通过 I2C 总线连接,并使用 shdlc。

执行上下文

执行上下文如下: - IRQ 处理程序 (IRQH):快速,无法休眠。 将传入的帧发送到 HCI,在 HCI 中,它们被传递到当前的 llc。 对于 shdlc,帧在 shdlc rx 队列中排队。

  • SHDLC 状态机工作程序 (SMW)

    仅当使用 llc_shdlc 时:处理 shdlc rx & tx 队列。

    分派 HCI cmd 响应。

  • HCI Tx Cmd 工作程序 (MSGTXWQ)

    序列化 HCI 命令的执行。

    在响应超时的情况下完成执行。

  • HCI Rx 工作程序 (MSGRXWQ)

    分派传入的 HCI 命令或事件。

  • 来自用户空间调用的 Syscall 上下文 (SYSCALL)

    从 NFC Core 调用的 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 必须从可以休眠的上下文中调用。 大多数情况下,这将是 syscall 上下文。 skb 将返回在响应中收到的结果。

在内部,执行是异步的。 因此,此 API 所做的只是将 HCI 命令排队,在堆栈上设置一个本地等待队列,并wait_event()等待完成。 该等待不可中断,因为可以保证命令在一些短超时后无论如何都会完成。

然后将调度 MSGTXWQ 上下文并调用 nfc_hci_msg_tx_work()。 此函数将取消排队下一个挂起的命令并将其 HCP 片段发送到较低层,较低层恰好是 shdlc。 然后,它将启动一个计时器,以便能够在没有收到响应的情况下完成命令并出现超时错误。

SMW 上下文被调度并调用 nfc_shdlc_sm_work()。 此函数处理 shdlc 的输入和输出成帧。 它使用驱动程序 xmit 发送帧,并在从驱动程序 IRQ 处理程序填充的 skb 队列中接收传入的帧。 SHDLC I(nformation) 帧有效负载是 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 从下面收到错误通知并且轮询处于活动状态时,它将向用户空间发送一个带有空标签列表的标签发现事件,以使其知道轮询操作将永远无法检测到标签。 如果轮询未激活并且该错误是粘滞的,则较低级别将在下次调用时返回它。