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/ 中查找。

创建新驱动程序

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

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

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

  1. 将 fuchsia_driver_component 或 driver_package 构建目标放在正确的位置,以便将驱动程序包含在系统中。
  2. 对于打包的驱动程序,应将 driver_package 构建目标添加到 //boards 或 //vendor/<foo>/boards 中的相关板文件,并添加到 xx_package_labels GN 实参中。
  3. 对于启动驱动程序,应将 fuchsia_driver_component 构建目标添加到 //boards 或 //vendor/<foo>/boards 中的相关板文件,并添加到 board_bootfs_labels GN 实参中。
  4. 在 <PATH>:tests 构建目标中添加 tests 构建目标,以便将 测试包含在 CQ 中。
  5. 在 meta/<NAME>.bind 中添加适当的绑定规则。
  6. 在 meta/<NAME>-info.json 中添加驱动程序信息。该文件必须包含 short_description 和 areas,且至少与 //build/drivers/areas.txt 中列出的一个区域匹配。
  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 命名空间中的 DEVICE 以及给定的 PCI 类/子类/接口匹配。pci 命名空间是从第一行的 fucnsia.pci 库导入的。如需了解详情,请参阅绑定 文档。

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

driver_bind_rules("bind") {
    rules = "meta/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_str_prop_t pci_device_props[] = {
    ddk::MakeStrProperty(bind_fuchsia::PCI_VID,
        static_cast<uint32_t>(device.info.vendor_id)),
    ddk::MakeStrProperty(bind_fuchsia::PCI_DID,
        static_cast<uint32_t>(device.info.device_id)),
    ddk::MakeStrProperty(bind_fuchsia::PCI_CLASS,
        static_cast<uint32_t>(device.info.base_class)),
    ddk::MakeStrProperty(bind_fuchsia::PCI_SUBCLASS,
        static_cast<uint32_t>(device.info.sub_class)),
    ddk::MakeStrProperty(bind_fuchsia::PCI_INTERFACE,
        static_cast<uint32_t>(device.info.program_interface)),
    ddk::MakeStrProperty(bind_fuchsia::PCI_REVISION,
        static_cast<uint32_t>(device.info.revision_id)),
    ddk::MakeStrProperty(bind_fuchsia::PCI_TOPO, pci_bind_topo),
};

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

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

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

驱动程序绑定

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

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

bind() 通常有四种结果:

  1. 驱动程序确定设备受支持,并且不需要执行任何 繁重的工作,因此会使用 C 中的 device_add() 或 ddk::Device::DdkAdd() 在 DDKTL C++ 封装库中发布新设备,并返回 `ZX_OK`。

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

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

  4. 驱动程序表示一个总线或控制器,其中包含 0..n 个可能会动态显示或消失的子级。在这种情况下,它应立即发布一个表示总线或控制器的设备,然后动态发布表示该总线上硬件的子级(下游驱动程序将绑定到这些子级)。示例:AHCI/SATA、USB 等。

在系统添加设备并使其可见后,客户端进程可以使用该设备,并且兼容的驱动程序可以绑定该设备。

Banjo 协议

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

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

驱动程序操作

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

来自驱动程序宿主外部进程的外部客户端请求由子级驱动程序(通常在同一进程中)处理,这些请求由与设备类对应的 banjo 协议处理。驱动程序到驱动程序的请求应使用 banjo 协议,而不是设备操作。

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

设备中断

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

内核将根据中断是边沿触发还是电平触发,自动处理屏蔽和取消屏蔽中断。对于电平触发的硬件中断, zx_interrupt_wait() 将在返回之前屏蔽 中断,并在下次再次调用时取消屏蔽中断 。对于边沿触发的中断,中断保持取消屏蔽状态。

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

您可以使用 zx_interrupt_trigger() 在 slot ZX_INTERRUPT_SLOT_USER 上发出中断句柄信号,以从 zx_interrupt_wait() 返回。这对于在驱动程序清理期间关闭中断线程是必要的。

FIDL 消息

非驱动程序进程

每个设备类的消息都在 FIDL 语言中定义。每个设备实现零个或多个 FIDL 协议,这些协议通过每个客户端的单个通道进行多路复用。驱动程序有机会通过 message() hook 来解释 FIDL 消息。

其他进程中的驱动程序

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

协议操作与 FIDL 消息

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

隔离设备

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

驱动程序权限

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

Privileged operations such as zx_vmo_create_contiguous() and zx_interrupt_create require a 根资源句柄。除了 系统驱动程序(ACPI 和 ARM 系统上的平台)之外,其他驱动程序无法使用此句柄。设备应请求其父级为其执行此类操作。如果父级驱动程序的协议未解决此用例,请与父级驱动程序的作者联系。

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

高级主题和提示

初始化时间过长

如果您的设备需要很长时间才能完成初始化,该怎么办?在上面讨论 null_bind() 函数时,我们指出成功返回会告知驱动程序管理器驱动程序现在与设备关联。我们不能在绑定函数中花费大量时间;我们基本上需要初始化设备、发布设备并完成操作。

但您的设备可能需要执行耗时的初始化操作,例如:

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

等等,这些操作可能需要很长时间才能完成。

您可以通过实现设备 init() hook 将设备发布为“不可见”。init() hook 在通过 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_get_name() 检索设备的名称

管理函数

以下函数用于管理设备:

函数 用途
device_add() 向父级添加设备
device_async_remove() 安排移除设备及其所有子级