This tutorial explains how to create a composite node in a Fuchsia system for a composite driver.
A composite node specification is a mechanism used to describe a composite node in a Fuchsia system. A composite node is a node that has multiple parent nodes. A composite driver is a driver that binds to a composite node and uses resources managed by multiple parent nodes in the system.
To create a composite node for a composite driver, you need to perform the following tasks:
The steps are:
For troubleshooting issues, see Debugging composite driver issues.
A composite node is a node with multiple parents. A composite node acts as a central point of contact for multiple parent nodes, enabling access to the resources managed by these parent nodes. When a driver is bound to a composite node, the composite driver can access the combined resources provided by the parent drivers.
Figure 1. A composite node has access to multiple parent nodes and their resources.
Before you begin, familiarize yourself with the development concepts below:
This tutorial assumes familiarity with the following concepts related to Fuchsia drivers:
A composite node specification is a mechanism for creating a composite node. It describes the nodes that can serve as parent nodes to the composite node and an appropriate composite driver that can bind to the composite node.
Unlike a component manifest (.cml
) and bind rules (.bind
), a composite node specification is not a file. It is an object that needs to be constructed and added (using the CompositeNodeManager
protocol) by a driver (to be exact, by the driver's source code) loaded in a Fuchsia system.
Figure 2. A composite node specification is a collection of parent specifications.
A composite node specification comprises a set of parent specifications, each representing the criteria of a parent node of the composite node. A set of parent specifications serve the following purposes:
Each parent specification in a composite node specification contains the following:
When a composite node specification is introduced to the system, the following events take place:
A composite node specification is added to the driver framework by a driver loaded in a Fuchsia system, which is typically a board driver.
The driver manager asks the driver index to search for a matching composite driver based on the composite node specification.
A composite driver whose bind rules can satisfy all the properties of the parent specifications in this composite node specification is matched. (See How does the driver framework match a composite driver to a composite node?)
Once a composite driver is found, the driver manager asks the driver index to search for nodes in the system based on the composite node specification.
Nodes whose node properties can satisfy the bind rules provided by the parent specifications in the composite node specification are matched. Each matching node becomes a parent node of the newly created composite node in step 3.
After all parent specifications have a match, the driver manager creates a composite node using the matched nodes as parents and finally binds the composite driver (matched in step 1) to the newly created composite node. (The composite node provides the primary node and node names.)
A composite driver‘s matching process is done by applying the composite driver’s bind rules to the properties provided by the parent specifications in a composite node specification. (Notice that this is different from applying the parent specifications' bind rules to match nodes in the topology.)
For example, these bind rules are from a composite driver:
composite focaltech_touch; using fuchsia.gpio; using fuchsia.hardware.i2c; using fuchsia.i2c; primary node "i2c" { fuchsia.hardware.i2c.Service == fuchsia.hardware.i2c.Service.ZirconTransport; fuchsia.BIND_I2C_ADDRESS == fuchsia.i2c.BIND_I2C_ADDRESS.FOCALTECH_TOUCH; } node "gpio-int" { fuchsia.BIND_PROTOCOL == fuchsia.gpio.BIND_PROTOCOL.DEVICE; fuchsia.gpio.FUNCTION == fuchsia.gpio.FUNCTION.TOUCH_INTERRUPT; }
Using these bind rules, a matching composite node specification requires a parent specification with properties that match i2c
and another parent specification with properties that match gpio-int
.
A match between a composite driver and a composite node (specification) is successful if the following conditions are met:
All parent specifications must match with a node in the composite driver's bind rules.
All non-optional nodes in the composite driver's bind rules must match with a parent specification.
For example, these bind rules from a composite driver includes an optional node:
composite virtio_input; using fuchsia.acpi; using fuchsia.hardware.pci; using fuchsia.pci; primary node "pci" { fuchsia.hardware.pci.Service == fuchsia.hardware.pci.Service.ZirconTransport; fuchsia.BIND_PCI_VID == fuchsia.pci.BIND_PCI_VID.VIRTIO; fuchsia.BIND_PCI_DID == fuchsia.pci.BIND_PCI_DID.VIRTIO_DEV_TYPE_INPUT; } optional node "acpi" { fuchsia.BIND_PROTOCOL == fuchsia.acpi.BIND_PROTOCOL.DEVICE; }
(Source: virtio_input.bind
)
Nodes do not need to be matched in the order listed in the composite driver's bind rules.
However, matching cannot be ambiguous, that is:
If an ambiguous case occurs, the driver manager will print a warning message in the logs. (Currently, the match still happens, but it will be disallowed in the future. See this related ticket.)
Figure 3. Each parent specification's bind rules are used to find a parent node of the composite node.
Figure 4. A composite driver‘s bind rules are matched against a composite node’s properties, which are collectively provided by parent specifications in a composite node specification.
The bind rules in a parent specification provide a list of accepted and rejected property values. For a node to satisfy the bind rules, its node properties must contain all the accepted node property values and none of the rejected ones.
For example, a parent specification can contain the following bind rules:
fuchsia.BIND_PROTOCOL
values 15
and 17
.fuchsia.BIND_PLATFORM_DEV_VID
values Intel
.Using these bind rules, a node can be matched only if it contains a value of 15
or 17
for the fuchsia.BIND_PROTOCOL
property and doesn't have the Intel
value for the fuchsia.BIND_PLATFORM_DEV_VID
property.
You first need to prepare the parent specifications that will make up a composite node specification.
To prepare parent specifications, the steps are:
Bind rules are used to find and match nodes in the topology according to parent specifications. A node's properties are evaluated against the bind rules in a parent specification. If they match, the node becomes a parent of a composite node.
The process for writing bind rules for a composite node's parent nodes is similar to the process described in the Bind rules tutorial. To determine the bind rules, you first need to identify the properties of each parent node that you want to bind to.
To view the properties of every node in the node topology, you can use the following ffx
command:
ffx driver list-devices -v
This command prints entries similar to the following:
Name : i2c-1-56 Topo Path: sys/platform/i2c-0/aml-i2c/i2c/i2c-1-56 Driver : fuchsia-boot:///#driver/i2c.so Flags : MUST_ISOLATE | BOUND Proto : ZX_PROTOCOL_I2C (24) 3 Properties [ 1/ 3] : Key fuchsia.BIND_I2C_BUS_ID Value 0x000001 [ 2/ 3] : Key fuchsia.BIND_I2C_ADDRESS Value 0x000038 [ 3/ 3] : Key "fuchsia.hardware.i2c.Service" Value "fuchsia.hardware.i2c.Service.ZirconTransport"
The example entry above shows the following node properties for the i2c-1-56
node:
fuchsia.I2C_BUS_ID
= 0x000001undefined
fuchsia.I2C_ADDRESS
= 0x000038
fuchsia.hardware.i2c.Service
= fuchsia.hardware.i2c.Service.ZirconTransport
To see which property values are acceptable, you can look up the bind libraries in the Fuchsia codebase (for example, in the src/devices/bind
directory). In this example, since the node is an I2C node, the property values are found in the fuchsia.i2c
bind library as shown below:
extend uint fuchsia.BIND_I2C_BUS_ID { I2C_A0_0 = 0, I2C_2 = 1, I2C_3 = 2, }; extend uint fuchsia.BIND_I2C_ADDRESS { BACKLIGHT = 0x2C, ETH = 0x18, FOCALTECH_TOUCH = 0x38, AMBIENTLIGHT = 0x39, AUDIO_CODEC = 0x48, GOODIX_TOUCH = 0x5d, TI_INA231_MLB = 0x49, TI_INA231_SPEAKERS = 0x40, TI_INA231_MLB_PROTO = 0x46, };
In addition to the bind libraries in the Fuchsia codebase, you can also generate bind libraries from FIDL libraries. In the example above, this is where the property for the fuchsia.hardware.i2c.Service
key and its value fuchsia.hardware.i2c.Service.ZirconTransport
come from. (For more information, see Generated bind libraries.)
Using the property values found in the bind library, you can remap the node properties as shown below:
fuchsia.BIND_I2C_BUS_ID
= fuchsia.i2c.BIND_I2C_BUS_ID.I2C_2
fuchsia.BIND_I2C_ADDRESS
= fuchsia.i2c.BIND_I2C_ADDRESS.FOCALTECH_TOUCH
fuchsia.hardware.i2c.Service
= fuchsia.hardware.i2c.Service.ZirconTransport
You can access these bind library values in your driver source code through its generated libraries. (For more information, see the Bind libraries codegen tutorial.)
The bind rules that match these properties are defined as shown below:
accept fuchsia.hardware.i2c.Service { fuchsia.hardware.i2c.Service.ZirconTransport } accept BIND_I2C_BUS_ID { fuchsia.i2c.BIND_I2C_BUS_ID.I2C_2 } accept BIND_I2C_ADDRESS { fuchsia.i2c.BIND_I2C_ADDRESS.FOCALTECH_TOUCH }
In DFv1, composite node specifications are written using the DDKTL (Device Driver Kit Template Library). The functions to write the bind rules are in composite-node-spec.h
.
With the DDK library and bind libraries codegen values, we can write the following:
const ddk::BindRule kI2cBindRules[] = { ddk::MakeAcceptBindRule(bind_fuchsia_hardware_i2c::SERVICE, bind_fuchsia_hardware_i2c::SERVICE_ZIRCONTRANSPORT), ddk::MakeAcceptBindRule(bind_fuchsia::I2C_BUS_ID, bind_fuchsia_i2c::BIND_I2C_BUS_ID_I2C_2), ddk::MakeAcceptBindRule(bind_fuchsia::I2C_ADDRESS, bind_fuchsia_focaltech_platform::BIND_I2C_ADDRESS_TOUCH), };
In DFv2, composite node specifications are defined by the CompositeNodeSpec
protocol from composite_node_spec.fidl
in the fuchsia.driver.framework
FIDL library. The composite_node_spec.h
library in the sdk/lib/driver/component/cpp
directory can be used to simplify defining the bind rules.
Using the CompositeNodeSpec
library and bind libraries codegen values, we can write the following:
auto i2c_bind_rules = std::vector { MakeAcceptBindRule(bind_fuchsia_hardware_i2c::SERVICE, bind_fuchsia_hardware_i2c::SERVICE_ZIRCONTRANSPORT), MakeAcceptBindRule(bind_fuchsia::I2C_BUS_ID, bind_fuchsia_i2c::BIND_I2C_BUS_ID_I2C_2), MakeAcceptBindRule(bind_fuchsia::I2C_ADDRESS, bind_fuchsia_focaltech_platform::BIND_I2C_ADDRESS_TOUCH), };
In addition to defining the bind rules for nodes, in parent specifications, you need to define the properties that will be used in finding a composite driver.
These properties are key-value pairs that are used to match a parent specification to a composite driver's bind rules. They are the same thing as node properties, following the same format. The property key can be integer-based or string-based while the property value can be an integer, boolean, string or enum type.
In DFv1, composite node specifications are written using DDKTL and the functions to write the bind rules are in composite-node-spec.h
. You can define properties using the DDK library and bind libraries codegen values as shown below:
const device_bind_prop_t kI2cProperties[] = { ddk::MakeProperty(bind_fuchsia_hardware_i2c::SERVICE, bind_fuchsia_hardware_i2c::SERVICE_ZIRCONTRANSPORT), ddk::MakeProperty(bind_fuchsia::I2C_ADDRESS, bind_fuchsia_focaltech_platform::BIND_I2C_ADDRESS_TOUCH), };
In DFv2, composite node specifications are written for composite_node_spec.fidl
in the fuchsia.driver.framework
FIDL library. The node_add_args.h
library in //sdk/lib/driver/component/cpp
can be used to simplify defining properties. You can define properties using the CompositeNodeSpec
library and bind libraries codegen values as shown below:
auto i2c_properties[] = std::vector { ddk::MakeProperty(bind_fuchsia::I2C_ADDRESS, bind_fuchsia_focaltech_platform::BIND_I2C_ADDRESS_TOUCH), };
Creating a composite node specification involves defining and adding a set of parent specifications to the driver manager.
A composite node specification is added to the driver framework by a driver loaded in a Fuchsia system, which is typically a board driver. (For more information on this process, see How does the driver framework create a composite node in a Fuchsia system?)
In DFv1, a driver can add composite node specifications through DDKTL with the DdkAddCompositeNodeSpec()
function.
The driver must first define a CompositeNodeSpec
object in the composite-node-spec.h
library. Using the example bind rules and properties in the previous section, you can define a CompositeNodeSpec
object with an I2C parent specification as following:
const ddk::BindRule kI2cBindRules[] = { ddk::MakeAcceptBindRule(bind_fuchsia_hardware_i2c::SERVICE, bind_fuchsia_hardware_i2c::SERVICE_ZIRCONTRANSPORT), ddk::MakeAcceptBindRule(bind_fuchsia::I2C_BUS_ID, bind_fuchsia_i2c::BIND_I2C_BUS_ID_I2C_2), ddk::MakeAcceptBindRule(bind_fuchsia::I2C_ADDRESS, bind_fuchsia_focaltech_platform::BIND_I2C_ADDRESS_TOUCH), }; const device_bind_prop_t kI2cProperties[] = { ddk::MakeProperty(bind_fuchsia_hardware_i2c::SERVICE, bind_fuchsia_hardware_i2c::SERVICE_ZIRCONTRANSPORT), ddk::MakeProperty(bind_fuchsia::I2C_ADDRESS, bind_fuchsia_focaltech_platform::BIND_I2C_ADDRESS_TOUCH), }; auto spec = ddk::CompositeNodeSpec(kI2cBindRules, kI2cProperties);
Any additional nodes can be added with the AddParentSpec()
function. For instance, if we want to add a parent specification for a GPIO interpret pin, we can write the following:
const ddk::BindRule kGpioInterruptRules[] = { ddk::MakeAcceptBindRule(bind_fuchsia::PROTOCOL, bind_fuchsia_gpio::BIND_PROTOCOL_DEVICE), ddk::MakeAcceptBindRule(bind_fuchsia::GPIO_PIN, bind_fuchsia_amlogic_platform_s905d2::GPIOZ_PIN_ID_PIN_4), }; const device_bind_prop_t kGpioInterruptProperties[] = { ddk::MakeProperty(bind_fuchsia::PROTOCOL, bind_fuchsia_gpio::BIND_PROTOCOL_DEVICE), ddk::MakeProperty(bind_fuchsia_gpio::FUNCTION, bind_fuchsia_gpio::FUNCTION_TOUCH_INTERRUPT)}; desc.AddParentSpec(kGpioInterruptRules, kGpioInterruptProperties);
Once the CompositeNodeSpec
object is ready, you can add it with DdkAddCompositeNodeSpec()
, where spec
is an object containing the composite node specification, for example:
auto status = DdkAddCompositeNodeSpec("ft3x27_touch", spec);
Since a CompositeNodeSpec
object follows the builder pattern, this can be simplified to:
auto status = DdkAddCompositeNodeSpec("ft3x27_touch", ddk::CompositeNodeSpec(kFocaltechI2cRules, kFocaltechI2cProperties) .AddParentSpec(kGpioInterruptRules, kGpioInterruptProperties) .set_metadata(metadata);
In DFv2, we use the CompositeNodeManager
protocol from the fuchsia.driver.framework
FIDL API to add a composite node specification:
@discoverable protocol CompositeNodeManager { /// Add the given spec to the driver manager. AddSpec(CompositeNodeSpec) -> () error CompositeNodeSpecError; };
If the composite node needs a parent from a node on the platform bus, then the board driver can add the composite node specification through the platform_bus.fidl
API. This applies to both DFv1 and DFv2.
/// Adds a composite node specification to the bus. This will add a platform device specified /// by |node| and insert a node into the composite node specification that matches the device. AddCompositeNodeSpec(struct { node Node; spec fuchsia.driver.framework.CompositeNodeSpec; }) -> () error zx.Status;
The platform bus API uses the same CompositeNodeSpec
struct defined in composite_node_spec.fidl
.
For example, say we defined the following composite node specification:
std::vector<fuchsia_driver_framework::BindRule> bind_rules = { fdf::MakeAcceptBindRule(bind_fuchsia_hardware_i2c::SERVICE, bind_fuchsia_hardware_i2c::SERVICE_ZIRCONTRANSPORT), fdf::MakeAcceptBindRule(bind_fuchsia::I2C_ADDRESS, bind_fuchsia_i2c::BIND_I2C_ADDRESS_BACKLIGHT), }; std::vector<fuchsia_driver_frameowork::Node> properties = { fdf::MakeProperty(bind_fuchsia_hardware_i2c::SERVICE, bind_fuchsia_hardware_i2c::SERVICE_ZIRCONTRANSPORT), fdf::MakeProperty(bind_fuchsia::I2C_ADDRESS, bind_fuchsia_i2c::BIND_I2C_ADDRESS_BACKLIGHT), }; {% verbatim %} std::vector<fuchsia_driver_framework::ParentSpec> spec = { {{bind_rules, properties}} }; {% endverbatim %}
(For more details on the library used in the example above, see Defining properties in DFv2 and Defining bind rules in DFv2.)
Once the composite node specification is defined, the board driver can connect to the platform bus through the PlatformBus
FIDL protocol and use the client end to call AddCompositeNodeSpec()
.
The AddCompositeNodeSpec()
call inserts a parent specification for a platform device (created from the data in the node field) into the given composite node specification and adds this modified composite node specification into the driver framework. It then creates and adds the platform device, for example:
fpbus::Node dev; dev.name() = "backlight"; dev.vid() = PDEV_VID_TI; // 0x10 dev.pid() = PDEV_PID_TI_LP8556; // 0x01 dev.did() = PDEV_DID_TI_BACKLIGHT; // 0x01 auto endpoints = fdf::CreateEndpoints<fuchsia_hardware_platform_bus::PlatformBus>(); if (endpoints.is_error()) { return endpoints.error_value(); } fdf::WireSyncClient<fuchsia_hardware_platform_bus::PlatformBus> pbus = endpoints->client; auto result = pbus.buffer(arena)->AddCompositeNodeSpec( fidl::ToWire(fidl_arena, dev), fidl::ToWire(fidl_arena, spec), false); if (!result.ok()) { FDF_LOG(ERROR, "AddCompositeNodeSpec request failed: %s", result.FormatDescription().data()); return result.status(); }
After AddCompositeNodeSpec()
is called, the following composite node specification is added to the driver framework:
Name : backlight Driver : fuchsia-boot:///#meta/ti-lp8556.cm Nodes : 2 Node 0 : None 3 Bind Rules [ 1/ 3] : Accept "fuchsia.BIND_PLATFORM_DEV_VID" { 0x000010 } [ 2/ 3] : Accept "fuchsia.BIND_PLATFORM_DEV_PID" { 0x000001 } [ 2/ 3] : Accept "fuchsia.BIND_PLATFORM_DEV_DID" { 0x000001 } 3 Properties [ 1/ 3] : Key "fuchsia.BIND_PLATFORM_DEV_VID" Value 0x000010 [ 2/ 3] : Key "fuchsia.BIND_PLATFORM_DEV_PID" Value 0x000001 [ 3/ 3] : Key "fuchsia.BIND_PLATFORM_DEV_DID" Value 0x000001 Node 1 : None 2 Bind Rules [ 1/ 2] : Accept "fuchsia.hardware.i2c.Service" { "fuchsia.hardware.i2c.Service.ZirconTransport" } [ 2/ 2] : Accept "fuchsia.BIND_I2C_ADDRESS" { 0x00002C } 2 Properties [ 1/ 2] : Key "fuchsia.hardware.i2c.Service" Value "fuchsia.hardware.i2c.Service.ZirconTransport" [ 2/ 2] : Key "fuchsia.BIND_I2C_ADDRESS" Value 0x00002C }
The first node (Node 0
) shown in the composite node specification example above matches the platform device created from AddCompositeNodeSpec()
. The first parent specification is inserted by AddCompositeSpec()
and is targeted for matching the platform device, which contains bind rules and properties from the VID, PID
, and DID
provided in fpbus::Node dev
. The remaining parent specifications are from the passed-in composite node specification.
Once a composite node specification is defined, you can start writing bind rules for your composite driver, which will be matched to the composite node created according to the composite node specification.
The process of writing bind rules for a composite driver is similar to writing bind rules for a driver.
The examples in the previous sections include the following properties in its parent specifications:
i2c parent specification properties { fuchsia.hardware.i2c.Service: fuchsia.hardware.i2c.Service.ZirconTransport, fuchsia.BIND_I2C_ADDRESS: fuchsia.focaltech.platform.BIND_I2C_ADDRESS_TOUCH, } gpio-interrupt parent specification properties { fuchsia.BIND_PROTOCOL: fuchsia.gpio.BIND_PROTOCOL_DEVICE, fuchsia.gpio.FUNCTION: fuchsia.gpio.FUNCTION.TOUCH_INTERRUPT, }
If you want to bind to a composite node specification with the properties above, you can write the following composite bind rules to match the target parent specifications:
composite focaltech_touch; using fuchsia.gpio; using fuchsia.hardware.i2c; using fuchsia.i2c; primary node "i2c" { fuchsia.hardware.i2c.Service == fuchsia.hardware.i2c.Service.ZirconTransport; fuchsia.BIND_I2C_ADDRESS == fuchsia.i2c.BIND_I2C_ADDRESS.FOCALTECH_TOUCH; } node "gpio-int" { fuchsia.BIND_PROTOCOL == fuchsia.gpio.BIND_PROTOCOL.DEVICE; fuchsia.gpio.FUNCTION == fuchsia.gpio.FUNCTION.TOUCH_INTERRUPT; }
To verify that a composite node is successfully created and is attempting to bind a composite driver, you can examine the logs and look for the statement similar to the following line:
Binding driver fuchsia-boot:///#meta/focaltech.cm
To verify that the composite node specification is added successfully and matched to a composite driver, run the following command:
ffx driver list-composite-node-specs -v
This command prints output similar to the following:
Name : ft3x27_touch Driver : fuchsia-boot:///#meta/focaltech.cm Nodes : 2 Node 0 : "i2c" (Primary) 3 Bind Rules [ 1/ 3] : Accept "fuchsia.hardware.i2c.Service" { "fuchsia.hardware.i2c.Service.ZirconTransport" } [ 2/ 3] : Accept "fuchsia.BIND_I2C_BUS_ID" { 0x000001 } [ 3/ 3] : Accept "fuchsia.BIND_I2C_ADDRESS" { 0x000038 } 2 Properties [ 1/ 2] : Key "fuchsia.hardware.i2c.Service" Value "fuchsia.hardware.i2c.Service.ZirconTransport" [ 2/ 2] : Key "fuchsia.BIND_I2C_ADDRESS" Value 0x000038 Node 1 : "gpio-int" 2 Bind Rules [ 1/ 2] : Accept "fuchsia.BIND_PROTOCOL" { 0x000014 } [ 2/ 2] : Accept "fuchsia.BIND_GPIO_PIN" { 0x000004 } 2 Properties [ 1/ 2] : Key "fuchsia.BIND_PROTOCOL" Value 0x000014 [ 2/ 2] : Key "fuchsia.gpio.FUNCTION" Value "fuchsia.gpio.FUNCTION.TOUCH_INTERRUPT"
If there is no matching composite driver for the composite node specification, the command will print output similar to the following:
Name : focaltech_touch Driver : None Nodes : 2 Node 0 : None 3 Bind Rules [ 1/ 3] : Accept "fuchsia.hardware.i2c.Service" { "fuchsia.hardware.i2c.Service.ZirconTransport" } [ 2/ 3] : Accept "fuchsia.BIND_I2C_BUS_ID" { 0x000001 } [ 3/ 3] : Accept "fuchsia.BIND_I2C_ADDRESS" { 0x000038 } 1 Properties [ 1/ 2] : Key "fuchsia.hardware.i2c.Service" Value "fuchsia.hardware.i2c.Service.ZirconTransport" [ 2/ 2] : Key "fuchsia.BIND_I2C_ADDRESS" Value 0x000038 Node 1 : None 2 Bind Rules [ 1/ 2] : Accept "fuchsia.BIND_PROTOCOL" { 0x000014 } [ 2/ 2] : Accept "fuchsia.BIND_GPIO_PIN" { 0x000004 } 2 Properties [ 1/ 2] : Key "fuchsia.BIND_PROTOCOL" Value 0x000014 [ 2/ 2] : Key "fuchsia.gpio.FUNCTION" Value "fuchsia.gpio.FUNCTION.TOUCH_INTERRUPT
For more information on the ffx driver
command, see View driver information.