blob: 0343eba7ab75ce6a40a839b6cafa4f1300c613f5 [file] [log] [blame] [view]
# Zircon Driver Development
Zircon drivers are shared libraries that are dynamically loaded in Device Host
processes in user space. The process of loading a driver is controlled by the
Device Coordinator. See [Device Model](device-model.md) for more information on
Device Hosts, Device Coordinator and the driver and device lifecycles.
## Directory structure
Zircon drivers are found under [system/dev](/zircon/system/dev).
They are grouped based on the protocols they implement.
The driver protocols are defined in
[ddk/include/ddk/protodefs.h](/zircon/system/ulib/ddk/include/ddk/protodefs.h).
For example, a USB ethernet driver goes in
[system/dev/ethernet](/src/connectivity/ethernet/drivers/)
rather than [system/dev/usb](/src/devices/usb/drivers) because it implements an ethernet protocol.
However, drivers that implement the USB stack are in [system/dev/usb](/src/devices/usb/drivers)
because they implement USB protocols.
In the driver's `BUILD.gn`, there should be a `zx_driver` target (for drivers in
the ZN build) or a `driver_module` target (for drivers in the GN build.) The
former will install the driver shared library in `/boot/driver/` and the latter
in `/system/driver/`. The device coordinator looks first in `/boot/driver/`,
then `/system/driver/` for loadable drivers.
## 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 Device Coordinator
successfully finds a matching driver for a device. A driver declares the
devices it is compatible with through bind rules, which are 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 program declares the [AHCI driver](/src/devices/block/drivers/ahci/ahci.h):
```
using deprecated.pci;
deprecated.BIND_PROTOCOL == deprecated.pci.BIND_PROTOCOL.DEVICE;
deprecated.BIND_PCI_CLASS == 0x01;
deprecated.BIND_PCI_SUBCLASS == 0x06;
deprecated.BIND_PCI_INTERFACE == 0x01;
```
These bind rules state that the driver binds to devices with a `BIND_PROTOCOL`
property that matches `DEVICE` from the `pci` namespace and with PCI class 1,
subclass 6, interface 1. The `pci` namespace is imported from the
`deprecated.pci` library on the first line. For more details, refer to the
[binding documentation](driver-binding.md).
To generate a driver declaration macro including these bind rules, there should
be a corresponding `bind_rules` build target.
```
bind_rules("bind") {
rules = "ahci.bind"
output = "ahci-bind.h"
deps = [
"//src/devices/bind/deprecated.pci",
]
}
```
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 "src/devices/block/drivers/ahci/ahci-bind.h"
...
ZIRCON_DRIVER(ahci, ahci_driver_ops, "zircon", "0.1");
```
The [PCI driver](/src/devices/bus/drivers/pci/kpci/kpci.c) 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
[zircon/driver/binding.h](/zircon/system/public/zircon/driver/binding.h). In the
near future, all bind properties will be defined by bind libraries like the
`deprecated.pci` library imported above. If you are introducing a new device
class, you may need to introduce new bind properties to the binding header as
well as the bind libraries.
Bind 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.
Drivers in the ZN build will need to continue to use the old C macro style of
bind rules until the migration is complete. The following is the C macro
equivalent of the bind rules for the AHCI driver.
```c
ZIRCON_DRIVER_BEGIN(ahci, ahci_driver_ops, "zircon", "0.1", 4)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PCI),
BI_ABORT_IF(NE, BIND_PCI_CLASS, 0x01),
BI_ABORT_IF(NE, BIND_PCI_SUBCLASS, 0x06),
BI_MATCH_IF(EQ, BIND_PCI_INTERFACE, 0x01),
ZIRCON_DRIVER_END(ahci)
```
Binding directives are evaluated sequentially. The branching directives
`BI_GOTO()` and `BI_GOTO_IF()` allow you to jump forward to the matching
label, defined by `BI_LABEL()`.
`BI_ABORT_IF_AUTOBIND` may be used (usually as the first instruction) to prevent
the default 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
devhost'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()`, resetting the hardware or otherwise ensuring it is in a known state.
Because the system recovers from a
driver crash by re-spawning the devhost, the hardware may be in an unknown
state when `bind()` is invoked.
A driver is required to publish a `zx_device_t` in `bind()` by calling
`device_add()`. This is necessary for the Device Coordinator to keep
track of the
device lifecycle. If the driver is not able to publish a functional device in
`bind()`, for example if it is initializing the full device in a thread, it
should publish an invisible device, and make this device visible when
initialization is completed. See `DEVICE_ADD_INVISIBLE` and
`device_make_visible()` in
[zircon/ddk/driver.h](/zircon/system/ulib/ddk/include/ddk/driver.h).
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 via `device_add()` and returns
`ZX_OK`.
2. The driver determines that even though the bind program 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 an invisible device and kicks off
a thread to keep working, while returning `ZX_OK`. That thread will eventually
make the device visible or, if it cannot successfully initialize it, remove it.
4. The driver represents a bus or controller with 0..n children which 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.
## Device 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 ddk-internal protocols of the device that are
called by other drivers.
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.
Device protocol headers are found in
[ddk/protocol/](/zircon/system/ulib/ddk/include/ddk/protocol). Ops and any data
structures passed between drivers should be defined in this header.
## 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, via MMIO) or by communicating with its
parent device (for example, queuing a USB transaction).
External client requests from processes outside the devhost are fulfilled by
children drivers, generally in the same process, are fulfilled by device
protocols corresponding to the device class. Driver-to-driver requests should
use device 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
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 via the `message()` hook.
## Protocol ops vs. FIDL messages
Protocol ops define the DDK-internal API for a device. FIDL messages define the
external API. Define a protocol op if the function is meant to be called by
other drivers. 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 proxy devhost.
The device exists in both the parent devhost and as the root of the new devhost.
Devmgr attempts to load **driver**`.proxy.so` into this proxy devhost. For example,
PCI is supplied by `libpci.so` so devmgr would look to load `libpci.proxy.so`. The
driver is provided a channel in `create()` when it creates the proxy device
(the "bottom half" that runs in the new devhost). The proxy device should cache
this channel for when it needs to communicate with the top half (e.g. if
it needs to call API on the parent device).
`rxrpc()` is invoked on the top half when this channel is written to by the
bottom half. There is no common wire protocol for this channel. For an
example, refer to the [PCI driver](/src/devices/bus/drivers/pci).
Note: This is a mechanism used by various bus devices and not something
general drivers should have to worry about. (please ping swetland if you think
you need to use this)
## Logging
[ddk/debug.h](/zircon/system/ulib/ddk/include/ddk/debug.h) defines the
`zxlogf(<log_level>,...)` macro. The log messages are printed to the system
debuglog over the network and on the serial port if available for the device.
By default, `ERROR` and `INFO` are always printed. You can control the log
level for a driver by passing the boot cmdline
`driver.<driver_name>.log=+<level>,-<level>`. For example,
`driver.sdhci.log=-info,+trace,+spew` enables the `TRACE` and `SPEW` logs and
disable the `INFO` logs for the sdhci driver.
The log levels prefixed by "L" (`LERROR`, `LINFO`, etc.) do not get sent over
the network and are useful for network logging.
## Driver testing
### Manual hardware unit tests
A driver may choose to implement the `run_unit_tests()` driver op, which
provides the driver a hook in which it may run unit tests at system
initialization with access to the parent device. This means the driver may test
its bind and unbind hooks, as well as any interactions with real hardware. If
the tests pass (the driver returns `true` from the hook) then operation will
continue as normal and `bind()` will execute. If the tests fail then the device
manager will assume that the driver is invalid and never attempt to bind it.
Since these tests must run at system initialization (in order to not interfere
with the usual operation of the driver) they are activated via a [kernel command
line flag](/docs/reference/kernel/kernel_cmdline.md). To enable the hook for a specific driver, use
`driver.<name>.tests.enable`. Or for all drivers: `driver.tests.enable`. If a
driver doesn't implement `run_unit_tests()` then these flags will have no
effect.
`run_unit_tests()` passes the driver a channel for it to write test output to.
Test output should be in the form of `fuchsia.driver.test.Logger` FIDL messages.
The driver-unit-test library contains a [helper class][] that integrates with
zxtest and handles logging for you.
[helper class]: /zircon/system/ulib/driver-unit-test/include/lib/driver-unit-test/logger.h
### Integration tests
`ZX_PROTOCOL_TEST` provides a mechanism to test drivers by running the driver
under test in an emulated environment. Write a driver that binds to a
`ZX_PROTOCOL_TEST` device. This driver should publish a device that the driver
under test can bind to, and it should implement the protocol functions the
driver under test invokes in normal operation. This helper driver should be
declared with `BI_ABORT_IF_AUTOBIND` in the bindings.
The test harness calls `fuchsia.device.test.RootDevice/CreateDevice` on
`/dev/test/test`, which will create a `ZX_PROTOCOL_TEST` device and return
its path. Then it calls `fuchsia.device.Controller/Bind` with the helper driver on the
newly created device. This approach generally works better for mid-layer
protocol drivers. It's possible to emulate real hardware with the same
approach but it may not be as useful.
The bindings defined in
[test.banjo](/sdk/banjo/ddk.protocol.test/test.banjo) are used
for testing libraries that run as part of a driver. For an example, refer to
[system/ulib/ddk/test](/zircon/system/ulib/ddk/test). The test harness for these
tests is
[system/utest/driver-test/main.cc](/zircon/system/utest/driver-test/main.cc)
## 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
instead. For example, the virtual console is implemented by the
[virtcon](/src/bringup/virtcon) service.
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](/zircon/system/dev/board/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.