blob: 5faaa297c74bd55e2713be9cf9ad33eb8a71b1bd [file] [log] [blame] [view] [edit]
# Fuchsia driver development
Caution: This page may contain information that is specific to the legacy
version of the driver framework (DFv1). Also the workflows documented on
this page may only be specific to the Fuchsia source checkout
(`fuchsia.git`) environment.
Fuchsia drivers are shared libraries that are dynamically loaded in driver host
processes in user space. The process of loading a driver is controlled by the
driver manager. See
[Device Model](/docs/development/drivers/concepts/device_driver_model/device-model.md)
for more information on driver hosts, driver manager and the driver and device
lifecycles.
## Directory structure
Drivers may be found throughout the source tree under `driver` subdirectories of
areas as specified in the
[source code layout](/docs/development/source_code/layout.md) document. Most
Fuchsia drivers are found under [//src/devices/](/src/devices). They are grouped
based on the protocols they implement. The driver protocols are defined in
[ddk/include/lib/ddk/protodefs.h](/src/lib/ddk/include/lib/ddk/protodefs.h). For
example, a USB ethernet driver goes in
[//src/connectivity/ethernet/drivers/](/src/connectivity/ethernet/drivers/)
rather than [//src/devices/usb/drivers/](/src/devices/usb/drivers) because it
implements an ethernet protocol. However, drivers that implement the USB stack
are in [//src/devices/usb/drivers/](/src/devices/usb/drivers) because they
implement USB protocols.
In the driver's `BUILD.gn`, there should be a `fuchsia_driver_component` target.
In order to get a driver to show up under `/boot/driver`, it should be included
as under the `board_bootfs_labels` list in the relevant board file(s) under
//boards. In order to get it to show up inside of `/system/driver` it should be
added to the system package with a `driver_package` build target, which should
then be referenced by relevant boardfile(s) under `//boards`. The driver manager
looks first in `/boot/driver`, then `/system/driver/` for loadable drivers.
## Creating a new driver
Creating a new driver can be done automatically by using the
[Create Tool](/tools/create/README.md). Simply run the following command:
```
fx create driver --path <PATH> --lang cpp
```
This will create the directory `<PATH>` containing an empty driver where the
last segment of `<PATH>` is the driver name and GN target name. After this
command is run, the following steps need to be followed:
1. Include the `fuchsia_driver_component` or `driver_package` build target in
the correct place to get your driver included into the system.
2. For packaged drivers the `driver_package` build target should be added to
the relevant board file in `//boards` or `//vendor/<foo>/boards` to a
`xx_package_labels` GN argument.
3. For boot drivers the `fuchsia_driver_component` build target should be added
to the relevant board file in `//boards` or `//vendor/<foo>/boards` to the
`board_bootfs_labels` GN argument.
4. Include the `tests` build target in the `<PATH>:tests` build target to get
your tests included in CQ.
5. Add proper bind rules in `<NAME>.bind`.
6. Add driver information in `<NAME>-info.json`. The file must include a
`short_description` and `areas` matching at least one of the areas listed at
`//build/drivers/areas.txt`.
7. Write the functionality for the driver.
## Declaring a driver
At a minimum, a driver should contain the driver declaration and implement the
`bind()` driver op.
Drivers are loaded and bound to a device when the driver manager successfully
finds a matching driver for a device. A driver declares the devices it is
compatible with through bind rules, which should be placed in a `.bind` file
alongside the driver. The bind compiler compiles those rules and creates a
driver declaration macro containing those rules in a C header file. The
following bind rules declares the
[AHCI driver](/src/devices/block/drivers/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;
```
These bind rules state that the driver binds to devices with a `BIND_PROTOCOL`
property that matches `DEVICE` from the `pci` namespace and the given PCI
class/subclass/interface. The `pci` namespace is imported from the `fucnsia.pci`
library on the first line. For more details, refer to the [binding
documentation](/docs/development/drivers/concepts/device_driver_model/driver-binding.md).
To generate a driver declaration macro including these bind rules, there should
be a corresponding `bind_rules` build target. This should declare dependencies
corresponding to the "using" statements in the bind file.
```
driver_bind_rules("bind") {
rules = "ahci.bind"
bind_output = "ahci.bindbc"
deps = [
"//src/devices/bind/fuchsia.pci",
"//src/devices/bind/fuchsia.pci.massstorage",
]
}
```
The driver can now include the generated header and declare itself with the
following macro. `"zircon"` is the vendor id and `"0.1"` is the driver version.
```c
#include <lib/ddk/binding_driver.h>
...
ZIRCON_DRIVER(ahci, ahci_driver_ops, "zircon", "0.1");
```
The [PCI driver](/src/devices/bus/drivers/pci/kpci.cc) publishes the matching
device with the following properties:
```c
zx_device_prop_t device_props[] = {
{BIND_PROTOCOL, 0, ZX_PROTOCOL_PCI},
{BIND_PCI_VID, 0, info.vendor_id},
{BIND_PCI_DID, 0, info.device_id},
{BIND_PCI_CLASS, 0, info.base_class},
{BIND_PCI_SUBCLASS, 0, info.sub_class},
{BIND_PCI_INTERFACE, 0, info.program_interface},
{BIND_PCI_REVISION, 0, info.revision_id},
{BIND_PCI_BDF_ADDR, 0, BIND_PCI_BDF_PACK(info.bus_id, info.dev_id,
info.func_id)},
};
```
For now, binding variables and macros are defined in
[lib/ddk/binding.h](/src/lib/ddk/include/lib/ddk/binding.h). In the near future,
all node properties will be defined by bind libraries like the `fuchsia.pci`
library imported above. If you are introducing a new device class, you may need
to introduce new node properties to the binding header as well as the
[bind libraries](/src/devices/bind/).
Node properties are 32-bit values. If your variable value requires greater than
a 32-bit value, split them into multiple 32-bit variables. An example is ACPI
HID values, which are 8 characters (64-bits) long. It is split into
`BIND_ACPI_HID_0_3` and `BIND_ACPI_HID_4_7`. Once the migration to bind
libraries is complete you will be able to use other data types such as strings,
larger numbers, and booleans.
You may specify `disable_autobind = true` in the `bind_rules` build rule to
disable the automatic binding behaviour. In that case, a driver can be bound to
a device using `fuchsia.device.Controller/Bind` FIDL call.
## Driver binding
A driver's `bind()` function is called when it is matched to a device. Generally
a driver will initialize any data structures needed for the device and
initialize hardware in this function. It should not perform any time-consuming
tasks or block in this function, because it is invoked from the driver host's
RPC thread and it will not be able to service other requests in the meantime.
Instead, it should spawn a new thread to perform lengthy tasks.
The driver should make no assumptions about the state of the hardware in
`bind()`. It may need to reset the hardware or otherwise ensure it is in a known
state. Because the system recovers from a driver crash by re-spawning the driver
host, the hardware may be in an unknown state when `bind()` is invoked.
There are generally four outcomes from `bind()`:
1. The driver determines the device is supported and does not need to do any
heavy lifting, so publishes a new device with `device_add()` in C or
`ddk::Device::DdkAdd()` in the
[DDKTL](/docs/development/drivers/concepts/driver_development/using-ddktl.md)
C++ wrapper library and returns `ZX_OK.
2. The driver determines that even though the bind rules matched, the device
cannot be supported (maybe due to checking hw version bits or whatnot) and
returns an error.
3. The driver needs to do further initialization before the device is ready or
it's sure it can support it, so it publishes a invisible device that
implements the [`init()`](/src/lib/ddk/include/lib/ddk/device.h) hook and
kicks off a thread to keep working, while returning `ZX_OK` to `bind()`.
That thread will eventually call
[`device_init_reply()`](/src/lib/ddk/include/lib/ddk/driver.h) in C or
`ddk::InitTxn::Reply()` in the
[DDKTL](/docs/development/drivers/concepts/driver_development/using-ddktl.md)
C++ wrapper library. The device is guaranteed not to be removed until the
reply is received. The status indicates `ZX_OK` if it was able to
successfully initialize the device and it should be made visible, or an
error to indicate that the device should be removed.
4. The driver represents a bus or controller with 0..n children that may
dynamically appear or disappear. In this case it should publish a device
immediately representing the bus or controller, and then dynamically publish
children (that downstream drivers will bind to) representing hardware on
that bus. Examples: AHCI/SATA, USB, etc.
After a device is added and made visible by the system, it is made available to
client processes and for binding by compatible drivers.
## Banjo protocols
A driver provides a set of device ops and optional protocol ops to a device.
Device ops implement the device lifecycle methods and the external interface to
the device that are called by other user space applications and services.
Protocol ops implement the in-process protocols of the device that are called by
other drivers loaded into the same driver host.
You can pass one set of protocol ops for the device in `device_add_args_t`. If a
device supports multiple protocols, implement the `get_protocol()` device op. A
device can only have one protocol id. The protocol id corresponds to the class
the device is published under in devfs.
## Driver operation
A driver generally operates by servicing client requests from children drivers
or other processes. It fulfills those requests either by communicating directly
with hardware (for example, through MMIO) or by communicating with its parent
device (for example, queueing a USB transaction).
External client requests from processes outside the driver host are fulfilled by
children drivers, generally in the same process, are fulfilled by banjo
protocols corresponding to the device class. Driver-to-driver requests should
use banjo protocols instead of device ops.
A device can get a protocol supported by its parent by calling
`device_get_protocol()` on its parent device.
## Device interrupts
Device interrupts are implemented by interrupt objects, which are a type of
kernel objects. A driver requests a handle to the device interrupt from its
parent device in a device protocol method. The handle returned will be bound to
the appropriate interrupt for the device, as defined by a parent driver. For
example, the PCI protocol implements `map_interrupt()` for PCI children. A
driver should spawn a thread to wait on the interrupt handle.
The kernel will automatically handle masking and unmasking the interrupt as
appropriate, depending on whether the interrupt is edge-triggered or
level-triggered. For level-triggered hardware interrupts,
[`zx_interrupt_wait()`](/docs/reference/syscalls/interrupt_wait.md) will mask
the interrupt before returning and unmask the interrupt when it is called again
the next time. For edge-triggered interrupts, the interrupt remains unmasked.
The interrupt thread should not perform any long-running tasks. For drivers that
perform lengthy tasks, use a worker thread.
You can signal an interrupt handle with
[`zx_interrupt_trigger()`](/docs/reference/syscalls/interrupt_trigger.md) on
slot `ZX_INTERRUPT_SLOT_USER` to return from `zx_interrupt_wait()`. This is
necessary to shut down the interrupt thread during driver clean up.
## FIDL messages
## Non-driver processes
Messages for each device class are defined in the
[FIDL](/docs/development/languages/fidl/README.md) language. Each device
implements zero or more FIDL protocols, multiplexed over a single channel per
client. The driver is given the opportunity to interpret FIDL messages through
the `message()` hook. These are only accessible to non-driver components by
means of devfs.
## Drivers in other processes
If a driver needs to communicate with a driver in a separate process, rather
than define protocol ops, it must instead host an outgoing directory, similar to
components, which should host all FIDL protocols the child driver would access
on bind.
## Protocol ops vs. FIDL messages
Protocol ops define the in-process API for a device. FIDL messages define the
API for communicating out of process. Define a protocol op if the function is
meant to be called by other drivers in the same process. A driver should call a
protocol op on its parent to make use of those functions.
## Isolate devices
Devices that are added with `DEVICE_ADD_MUST_ISOLATE` spawn a new driver host.
The device must have an accompanying outgoing directory which hosts FIDL
protocols. A driver which is bound to the device will be loaded into the new
driver host and provided the ability to connect FIDL protocols exported in the
outgoing directory provided by the parent driver.
## Driver rights
Although drivers run in user space processes, they have a more restricted set of
rights than normal processes. Drivers are not allowed to access the filesystem,
including devfs. That means a driver cannot interact with arbitrary devices. If
your driver needs to do this, consider writing a service component instead. For
example, the virtual console is implemented by the
[virtcon](/src/bringup/bin/virtcon) component.
Privileged operations such as `zx_vmo_create_contiguous()` and
[`zx_interrupt_create`](/docs/reference/syscalls/interrupt_create.md) require a
root resource handle. This handle is not available to drivers other than the
system driver ([ACPI](/src/devices/board/drivers/x86) on x86 systems and
[platform](/src/devices/bus/drivers/platform) on ARM systems). A device should
request its parent to perform such operations for it. Contact the author of the
parent driver if its protocol does not address this use case.
Similarly, a driver is not allowed to request arbitrary MMIO ranges, interrupts
or GPIOs. Bus drivers such as PCI and platform only return the resources
associated to the child device.
## Advanced Topics and Tips
### Taking a long time to initialize
What if your device takes a long time to initialize? When we discussed the
`null_bind()` function above, we indicated that a successful return told the
driver manager that the driver is now associated with the device. We can't spend
a lot of time in the bind function; we're basically expected to initialize our
device, publish it, and be done.
But your device might need to perform a lengthy initialization operation, such
as:
* enumerate hardware points
* load firmware
* negotiate a protocol
and so on, which might take a long time to do.
You can publish your device as "invisible" by implementing the device `init()`
hook. The `init()` hook is run after the device is added through `device_add()`,
and may be used to safely access the device state and to spawn a worker thread.
The device will remain invisible and is guaranteed not to be removed until
`device_init_reply()` is called, which may be done from any thread. This meets
the requirements for the binding function, but nobody is able to use your device
(because nobody knows about it yet, because it's not visible). Now your device
can perform the long operations with a background thread.
When your device is ready to service client requests, call `device_init_reply()`
which will cause it to appear in the pathname space.
#### Power savings
Two callouts, `suspend()` and `resume()`, are available for your device in order
to support power or other resource saving features.
Both take a device context pointer and a flags argument, but the flags argument
is used only in the suspend case.
Flag | Meaning
--------------------------------------- | -------
`DEVICE_SUSPEND_FLAG_REBOOT` | The driver should shut itself down in preparation for a reboot or shutdown of the machine
`DEVICE_SUSPEND_FLAG_REBOOT_BOOTLOADER` | ?
`DEVICE_SUSPEND_FLAG_REBOOT_RECOVERY` | ?
`DEVICE_SUSPEND_FLAG_POWEROFF` | The driver should shut itself down in preparation for power off
`DEVICE_SUSPEND_FLAG_MEXEC` | The driver should shut itself down in preparation for a [soft reboot](/docs/reference/syscalls/system_mexec.md)
`DEVICE_SUSPEND_FLAG_SUSPEND_RAM` | The driver should arrange so that it can be restarted from RAM
<!---
Yeah, I'm just guessing on the flags; they're used so little...
For documentation purposes, what should I write? That they are just hints, or
that you *must* do something because of a given flag, or ... ?
-->
## Reference: Support functions
This section lists support functions that are provided for your driver to use.
### Accessor functions
The context block that's passed as the first argument to your driver's protocol
functions is an opaque data structure. This means that in order to access the
data elements, you need to call an accessor function:
Function | Purpose
------------------- | --------------------------------
`device_get_name()` | Retrieves the name of the device
### Administrative functions
The following functions are used to administer the device:
Function | Purpose
----------------------- | ------------------------------------------------------
`device_add()` | Adds a device to a parent
`device_async_remove()` | Schedules the removal of a device and all its children
<!---
@@@ Notes only @@@
This section is great for things like talking about buffer management,
threading, best practices, advanced options for device_add(), and so on.
I think it can be somewhere between the man page ("printf is used to print a string
and takes the following parameters") and an application note &mdash; I want to see
examples of how to use the functions, what the arguments mean, what the impact of
various design decisions is, that kind of thing.
-->