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 目标名称。运行此命令后,需要执行以下步骤:
- 在正确的位置添加
fuchsia_driver_component
或driver_package
build 目标,以将驱动程序添加到系统中。 - 对于打包的驱动程序,应将
driver_package
构建目标添加到//boards
或//vendor/<foo>/boards
中的相关开发板文件,或添加到xx_package_labels
GN 参数。 - 对于启动驱动程序,应将
fuchsia_driver_component
构建目标添加到//boards
中的相关开发板文件,或添加到board_bootfs_labels
GN 参数的//vendor/<foo>/boards
中。 - 在
<PATH>:tests
构建目标中添加tests
构建目标,以便将您的测试纳入 CQ。 - 在
meta/<NAME>.bind
中添加了适当的绑定规则。 - 在
meta/<NAME>-info.json
中添加了驱动程序信息。该文件必须包含与//build/drivers/areas.txt
中列出的至少一个区域匹配的short_description
和areas
。 - 为驱动程序编写功能。
声明驱动程序
驱动程序至少应包含驱动程序声明并实现 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;
这些绑定规则表明,驱动程序会与 pci
命名空间中的 DEVICE
和给定 PCI 类/子类/接口匹配的 BIND_PROTOCOL
属性的设备绑定。第一行从 fucnsia.pci
库导入了 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
build 规则中指定 disable_autobind = true
以停用自动绑定行为。在这种情况下,可以使用 fuchsia.device.Controller/Bind
FIDL 调用将驱动程序绑定到设备。
驱动程序绑定
当驱动程序与设备匹配时,系统会调用驱动程序的 bind()
函数。通常,驱动程序会在此函数中初始化设备所需的所有数据结构并初始化硬件。它不应在此函数中执行任何耗时任务或阻塞,因为它是从驱动程序主机的 RPC 线程中调用的,在此期间无法处理其他请求。而是应生成新线程来执行耗时任务。
驱动程序不应对 bind()
中的硬件状态做任何假设。它可能需要重置硬件或以其他方式确保其处于已知状态。由于系统通过重新生成驱动程序主机来从驱动程序崩溃中恢复,因此在调用 bind()
时,硬件可能处于未知状态。
bind()
通常有以下四种结果:
驱动程序确定设备受支持且无需执行任何繁重工作,因此在 C 中使用
device_add()
或在 DDKTL C++ 封装容器库中使用ddk::Device::DdkAdd()
发布新设备,并返回 `ZX_OK。驱动程序确定,即使绑定规则匹配,也无法支持设备(可能是因为检查硬件版本位或其他原因),并返回错误。
驱动程序需要在设备准备就绪或确定可以支持它之前执行进一步的初始化,因此它会发布一个实现
init()
钩子的不可见设备,并启动一个线程以继续工作,同时将ZX_OK
返回给bind()
。该线程最终会调用 C 中的device_init_reply()
或 DDKTL C++ 封装容器库中的ddk::InitTxn::Reply()
。在收到回复之前,我们保证不会移除设备。如果能够成功初始化设备且应使其可见,状态为ZX_OK
;如果出现错误,则表示应移除设备。驱动程序表示具有 0..n 个可能动态显示或消失的子项的总线或控制器。在这种情况下,它应立即发布一个代表总线或控制器的设备,然后动态发布代表该总线上的硬件的子设备(下游驱动程序将绑定到这些子设备)。示例:AHCI/SATA、USB 等。
设备被添加并由系统公开后,系统会将其提供给客户端进程,并供兼容的驱动程序绑定。
Banjo 协议
驱动程序向设备提供一组设备操作和可选的协议操作。设备操作会实现设备生命周期方法以及其他用户空间应用和服务调用的设备外部接口。协议操作会实现设备的进程内协议,这些协议会被加载到同一驱动程序主机中的其他驱动程序调用。
您可以在 device_add_args_t
中为设备传递一组协议操作。如果设备支持多种协议,请实现 get_protocol()
设备操作。设备只能有一个协议 ID。协议 ID 对应于设备在 devfs 中发布的类。
驱动程序操作
驱动程序通常通过处理来自子驱动程序或其他进程的客户端请求来运行。它可以通过直接与硬件通信(例如通过 MMIO)或与其父设备通信(例如将 USB 事务加入队列)来执行这些请求。
来自驱动程序主机外部进程的外部客户端请求由子驱动程序(通常位于同一进程中)通过与设备类对应的 banjo 协议来处理。驱动程序到驱动程序请求应使用 banjo 协议,而不是设备操作。
设备可以通过在其父级设备上调用 device_get_protocol()
来获取其父级支持的协议。
设备中断
设备中断由中断对象实现,中断对象是一种内核对象。驱动程序在设备协议方法中向其父设备请求设备中断的句柄。返回的句柄将绑定到父级驱动程序定义的设备的适当中断。例如,PCI 协议会为 PCI 子级实现 map_interrupt()
。驱动程序应生成一个线程来等待中断句柄。
内核会根据中断是边沿触发还是电平触发,自动处理中断屏蔽和取消屏蔽。对于电平触发的硬件中断,zx_interrupt_wait()
会在返回之前屏蔽中断,并在下次再次调用时取消屏蔽中断。对于边沿触发的 interrupt,interrupt 将保持未屏蔽状态。
中断线程不应执行任何长时间运行的任务。对于执行长时间任务的驱动程序,请使用工作器线程。
您可以在槽 ZX_INTERRUPT_SLOT_USER
上使用 zx_interrupt_trigger()
向中断句柄发出信号,以便从 zx_interrupt_wait()
返回。这对于在驱动程序清理期间关闭中断线程至关重要。
FIDL 消息
非驱动程序进程
每个设备类的消息均采用 FIDL 语言定义。每部设备都实现零个或多个 FIDL 协议,这些协议会通过每个客户端的单个通道进行多路复用。驱动程序有机会通过 message()
钩子解读 FIDL 消息。
其他进程中的驱动程序
如果驱动程序需要与单独进程中的驱动程序通信,而不是定义协议操作,则必须托管一个出站目录(类似于组件),该目录应托管子驱动程序在绑定时会访问的所有 FIDL 协议。
协议操作与 FIDL 消息
协议操作用于定义设备的进程内 API。FIDL 消息定义了用于进程间通信的 API。如果该函数要由同一进程中的其他驱动程序调用,请定义一个协议操作。驱动程序应对其父级调用协议操作,以便使用这些函数。
隔离设备
使用 DEVICE_ADD_MUST_ISOLATE
添加的设备会生成新的驱动程序主机。设备必须有一个随附的传出目录,用于托管 FIDL 协议。绑定到设备的驱动程序将加载到新的驱动程序主机中,并能够连接父驱动程序提供的传出目录中导出的 FIDL 协议。
驾驶员权利
虽然驱动程序在用户空间进程中运行,但其权限比普通进程更受限。驱动程序不得访问文件系统,包括 devfs。这意味着驱动程序无法与任意设备互动。如果您的驱动程序需要执行此操作,请考虑改为编写服务组件。例如,虚拟控制台由 virtcon 组件实现。
zx_vmo_create_contiguous()
和 zx_interrupt_create
等特权操作需要根资源句柄。除了系统驱动程序(在 x86 系统上为 ACPI,在 ARM 系统上为 platform)之外,其他驱动程序无法使用此句柄。设备应请求其父级设备为其执行此类操作。如果父级驱动程序的协议不适用于此用例,请与其作者联系。
同样,驱动程序也不得请求任意 MMIO 范围、中断或 GPIO。PCI 和平台等总线驱动程序仅返回与子设备关联的资源。
高级主题和提示
初始化需要很长时间
如果设备需要很长时间才能完成初始化,该怎么办?在上面讨论 null_bind()
函数时,我们曾提到,成功返回会告知驱动程序管理器驱动程序现在已与设备相关联。我们不能在绑定函数中花费太多时间;我们基本上需要初始化设备、发布设备,然后就完成了。
但您的设备可能需要执行耗时较长的初始化操作,例如:
- 枚举硬件点
- 加载固件
- 协商协议
等等,这可能需要很长时间。
您可以通过实现设备 init()
hook 将设备发布为“不可见”。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() |
安排移除某个设备及其所有子设备 |