C++ 驱动程序评分标准

编写基本驱动程序

代码结构应遵循 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_bytecode
  • fuchsia_cc_driver
  • fuchsia_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 服务:

以下库适用于常见的内部 Zircon 对象、内存区域或驱动程序运行时资源:

集成测试

集成测试应使用 驱动程序测试领域 (sdk/lib/driver_test_realm/) 框架。首先,测试必须建立与 DriverTestRealm 服务的连接,在 RealmArgs 中执行必要的配置,然后使用实参调用 Start()。