编写基本驱动程序
代码结构应遵循 fx create driver goldens
模板。
元目录
每个驱动程序目录都必须有一个 meta 子目录,其中包含以下文件:
- 一个
bind文件,用于定义驱动程序的bind规则 - 一个组件清单
驱动程序源代码
使用 驱动程序组件库
(sdk/lib/driver/component/) 编写驱动程序。驱动程序应继承自
fdf::DriverBase2 类,该类在头文件
<lib/driver/component/cpp/driver_base2.h> 中定义。
驱动程序提供 FUCHSIA_DRIVER_EXPORT2 宏,用于导出
头文件
<lib/driver/component/cpp/driver_export2.h> 中定义的驱动程序符号。此宏应位于
.cc 文件中。
初始化驱动程序
避免在驱动程序的构造函数中放置任何逻辑。您必须通过替换
fdf::DriverBase2提供的Start()方法中的一个(且只能替换一个),来实现驱动程序的初始化逻辑,而不是使用
构造函数,即使初始化逻辑为空也是如此。
确保仅使用一个 Start() 变体来实现此目的。
初始化逻辑包含:
- 提取和配置所有驱动程序资源。
- 建立服务连接。
- 将驱动程序自己的服务添加到出站目录。
- 添加子节点(在资源设置和提供自己的服务后执行)。
- 使用
zx_bti_release_quarantine()将 BTI 从隔离区释放。
关闭驱动程序
驱动程序的析构函数应不包含任何逻辑。在大多数情况下,无需替换 DriverBase2::Stop() 并为其提供实现。除非您的驱动程序的功能需要(例如执行正常硬件关闭或取消固定 DMA),否则请勿实现 DriverBase2::Stop()。必须在函数末尾调用函数形参中的 fdf::StopCompleter。
build 文件
所有新驱动程序都必须使用 Bazel 进行 build 过程,并且必须包含以下目标:
fuchsia_driver_bind_bytecodefuchsia_cc_driverfuchsia_driver_component
驱动程序通信
驱动程序通过 FIDL 服务与其父级驱动程序通信。
提供服务
提供 FIDL 服务的驱动程序必须维护 fidl::ServerBindingGroup(使用 Zircon 传输时)或 fdf::ServerBindingGroup(对于驱动程序传输)。除非驱动程序必须限制为单个客户端连接,否则请避免使用 fidl::ServerBinding 或 fdf::ServerBinding。
必须在创建任何子节点之前,将服务器绑定添加到 Start() 函数中的驱动程序的出站目录。此外,CML 文件必须在功能中指定服务,并从 self 中公开该服务。
使用服务
如需使用服务,驱动程序应将其包含在 bind 规则中,并在 CML 的 uses 部分中指定该服务。
添加子级
添加子节点的主要原因是,驱动程序需要为另一个驱动程序提供服务或资源。避免无故添加子级。
除非驱动程序需要动态添加子节点,否则应将子节点作为驱动程序的 Start() 函数中的初始化逻辑的一部分添加。
应使用 DriverBase2's AddChild() 或
add_child 帮助程序库 (sdk/lib/driver/node/cpp/add_child.h) 添加子节点。
只有当子节点需要使用不同的记录器时,才应使用帮助程序库。
所有子节点都应为无所有权,除非它用于支持已废弃的 devfs。如果子节点有所有权,则驱动程序必须存储从帮助程序函数接收的 fuchsia_driver_framework::Node 客户端端点。
添加子节点时,请明确定义子节点的名称,而不是使用 DriverBase2 中的 name()。使用
node_offers 帮助程序库
(sdk/lib/driver/component/cpp/node_offers.h) 创建 offer。使用
node_properties 库
(sdk/lib/driver/component/cpp/node_properties.h) 创建属性。节点属性键和
值应使用生成的绑定库代码绑定。
日志记录
对于日志记录,驱动程序应使用基于格式的日志 API,例如 fdf::info() found
in the driver logger library
(sdk/lib/driver/logging/cpp/logger.h)。如果您需要记录自定义类型,
implement the std::format。
记录 FIDL 错误等失败时,请使用 warning
或 error 日志级别,遵循 Fuchsia 日志记录准则。
错误处理
从易出错的函数返回 zx::result<T>,而不是 void
或 zx_status_t 加输出形参,以便调用方可以处理错误。
对您自己的错误类型使用 fit::result<E, T>。将签名必须返回 zx_status_t(C ABI、旧版接口)的函数标记为 [[nodiscard]]。
切勿在失败后报告成功。使用 take_error() 或 zx::make_result(status) 将错误传递给接受错误的完成程序(例如 fdf::StartCompleter)。如果完成程序无法报告错误 (fdf_power::ResumeCompleter),请将驱动程序的状态保留为显示失败。
记录或注释任何在失败后继续执行的地方。
线程处理和同步
在添加锁之前,请了解哪个调度程序运行代码。仅从一个 同步调度程序(例如 Start() 中的默认调度程序)访问的状态不需要互斥锁。从不同步的调度程序、多个调度程序或驱动程序运行时不管理的线程(专用中断线程、std::thread)访问的状态需要互斥锁。在声明状态的位置记录此信息,或检查 fdf::Dispatcher::GetCurrent()。
在调用任何可以重新进入驱动程序或等待同一锁的内容时,请勿持有锁:更新状态,释放锁,然后进行调用。 注释任何有意在调用中持有的锁。请勿在每次调用前后释放并重新获取锁,或添加获取锁并返回副本的 getter;这两种方法都会导致 TOCTOU 竞争。
Zircon 资源
中断
应在初始化期间检索中断并将其存储在驱动程序中。
中断应由 async::Irq 对象 (sdk/lib/async/include/lib/async/cpp/irq.h) 封装。
应使用 IrqMethod 对象来处理中断触发器。处理中断触发器后,应使用 ack() 确认 Irq 对象,以便重新启用中断并可以再次触发。
DMA
对于内存操作,驱动程序应使用 MmioBuffer,该库位于
sdk/lib/driver/mmio/cpp/mmio-buffer.h。此库提供对原始 mmio_block_t 对象的封装,用于读取和写入。
为确保安全访问寄存器位字段,建议使用
hwreg/bitfields 库。此库位于 zircon/system/ulib/hwreg/include/hwreg/bitfields.h。
hwreg setter 不会将值限制为字段宽度:它们在调试 build 中为 ZX_DEBUG_ASSERT,并在发布 build 中覆盖相邻字段。在寄存器、DMA 传输大小或缓冲区索引中使用长度、偏移量、索引和计数(来自驱动程序外部(FIDL 客户端、总线主机、元数据、固件)以及从中计算出的值)之前,请先检查这些值。如果值
进入驱动程序,则返回 ZX_ERR_INVALID_ARGS,或者如果协议
允许最大值,则进行限制;计算边界,使其不会溢出;确认客户端提供的 ID 已由该客户端注册
(VMO 注册)。注释(命名已保证范围的内容)可以替换检查;如需了解何时断言,请参阅
前提条件检查。
驱动程序应控制其 BTI,并在初始化期间停止任何可能正在进行的 DMA。完成后,它应通过对其调用 zx_bti_release_quarantine() 来告知 BTI 它已重新获得对硬件的控制权。打算与硬件共享 DMA 的驱动程序需要提前固定 BTI。硬件完成对内存的访问后,驱动程序负责取消固定内存。
在任何情况下,都不应在析构函数中取消固定 BTI。
原子操作 (std::memory_order_release) 对 CPU 之间的内存访问进行排序,而不是对设备的访问进行排序。在将 DMA 描述符(门铃寄存器写入、所有权位、索引更新)通知设备之前,请了解缓冲区是否已缓存,并在通知时注释描述符写入之前的排序方式。屏障对访问进行排序;
缓存操作使数据对设备可见;一
者不能相互替代。请勿添加屏障以防万一:使用它排序的两个访问来注释每个显式屏障。
当传输完成、失败或取消时,以及在关闭时,取消固定每个 zx::pmt,但仅在设备停止后才执行此操作;如果您无法确认(例如,停用超时),请将其保留为固定状态。销毁 zx::pmt 而不调用 unpin() 会将页面隔离,直到 zx_bti_release_quarantine()。
时钟
时钟通过 fuchsia.hardware.clock FIDL 服务进行控制。
驱动程序必须对其依赖的所有时钟调用 Enable(),然后在不再需要时钟信号时调用 Disable()。
驱动程序不得在未先启用时钟的情况下调用 Disable()。
测试
验证 build 目标是否包含必要的驱动程序测试。
单元测试
应使用 gtests
(third_party/googletest/src/googletest/include/gtest/gtest.h) 编写单元测试。
测试整个驱动程序
如果正在测试整个驱动程序,则驱动程序应由
ForegroundDriverTest 或 BackgroundDriverTest 从
驱动程序测试库
(sdk/lib/driver/testing/cpp/driver_test.h) 封装。单元测试应在测试初始化后立即调用
StartDriver(),并在
Teardown() 函数中调用 StopDriver()。驱动程序所需的所有服务和资源都应在自定义驱动程序测试的 Environment 类中进行初始化和提供。
如果驱动程序需要平台设备服务,则测试应实例化
FakePlatformDevice
(sdk/lib/driver/fake-platform-device/) DriverTestEnvironment 并提供该服务。
同样,如果驱动程序需要 FIDL 服务,则应在 DriverTestEnvironment 中实例化并提供该服务的测试实现。
测试驱动程序的各个部分
如果仅隔离驱动程序逻辑的子集进行测试,则测试可能需要特定的驱动程序环境组件。
评估使用驱动程序记录器库
(sdk/lib/driver/logging/cpp/logger.h) 的逻辑时,测试需要实例化
并维护 fdf_testing::ScopedGlobalLogger 实例
(sdk/lib/driver/testing/cpp/scoped_global_logger.h)。
此外,如果逻辑需要驱动程序调度程序,则测试必须配置并持有 fdf_testing::DriverRuntime 对象 (sdk/lib/driver/testing/cpp/driver_runtime.h)。
伪造/模拟库
需要伪造 FIDL 服务或资源的测试应尽可能使用 sdk/lib/driver/ 中提供的伪造和模拟库。
以下库适用于常见的 FIDL 服务:
- //sdk/lib/driver/fake-clock -
fuchsia.hardware.clock服务。 - //sdk/lib/driver/fake-gpio -
fuchsia.hardware.gpio服务。 - //sdk/lib/driver/fake-interconnect -
fuchsia.hardware.interconnect服务。 - //sdk/lib/driver/fake-pin -
fuchsia.hardware.pin服务。 - //sdk/lib/driver/fake-platform-device -
fuchsia.hardware.platform.device和fuchsia.hardware.power服务。 - //sdk/lib/driver/fake-reset -
fuchsia.hardware.reset服务。 - //sdk/lib/driver/fake-vreg -
fuchsia.hardware.vreg服务。
以下库适用于常见的内部 Zircon 对象、内存区域或驱动程序运行时资源:
- //sdk/lib/driver/fake-bti:伪造 Zircon BTI (
zx::bti) 对象。 - //sdk/lib/driver/fake-object:伪造常规 Zircon 内核
对象 (
zx::handle)。 - //sdk/lib/driver/fake-resource:伪造 Zircon 资源
(
zx::resource) 对象。 - //sdk/lib/driver/fake-mmio-reg:通过 内存结构伪造 MMIO 寄存器。
- //sdk/lib/driver/mock-mmio:模拟 MMIO 区域以进行 内存映射 I/O 寄存器测试。
集成测试
集成测试应使用 驱动程序测试领域
(sdk/lib/driver_test_realm/) 框架。首先,测试必须建立与 DriverTestRealm 服务的连接,在 RealmArgs 中执行必要的配置,然后使用实参调用 Start()。