Writing a basic driver
The code structure should follow the fx create driver goldens
template.
Meta directory
Every driver directory is required to have a meta subdirectory containing
these files:
- A
bindfile defining the driver'sbindrules - A component manifest
Driver source code
Write drivers using the Driver Component library
(sdk/lib/driver/component/). Drivers should inherit from the
fdf::DriverBase2 class defined in the header
<lib/driver/component/cpp/driver_base2.h>.
The driver provides the FUCHSIA_DRIVER_EXPORT2 macro used to export the
driver symbol which is defined in the header
<lib/driver/component/cpp/driver_export2.h>. This macro should be located
in a .cc file.
Initializing a driver
Avoid placing any logic within the driver's constructor. Rather than using a
constructor, you must implement the driver’s initialization logic by overriding
exactly one of the Start() methods provided by
fdf::DriverBase2, even if the initialization logic is empty.
Ensure that only a single Start() variant is used for this purpose.
The initialization logic contains:
- Fetching and configuring all driver resources.
- Establishing service connections.
- Adding the driver's own service to the outgoing directory.
- Add child nodes (to be performed after resource setup and providing its own service).
- Release BTI from quarantine with
zx_bti_release_quarantine().
Shutting down a driver
The driver's destructor should remain free of any logic. In the majority of
scenarios, overriding and providing implementation for DriverBase2::Stop() is
unnecessary. Refrain from implementing DriverBase2::Stop() unless it is
specifically required for your driver's functionality, such as performing
graceful hardware shutdowns or unpinning DMA. The fdf::StopCompleter in the
function parameter must be called at the end of the function.
Build file
All new drivers must use Bazel for the build process and must include the following targets:
fuchsia_driver_bind_bytecodefuchsia_cc_driverfuchsia_driver_component
Driver communication
Drivers communicate with their parent drivers through FIDL services.
Serving a service
Drivers serving FIDL services are required to maintain a
fidl::ServerBindingGroup (when using Zircon transport) or an
fdf::ServerBindingGroup (for the driver transport). Unless the driver must
be restricted to a single client connection, avoid using fidl::ServerBinding
or fdf::ServerBinding.
The server bindings must be added to the driver’s outgoing directory within the
Start() function before any child nodes are created. Furthermore, the CML
file must specify the service in the capability and expose it from self.
Using a service
To use a service, drivers should include it in their bind rules and specify
it within the uses section of the CML.
Adding a child
The primary reason for adding a child node is if the driver needs to provide
services or resources for another driver. Avoid adding a child without reason.
Unless the driver needs to add child nodes dynamically, the child node should
be added as part of the initialization logic in the driver’s Start()
function.
Child nodes should be added using DriverBase2's AddChild() or the
add_child helper library (sdk/lib/driver/node/cpp/add_child.h).
The helper library should only be used if the child node needs to use a
different logger.
All child nodes should be unowned unless it’s being used to support devfs,
which is deprecated. If the child node is owned, then the driver must store the
fuchsia_driver_framework::Node client end that it receives from the helper
functions.
When adding a child node, explicitly define the child node’s name instead of
using name() from DriverBase2. Create the offers with the
node_offers helper library
(sdk/lib/driver/component/cpp/node_offers.h). Create the properties using the
node_properties library
(sdk/lib/driver/component/cpp/node_properties.h). The node property key and
values should use the generated bind library code bindings.
Logging
For logging, drivers should use format-based log APIs like fdf::info() found
in the driver logger library
(sdk/lib/driver/logging/cpp/logger.h). If you need to log a custom type,
implement the std::format.
Follow the Fuchsia logging guidelines by using warning
or error log levels when documenting failures such as FIDL errors.
Error handling
Return zx::result<T> from fallible functions instead of void
or a zx_status_t plus an out-parameter, so the caller can handle the error.
Use fit::result<E, T> for your own error types. Mark functions whose
signature must return zx_status_t (C ABI, legacy interfaces) [[nodiscard]].
Never report success after a failure. Pass errors to completers that accept
them (such as fdf::StartCompleter) with take_error() or
zx::make_result(status). If the completer cannot report an error
(fdf_power::ResumeCompleter), leave the driver's state showing the failure.
Log or comment any place that continues after a failure on purpose.
Threading and synchronization
Know which dispatcher runs the code before adding a lock. State accessed only
from one synchronized dispatcher, such as the default one
from Start(), needs no mutex. State accessed from an unsynchronized
dispatcher, from several dispatchers, or from a thread the driver runtime does
not manage (a dedicated interrupt thread, std::thread) does. Document this
where the state is declared, or check fdf::Dispatcher::GetCurrent().
Do not hold a lock while calling anything that can re-enter the driver or wait on the same lock: update the state, release the lock, then make the call. Comment any lock held across a call on purpose. Do not release and re-acquire the lock around each call, or add a getter that takes the lock and returns a copy; both cause TOCTOU races.
Zircon resources
Interrupts
Interrupts should be retrieved and stored in the driver during initialization.
The interrupts should be wrapped by an async::Irq object
(sdk/lib/async/include/lib/async/cpp/irq.h).
An IrqMethod object should be used to handle interrupt triggers. Once the
interrupt trigger is handled, the Irq object should be acknowledged with
ack() so the interrupt is re-armed and can be triggered again.
DMA
For memory operations, drivers should use MmioBuffer, located at
sdk/lib/driver/mmio/cpp/mmio-buffer.h. This library provides a wrapper around
the raw mmio_block_t object for reading and writing.
To ensure safe access to register bitfields, it is recommended to use the
hwreg/bitfields library. This library is found at
zircon/system/ulib/hwreg/include/hwreg/bitfields.h.
hwreg setters do not clamp values to the field width: they ZX_DEBUG_ASSERT
in debug builds and overwrite neighboring fields in release builds. Check
lengths, offsets, indices, and counts from outside the driver (FIDL clients,
bus hosts, metadata, firmware), and values computed from them, before using
them in a register, a DMA transfer size, or a buffer index. Where the value
enters the driver, return ZX_ERR_INVALID_ARGS, or clamp if the protocol
allows a maximum value; compute bounds so they cannot overflow; confirm that a
client-supplied ID was registered by that client
(VMO registration). A comment naming what already
guarantees the range can replace a check; see
Precondition checks for when to assert instead.
The driver should take control of its BTI and stop any DMA which might be
ongoing during initialization. Once that is complete, it should tell the BTI
that it has regained control of the hardware by calling
zx_bti_release_quarantine() on it. Drivers that intend to share DMA with
hardware are required to pin the BTI beforehand. After the hardware has
finished accessing the memory, the driver is responsible for unpinning it.
Under no circumstances should the BTI be unpinned within the destructor.
Atomics (std::memory_order_release) order memory accesses between CPUs, not
accesses by a device. Before notifying the device of a DMA descriptor (a
doorbell register write, an ownership bit, an index update), know whether the
buffer is cached, and, at the notification, comment how the descriptor write
is ordered before it. Barriers order accesses;
cache operations make data visible to the device; one
does not replace the other. Do not add a barrier just in case: comment every
explicit barrier with the two accesses it orders.
Unpin each zx::pmt when a transfer completes, fails, or is canceled, and at
shutdown, but only after the device has stopped; if you cannot confirm that
(for example, a disable timed out), leave it pinned. Destroying a zx::pmt
without calling unpin() quarantines the pages until
zx_bti_release_quarantine().
Clocks
Clocks are controlled via the fuchsia.hardware.clock FIDL service.
Drivers are required to call Enable() on all clocks they depend on and
subsequently call Disable() once the clock signal is no longer needed.
Drivers must not call Disable() without first enabling the clock.
Testing
Verify that build targets include the necessary driver tests.
Unit tests
Unit tests should be written using gtests
(third_party/googletest/src/googletest/include/gtest/gtest.h).
Testing the entire driver
If the entire driver is being tested, the driver should be wrapped by
ForegroundDriverTest or BackgroundDriverTest from the
driver test library
(sdk/lib/driver/testing/cpp/driver_test.h). The unit test should call
StartDriver() right after the test is initialized and call StopDriver() in
the Teardown() function. All services and resources needed by the driver
should be initialized and served in the custom driver test’s Environment
class.
If the driver requires a platform device service, the test should instantiate a
FakePlatformDevice
(sdk/lib/driver/fake-platform-device/) DriverTestEnvironment and serve it.
Similarly, if the driver requires a FIDL service, a test implementation of the
service should be instantiated and served in the DriverTestEnvironment.
Testing parts of the driver
Tests may require specific driver environment components if only a subset of the driver's logic is being isolated for testing.
When evaluating logic that utilizes the driver logger library
(sdk/lib/driver/logging/cpp/logger.h), the test is required to instantiate
and maintain a fdf_testing::ScopedGlobalLogger instance
(sdk/lib/driver/testing/cpp/scoped_global_logger.h).
Furthermore, if a driver dispatcher is necessary for the logic, the test must
configure and hold a fdf_testing::DriverRuntime object
(sdk/lib/driver/testing/cpp/driver_runtime.h).
Fakes/mocks libraries
Tests requiring fake FIDL services or resources should utilize the fake and
mock libraries provided in sdk/lib/driver/ whenever they are available.
The following libraries are available for common FIDL services:
- //sdk/lib/driver/fake-clock -
fuchsia.hardware.clockservice. - //sdk/lib/driver/fake-gpio -
fuchsia.hardware.gpioservice. - //sdk/lib/driver/fake-interconnect -
fuchsia.hardware.interconnectservice. - //sdk/lib/driver/fake-pin -
fuchsia.hardware.pinservice. - //sdk/lib/driver/fake-platform-device -
fuchsia.hardware.platform.deviceandfuchsia.hardware.powerservices. - //sdk/lib/driver/fake-reset -
fuchsia.hardware.resetservice. - //sdk/lib/driver/fake-vreg -
fuchsia.hardware.vregservice.
The following libraries are available for common internal Zircon objects, memory regions, or driver-runtime resources:
- //sdk/lib/driver/fake-bti: Fakes Zircon BTI (
zx::bti) objects. - //sdk/lib/driver/fake-object: Fakes general Zircon kernel
objects (
zx::handle). - //sdk/lib/driver/fake-resource: Fakes Zircon resource
(
zx::resource) objects. - //sdk/lib/driver/fake-mmio-reg: Fakes MMIO registers via memory structures.
- //sdk/lib/driver/mock-mmio: Mocks MMIO regions for memory-mapped I/O register testing.
Integration tests
Integration tests should use the Driver Test Realm
(sdk/lib/driver_test_realm/) framework. To begin, the test must establish a
connection to the DriverTestRealm service, perform necessary configuration in
RealmArgs, and then call Start() with the arguments.