Fuchsia 驱动程序开发 (DFv1)

Fuchsia 驱动程序是在用户空间的驱动程序主机进程中动态加载的共享库。加载驱动程序的过程由驱动程序管理器控制。如需详细了解驱动程序主机、驱动程序管理器以及驱动程序和设备生命周期,请参阅设备型号

目录结构

在整个源代码树中,您可以在源代码布局文档中指定的区域的 driver 子目录下找到驱动程序。大多数 Fuchsia 驱动程序位于 //src/devices/ 下。系统会根据它们实现的协议对其进行分组。驱动程序协议在 ddk/include/lib/ddk/protodefs.h 中定义。例如,USB 以太网驱动程序位于 //src/connectivity/ethernet/drivers/ 中,而非 //src/devices/usb/drivers/ 中,因为它实现了以太网协议。不过,实现 USB 堆栈的驱动程序位于 //src/devices/usb/drivers/ 中,因为它们实现了 USB 协议。

在驱动程序的 BUILD.gn 中,应该有一个 fuchsia_driver_component 目标。为了让驱动程序显示在 /boot/driver 下,应在 //boards 下相关开发板文件内的 board_bootfs_labels 列表中包含该驱动程序。为了让它显示在 /system/driver 内部,应将其添加到具有 driver_package 构建目标的系统软件包中,然后 //boards 下的相关板级文件应引用该目标。对于可加载的驱动程序,驱动程序管理器会首先位于 /boot/driver 中,然后位于 /system/driver/ 中。

创建新驱动程序

您可以使用 Create Tool 自动创建新驱动程序。只需运行以下命令:

fx create driver --path <PATH> --lang cpp

这将创建包含空驱动程序的目录 <PATH>,其中 <PATH> 的最后一段是驱动程序名称和 GN 目标名称。运行此命令后,需要执行以下步骤:

  1. 在正确的位置添加 fuchsia_driver_componentdriver_package 构建目标,以使驱动程序加入系统。
  2. 对于打包的驱动程序,应将 driver_package 构建目标添加到 //boards//vendor/<foo>/boards 中的相关板级文件,并添加到 xx_package_labels GN 参数中。
  3. 对于启动驱动程序,fuchsia_driver_component build 目标应添加到相关板级文件中的 //boards//vendor/<foo>/boards 中,并添加到 board_bootfs_labels GN 参数中。
  4. <PATH>:tests build 目标中添加 tests build 目标,以使测试包含在 CQ 中。
  5. <NAME>.bind 中添加适当的绑定规则。
  6. <NAME>-info.json 中添加驱动程序信息。该文件必须包含与 //build/drivers/areas.txt 中列出的至少一个区域匹配的 short_descriptionareas
  7. 为驱动程序编写功能。

声明驱动程序

驱动程序至少应包含驱动程序声明并实现 bind() 驱动程序操作。

当驱动程序管理器成功为设备找到匹配的驱动程序时,便会加载驱动程序并绑定到设备。驱动程序通过绑定规则声明与其兼容的设备,这些规则应与驱动程序一起放在 .bind 文件中。绑定编译器会编译这些规则,并在 C 头文件中创建包含这些规则的驱动程序声明宏。以下绑定规则声明了 AHCI 驱动程序

using fuchsia.pci;
using fuchsia.pci.massstorage;

fuchsia.BIND_PROTOCOL == fuchsia.pci.BIND_PROTOCOL.DEVICE;
fuchsia.BIND_PCI_CLASS == fuchsia.pci.BIND_PCI_CLASS.MASS_STORAGE;
fuchsia.BIND_PCI_SUBCLASS == fuchsia.pci.massstorage.BIND_PCI_SUBCLASS_SATA;
fuchsia.BIND_PCI_INTERFACE == 0x01;
fuchsia.BIND_COMPOSITE == 1;

这些绑定规则声明驱动程序绑定到 BIND_PROTOCOL 属性与 pci 命名空间和给定 PCI 类/子类/接口中的 DEVICE 相匹配的设备。pci 命名空间是从第一行的 fucnsia.pci 库导入的。如需了解详情,请参阅绑定文档

如需生成包含这些绑定规则的驱动程序声明宏,应该有相应的 bind_rules 构建目标。这应该声明与绑定文件中的“using”语句相对应的依赖项。

driver_bind_rules("bind") {
    rules = "ahci.bind"
    bind_output = "ahci.bindbc"
    deps = [
        "//src/devices/bind/fuchsia.pci",
        "//src/devices/bind/fuchsia.pci.massstorage",
    ]
}

驱动程序现在可以包含生成的标头,并使用以下宏声明自身。"zircon" 是供应商 ID,"0.1" 是驱动程序版本。

#include <lib/ddk/binding_driver.h>
...
ZIRCON_DRIVER(ahci, ahci_driver_ops, "zircon", "0.1");

PCI 驱动程序会发布具有以下属性的匹配设备:

zx_device_prop_t device_props[] = {
    {BIND_PROTOCOL, 0, ZX_PROTOCOL_PCI},
    {BIND_PCI_VID, 0, info.vendor_id},
    {BIND_PCI_DID, 0, info.device_id},
    {BIND_PCI_CLASS, 0, info.base_class},
    {BIND_PCI_SUBCLASS, 0, info.sub_class},
    {BIND_PCI_INTERFACE, 0, info.program_interface},
    {BIND_PCI_REVISION, 0, info.revision_id},
    {BIND_PCI_BDF_ADDR, 0, BIND_PCI_BDF_PACK(info.bus_id, info.dev_id,
                                             info.func_id)},
};

目前,绑定变量和宏在 lib/ddk/binding.h 中定义。在不久的将来,所有节点属性都将由绑定库(例如上面导入的 fuchsia.pci 库)定义。如果您要引入新的设备类,则可能需要为绑定头文件以及绑定库引入新的节点属性。

节点属性是 32 位值。如果您的变量值需要大于 32 位的值,请将其拆分为多个 32 位变量。例如 ACPI HID 值,其长度为 8 个字符(64 位)。它分为 BIND_ACPI_HID_0_3BIND_ACPI_HID_4_7。完成绑定库的迁移后,您将能够使用其他数据类型,例如字符串、较大数字和布尔值。

您可以在 bind_rules 构建规则中指定 disable_autobind = true 以停用自动绑定行为。在这种情况下,可以使用 fuchsia.device.Controller/Bind FIDL 调用将驱动程序绑定到设备。

驱动程序绑定

当驱动程序与设备匹配时,系统会调用驱动程序的 bind() 函数。通常情况下,驱动程序将初始化设备所需的任何数据结构,并在此函数中初始化硬件。它不应在此函数中执行任何耗时的任务或块,因为它是从驱动程序主机的 RPC 线程调用的,在此期间无法为其他请求提供服务。相反,它应该生成一个新线程来执行冗长的任务。

驱动程序不应对 bind() 中的硬件状态做任何假设。可能需要重置硬件或以其他方式确保硬件处于已知状态。由于系统会通过重新生成驱动程序主机从驱动程序崩溃中恢复,因此在调用 bind() 时,硬件可能处于未知状态。

bind() 通常会带来四种结果:

  1. 驱动程序确定设备是受支持的设备,因此无需执行任何繁重操作,因此使用 C 语言device_add()或在 DDKTLC++ 封装容器库中ddk::Device::DdkAdd()发布新设备,并返回 `ZX_OK。

  2. 驱动程序确定,即使绑定规则匹配,设备也无法受支持(可能是由于检查 hw 版本位或其他原因),并返回错误。

  3. 驱动程序需要在设备准备就绪或确定它可以支持之前执行进一步的初始化,因此它会发布一个实现 init() 钩子的不可见设备并启动线程以继续工作,同时将 ZX_OK 返回给 bind()。该线程最终将调用 C 中的 device_init_reply()DDKTL C++ 封装容器库中的 ddk::InitTxn::Reply()。在收到回复之前,不可以移除设备。如果能够成功初始化设备,并且应将其设为可见,则状态会指示 ZX_OK;或者指示应移除设备的错误。

  4. 驱动程序表示具有 0..n 个子项(可以动态出现或消失)的总线或控制器。在这种情况下,它应该立即发布一台表示总线或控制器的设备,然后动态发布表示该总线上硬件的子项(下游驱动程序将绑定到这些子节点)。示例:AHCI/SATA、USB 等

设备添加并被系统可见后,便可供客户端进程使用,并可供兼容的驱动程序进行绑定。

班卓琴的琴弦

驱动程序为设备提供一组设备操作和可选的协议操作。设备操作会实现设备生命周期方法以及由其他用户空间应用和服务调用的设备的外部接口。协议操作可实现设备的进程内协议,由加载到同一驱动程序主机的其他驱动程序调用。

您可以在 device_add_args_t 中为设备传递一组协议操作。如果设备支持多个协议,请实现 get_protocol() 设备操作。一台设备只能有一个协议 ID。协议 ID 对应于设备在 devfs 下发布的类。

驱动程序操作

驱动程序通常通过处理来自子驱动程序或其他进程的客户端请求来运行。它通过直接与硬件通信(例如通过 MMIO)或与其父级设备通信(例如将 USB 事务加入队列)来满足这些请求。

来自驱动程序主机外部进程的外部客户端请求由子驱动程序(通常在同一进程)中实现,由与设备类对应的班卓琴协议执行。驾驶员到驾驶员请求应使用 banjo 协议,而不是设备操作。

设备可以通过在其父设备上调用 device_get_protocol() 来获取其父对象支持的协议。

设备中断

设备中断由中断对象实现,中断对象是一种内核对象。驱动程序通过设备协议方法从其父设备请求设备中断句柄。返回的句柄将绑定到设备的相应中断(如父驱动程序所定义)。例如,PCI 协议会为 PCI 子级实现 map_interrupt()。驱动程序应生成一个线程来等待中断句柄。

内核将根据需要自动遮盖和取消遮盖中断,具体取决于中断是边缘触发还是电平触发。对于电平触发的硬件中断,zx_interrupt_wait() 会在返回之前遮盖中断,并在下次再次调用时取消遮盖中断。对于边缘触发的中断,中断会保持未遮盖状态。

中断线程不应执行任何长时间运行的任务。对于执行冗长任务的驱动程序,请使用工作器线程。

您可以通过槽位 ZX_INTERRUPT_SLOT_USER 上的 zx_interrupt_trigger() 发出信号来指示中断句柄,以便从 zx_interrupt_wait() 返回。要在清理驱动程序期间关闭中断线程,必须执行此操作。

FIDL 消息

非驱动程序进程

每个设备类的消息均使用 FIDL 语言定义。每个设备实现零个或多个 FIDL 协议,在每个客户端通过一个通道进行多路复用。驱动程序有机会通过 message() 钩子解读 FIDL 消息。非驱动程序组件只能通过 devfs 访问这些文件。

其他进程中的驱动程序

如果驱动程序需要在单独的进程中与驱动程序进行通信(而不是定义协议操作),则必须改为托管一个传出目录,类似于组件,后者应托管子驱动程序会在绑定时访问的所有 FIDL 协议。

协议操作与 FIDL 消息

协议操作用于定义设备的进程内 API。FIDL 消息定义了用于进程外通信的 API。如果函数要由同一进程中的其他驱动程序调用,请定义协议操作。驱动程序应在其父项上调用协议操作以使用这些函数。

隔离设备

使用 DEVICE_ADD_MUST_ISOLATE 添加的设备会生成新的驱动程序主机。设备必须有一个托管 FIDL 协议的随附传出目录。绑定到设备的驱动程序将被加载到新的驱动程序主机中,并能够连接在父级驱动程序提供的传出目录中导出的 FIDL 协议。

驾驶权

虽然驱动程序在用户空间进程中运行,但与普通进程相比,驱动程序拥有一组限制更严格的权限。不允许驱动程序访问文件系统,包括 devfs。这意味着驾驶员无法与任意设备交互。如果驱动程序需要执行此操作,请考虑改为编写服务组件。例如,虚拟控制台由 virtcon 组件实现。

zx_vmo_create_contiguous()zx_interrupt_create 等特权操作需要根资源句柄。此句柄不适用于系统驱动程序(x86 系统上的 ACPI 和 ARM 系统上的平台)以外的驱动程序。设备应请求其父级为其执行此类操作。如果父级驱动程序的协议无法解决此用例,请与父级驱动程序的作者联系。

同样,驱动程序也不得请求任意 MMIO 范围、中断或 GPIO。总线驱动程序(如 PCI 和平台)仅返回与子设备关联的资源。

高级主题和提示

初始化用时过长

如果设备需要很长时间来初始化,该怎么办?当我们讨论上述 null_bind() 函数时,我们表示成功的返回会告知驱动程序管理器,驱动程序现在已与设备关联。我们不能在绑定函数上花费太多时间;基本上,我们应该先初始化设备,发布设备,然后完成相关工作。

但是,您的设备可能需要执行一个很长的初始化操作,例如:

  • 枚举硬件点
  • 加载固件
  • 协商协议

依此类推,这可能需要很长时间。

您可以通过实现设备 init() 钩子,将设备发布为“不可见”状态。init() 钩子在通过 device_add() 添加设备后运行,可用于安全地访问设备状态和生成工作器线程。设备将保持不可见状态,并保证在调用 device_init_reply()(可从任何线程中完成)之前不会将其移除。这满足绑定函数的要求,但任何人都无法使用您的设备(因为因为不可见,所以没有人知道它)。现在,您的设备可以使用后台线程执行长时间操作。

当您的设备准备好处理客户端请求时,调用 device_init_reply(),这样会使它显示在路径名空间中。

省电

为了支持省电功能或其他节约资源的功能,您的设备可以使用两个宣传信息:suspend()resume()

两者都接受设备上下文指针和标志参数,但标志参数仅用于挂起情形。

举报 含义
DEVICE_SUSPEND_FLAG_REBOOT 驱动程序应自行关闭,为机器的重新启动或关闭做好准备
DEVICE_SUSPEND_FLAG_REBOOT_BOOTLOADER
DEVICE_SUSPEND_FLAG_REBOOT_RECOVERY
DEVICE_SUSPEND_FLAG_POWEROFF 驾驶员应自行关闭以准备关机
DEVICE_SUSPEND_FLAG_MEXEC 驱动程序应自行关闭,为软重启做好准备
DEVICE_SUSPEND_FLAG_SUSPEND_RAM 驱动程序应能够从 RAM 重新启动

参考文档:支持函数

本部分列出了为驱动程序提供的支持函数。

访问器函数

作为第一个参数传递给驱动程序协议函数的上下文块是一个不透明数据结构。这意味着,要访问数据元素,您需要调用访问器函数:

功能 目的
device_get_name() 检索设备的名称

管理功能

以下函数用于管理设备:

功能 目的
device_add() 向家长添加设备
device_async_remove() 安排移除设备及其所有子级