blob: 3af42f83cbe1a15f23f0a897022504ae8d413801 [file] [log] [blame] [view]
# Composite Devices
This document is part of the [Zircon Driver Development
## Introduction
In this section, we look at **composite devices**.
A composite device is a device composed of other devices.
These devices address the case of hardware-level composition,
in which a "device" (from the user's perspective) is implemented by several
distinct hardware blocks.
Examples include:
* a touch panel composed of an I2C device and a GPIO,
* an ethernet device composed of a MAC chip and one or more PHYs, or
* an audio device composed of an audio controller and a set of codecs.
In these situations, the relationship of the hardware is known to the board
driver at boot time (either statically or through a dynamic means, such as
We'll use the `astro-audio` device for our examples:
![Figure: Composite hardware device on I2C bus with GPIOs](composite-audio.png)
This device features:
* an I2C bus interface
* two sets of GPIOs (one for fault, one for enable)
* MMIO (memory mapped I/O) for bulk data transfer, and
* an IRQ (interrupt request) line to generate interrupts to the driver.
Note that the `ZX_PROTOCOL_I2C` and `ZX_PROTOCOL_GPIO` protocols are used to
transfer data; that is, I2C messages, and GPIO pin status are sent and received
via the respective drivers.
The `ZX_PROTOCOL_PDEV` part is different.
Here, the protocol is used only to grant access (the green checkmarks in the
diagram) to the MMIO and IRQ; the actual MMIO data and interrupts are **not**
handled by the `PDEV`; they're handled directly by the `astro-audio` driver
## Creating a composite device
To create a composite device, a number of data structures need to be set up.
### Binding instructions
We need a number of binding instructions (`zx_bind_inst_t`) that tell us which
devices we match.
These binding instructions are the ones we've already discussed in the
["Registration" topic]( in the introduction section.
For the `astro-audio` device, we have:
static const zx_bind_inst_t root_match[] = {
static const zx_bind_inst_t i2c_match[] = {
static const zx_bind_inst_t fault_gpio_match[] = {
static const zx_bind_inst_t enable_gpio_match[] = {
These binding instructions are used to find the devices.
We have four binding instruction arrays; a `root_match[]`, which contains
common information for the other three, and then the three devices:
the I2C (`i2c_match[]`) device and the two GPIOs (`fault_gpio_match[]` and
These instructions are then placed into an array of structures
(`device_fragment_part_t`) which defines each fragment:
![Figure: Binding instructions gathered into a fragment
In the `astro-audio` device, we have:
static const device_fragment_part_t i2c_fragment[] = {
{ countof(root_match), root_match },
{ countof(i2c_match), i2c_match },
static const device_fragment_part_t fault_gpio_fragment[] = {
{ countof(root_match), root_match },
{ countof(fault_gpio_match), fault_gpio_match },
static const device_fragment_part_t enable_gpio_fragment[] = {
{ countof(root_match), root_match },
{ countof(enable_gpio_match), enable_gpio_match },
At this point, we have three fragment devices, `i2c_fragment[]`,
`fault_gpio_fragment[]`, and `enable_gpio_fragment[]`.
### Fragment device matching rules
The following rules apply:
1. The first element must describe the root of the device tree — this
is why we've used the mnemonic `root_match` identifier.
Note that this requirement is likely to change, since most users provide
an "always match" anyway.
2. The last element must describe the target device itself.
3. The remaining elements must match devices on the path from the root to
the target device, in order.
Some of those **devices** may be skipped, but every **element** must
be matched.
4. Every device on the path that has a property from the range
`BIND_TOPO_START` through `BIND_TOPO_END` (basically buses, like I2C
and PCI) must be matched.
These sequences of matches must be unique.
Finally, we combine them into an aggregate called `fragments[]` of type
![Figure: Gathering fragments into an aggregate](composite-fragments.png)
This now gives us a single identifier, `fragments[]`, that we can use
when creating the composite device.
In `astro-audio`, this looks like:
static const device_fragment_t fragments[] = {
{ countof(i2c_fragment), i2c_fragment },
{ countof(fault_gpio_fragment), fault_gpio_fragment },
{ countof(enable_gpio_fragment), enable_gpio_fragment },
### Creating the device
For simple (non-composite) devices, we used **device_add()** (which we
saw in the ["Registration" section]( previously).
For composite devices, we use **device_add_composite()**:
zx_status_t device_add_composite(
zx_device_t* dev,
const char* name,
const zx_device_prop_t* props,
size_t props_count,
const device_fragment_t* fragments,
size_t fragments_count,
uint32_t coresident_device_index);
The arguments are as follows:
Argument | Meaning
`dev` | Parent device
`name` | The name of the device
`props` | Properties ([see "Declaring a Driver"](
`props_count` | How many entries are in `props`
`fragments` | The individual fragment devices
`fragments_count` | How many entries are in `fragments`
`coresident_device_index` | Which devhost to use
The `dev` value must be the `zx_device_t` corresponding to the "`sys`"
device (i.e., the platform bus driver's device).
Note that the `coresident_device_index` is used to indicate which devhost
the new device should use.
If you specify `UINT32_MAX`, the device will reside in a new devhost.
> Note that `astro-audio` uses **pbus_composite_device_add()** rather
> than **composite_device_add()**.
> The difference is that **pbus_composite_device_add()** is an API
> provided by the platform bus driver that wraps **composite_device_add()** and
> inserts an additional fragment for ferrying over direct-access resources
> such as MMIO, IRQs, and BTIs.
## Using a composite device
From a programming perspective, a composite device acts like an ordinary device
that implements a `ZX_PROTOCOL_COMPOSITE` protocol.
This allows you to access all of the individual fragments that make up the
composite device.
The first thing to do is get a list of the devices.
This is done via **composite_get_fragments()**:
void composite_get_fragments (
composite_protocol_t* composite,
zx_device_t* fragments,
size_t fragment_count,
size_t* actual_count);
The arguments are as follows:
Argument | Meaning
`composite` | The protocol handle
`fragments` | Pointer to an array of `zx_device_t*`
`fragment_count` | Size of `fragments` array
`actual_count` | Actual number of entries filled in `fragments`
The program starts by calling **device_get_protocol()** to get the protocol for the
composite driver:
composite_protocol_t composite;
auto status = device_get_protocol(parent, ZX_PROTOCOL_COMPOSITE, &composite);
Assuming there aren't any errors (`status` is equal to `ZX_OK`), the next step is to
declare an array of `zx_device_t*` pointers to hold the devices, and call
enum {
zx_device_t* fragments[FRAGMENT_COUNT];
size_t actual;
composite_get_fragments(&composite, fragments, FRAGMENT_COUNT, &actual);
if (actual != FRAGMENT_COUNT) {
zxlogf(ERROR, "%s: could not get our fragments", __FILE__);
> The ordering of the devices returned by **device_get_fragments()**
> is defined to be the same as the ordering given to the **device_add_composite()**
> call by the board driver.
> Therefore, any enums are for convenience, and are not inherently tied to the
> ordering.
> Many composite devices will have a fixed number of fragments in a specific
> order, but there may also be composite devices that have a variable number
> of fragments, in which case the fragments might be identified by device
> metadata (via **device_get_metadata()**), or by some other means.
## Advanced Topics
Here we discuss some specialized / advanced topics.
### Composite devices and proxies
What's actually going on in the `astro-audio` driver is a little more complex than
initially shown:
![Figure: Composite hardware device using proxies](composite-proxy.png)
The fragments are bound to an internal driver (located in the
[//src/devices/internal/drivers/fragment][fragment] directory).
The driver handles proxying across process boundaries if necessary.
This proxying uses the `DEVICE_ADD_MUST_ISOLATE` mechanism (introduced
in the [Isolate devices][isolate] section).
When a device is added with `DEVICE_ADD_MUST_ISOLATE`, two devices
end up being created:
the normal device, in the same process as its parent, and a proxy.
The proxy is created in a new devhost; if the normal device's
driver is ``, then its driver is ``.
This driver is expected to implement a **create()** method which calls
**device_add()** and stashes the IPC channel it's given.
That channel will be used later for communicating with the normal
device in order to satisfy the proxy's children's requests.
The normal device implements the `rxrpc` hook, which is invoked by
the driver runtime each time a message is received from the channel
shared with the proxy.
So, in order to implement a new protocol proxy, one must modify the
`` drivers to handle the desired protocol by sending
messages to the normal device, and modify the `` driver to
service those messages appropriately.
The fragment proxy is implemented in
[/src/devices/internal/drivers/fragment/][], and
the other half in
<!-- xrefs -->
[]: /src/devices/internal/drivers/fragment/
[]: /src/devices/internal/drivers/fragment/
[fragment]: /src/devices/internal/drivers/fragment/
[composite.banjo]: /zircon/system/banjo/ddk.protocol.composite/composite.banjo
[driver.h]: /src/lib/ddk/include/ddk/driver.h
<!-- diagram source at -->