Driver Binding

In Fuchsia, the driver framework maintains a tree of drivers and devices in the system. In this tree, a device represents access to some hardware available to the OS. A driver both publishes and binds to devices. For example, a USB driver might bind to a PCI device (its parent) and publish an ethernet device (its child). In order to determine which devices a driver can bind to, each driver has a bind program and each device has a set of properties. The bind program defines a condition that matches the properties of devices that it wants to bind to. For more details, see the Zircon Driver Development Kit documentation.

Bind programs and the conditions they refer to are defined by a domain specific language. The bind compiler consumes this language and produces bytecode for bind programs. In the future, it will also produce code artefacts that drivers may refer to when publishing device properties. The language has two kinds of source files: programs, and libraries. Libraries are used to share property definitions between drivers and bind programs.

Note: Driver binding is under active development and this document describes the current state. Not all drivers use this form of bind rules but a migration is under way to convert them all.

One thing to note about this stage of the migration is that there is no support for defining device property keys in bind libraries (see below). Instead, the keys from the old driver binding system (driver/binding.h) are available to be extended. These keys are hardcoded into the bind compiler and are available under the deprecated namespace. For example, the PCI vendor ID key is deprecated.BIND_PCI_VID. Eventually this namespace will be removed and all bind property keys will be defined in bind libraries.

The compiler

The compiler takes a list of library sources, and one program source. For example:

fx bindc \
  --include src/devices/bind/deprecated.usb/deprecated.usb.bind \
  --output tools/bindc/examples/gizmo.h \
  tools/bindc/examples/gizmo.bind

Currently, it produces a C header file that may be included by a driver. The header file defines a macro:

ZIRCON_DRIVER(Driver, Ops, VendorName, Version)
  • Driver is the name of the driver.
  • Ops is a zx_driver_ops, which are the driver operation hooks
  • VendorName is a string representing the name of the driver vendor.
  • Version is a string representing the version of the driver.

For more details, see [the driver development documentation] (/docs/concepts/drivers/driver-development).

Bind rules

A bind program defines the conditions to call a driver‘s bind() hook. Each statement in the bind program is a condition over the properties of the device that must hold true in order for the driver to bind. If the bind rules finish executing and all conditions are true, then the device coordinator will call the driver’s bind() hook.

There are four kinds of statements:

  • Condition statements are equality (or inequality) expressions of the form <key> == <value> (or <key> != <value>).
  • Accept statements are lists of permissible values for a given key.
  • If statements provide simple branching.
  • Abort statements cause the bind rule execution to terminate and the driver will not bind.

Example

This example bind program can be found at //tools/bindc/examples/gizmo.bind.

using deprecated.usb;

// The device must be a USB device.
deprecated.BIND_PROTOCOL == deprecated.usb.BIND_PROTOCOL.DEVICE;

if deprecated.BIND_USB_VID == deprecated.usb.BIND_USB_VID.INTEL {
  // If the device's vendor is Intel, the device class must be audio.
  deprecated.BIND_USB_CLASS == deprecated.usb.BIND_USB_CLASS.AUDIO;
} else if deprecated.BIND_USB_VID == deprecated.usb.BIND_USB_VID.REALTEK {
  // If the device's vendor is Realtek, the device class must be one of the following values:
  accept deprecated.BIND_USB_CLASS {
    deprecated.usb.BIND_USB_CLASS.COMM,
    deprecated.usb.BIND_USB_CLASS.VIDEO,
  }
} else {
  // If the vendor is neither Intel or Realtek, do not bind.
  abort;
}

Language restrictions

There are some restrictions on the language that are imposed to improve readability and ensure that bind rules are simple representations of the conditions under which a driver should bind.

  • Empty blocks are not allowed. It's ambiguous whether an empty block should mean that the driver will bind or abort. The author should either use an explicit abort statement, or refactor the previous if statement conditions into a condition statement.

  • If statements must have else blocks and are terminal. This restriction increases readability by making explicit the branches of execution. Since no statement may follow an if statement, it is easy to trace a path through the bind rules.

Grammar

program = using-list , ( statement )+ ;

using-list = ( using , ";" )* ;

using = "using" , compound-identifier , ( "as" , IDENTIFIER ) ;

statement = condition , ";" | accept | if-statement | abort ;

condition = compound-identifier , condition-op , value ;

condition-op = "==" | "!=" ;

accept = "accept" , compound-identifier , "{" ( value , "," )+ "}" ;

if-statement = "if" , condition , "{" , ( statement )+ , "}" ,
                ( "else if" , "{" , ( statement )+ , "}" )* ,
                "else" , "{" , ( statement )+ , "}" ;

abort = "abort" , ";" ;

compound-identifier = IDENTIFIER ( "." , IDENTIFIER )* ;

value = compound-identifier | STRING-LITERAL | NUMERIC-LITERAL | "true" | "false" ;

An identifier matches the regex [a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])? and must not match any keyword. The list of keywords is:

abort
accept
as
else
if
using

A string literal matches the regex ”[^”]*”, and a numeric literal matches the regex [0-9]+ or 0x[0-9A-F]+.

The bind compiler will ignore (treat as whitespace) any line prefixed by //, and any multiple lines delimited by /* and */.

Build targets

To declare bind rules within the Fuchsia build system, use the following build target:

bind_rules("bind") {
  rules = <bind rules filename>
  output = <generated header filename>
  deps = [ <list of bind library targets> ]
}

For more details, refer to //build/bind/bind.gni.

Bind libraries

A bind library defines a set of properties that drivers may assign to their children. Also, bind programs may refer to bind libraries.

Namespacing

A bind library begins by defining its namespace:

library <vendor>.<library>;

Every namespace must begin with a vendor and each vendor should ensure that there are no clashes within their own namespace. However, the language allows for one vendor to extend the library of another. Google will use fuchsia for public libraries.

Any values introduced by a library are namespaced. For example, the following library defines a new PCI device ID GIZMO_VER_1.

library gizmotronics.gizmo;

using fuchsia.pci as pci;

extend uint pci.device_id {
  GIZMO_VER_1 = 0x4242,
};

To refer to this value the driver author should use the fully qualified name, as follows.

using fuchsia.pci as pci;
using gizmotronics.gizmo;

pci.device_id == gizmotronics.gizmo.device_id.GIZMO_VER_1

Keys and values

Device property definitions look similar to variable declarations in other languages.

<type> <name>;
Or:
<type> <name> {
  <value>,
  <value>,
  …
};

A bind library may also extend properties from other libraries.

extend <type> <name> {
  <value>,
  …
};

Each key has a type, and all values that correspond to that key must be of that type. The language supports primitive types: one of uint, string, or bool; and enumerations (enum). When defining keys you should prefer enumerations except when values will be provided by an external source, such as hardware.

When definining a primitive value use the form <identifier> = <literal>, and for enumerations only an identifier is necessary. It is valid to define multiple primitive values with the same literal.

Grammar

library = library-header , using-list , declaration-list ;

library-header = "library" , compound-identifier , ";" ;

using-list = ( using , ";" )* ;

using = "using" , compound-identifier , ( "as" , IDENTIFIER ) ;

compound-identifier = IDENTIFIER ( "." , IDENTIFIER )* ;

declaration-list = ( declaration , ";" )* ;

declaration = primitive-declaration | enum-declaration ;

primitive-declaration = ( "extend" ) , type , compound-identifier ,
                        ( "{" primitive-value-list "}" ) ;

type = "uint" | "string" | "bool";

primitive-value-list = ( IDENTIFIER , "=" , literal , "," )* ;

enum-declaration = ( "extend" ) , "enum" , compound-identifier ,
                   ( "{" , enum-value-list , "}" ) ;

enum-value-list = ( IDENTIFIER , "," )* ;

literal = STRING-LITERAL | NUMERIC-LITERAL | "true" | "false" ;

An identifier matches the regex [a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])? and must not match any keyword. The list of keywords is:

as
bool
enum
extend
library
string
uint
using

A string literal matches the regex ”[^”]*”, and a numeric literal matches the regex [0-9]+ or 0x[0-9A-F]+.

The bind compiler will ignore (treat as whitespace) any line prefixed by //, and any multiple lines delimited by /* and */.

Build targets

To declare a bind library within the Fuchsia build system, use the following build target:

bind_library(<library name>) {
  source = <bind library filename>
  public_deps = [ <list of bind library targets> ]
}

For more details, refer to //build/bind/bind.gni.

Debugger

The debugger can be used to run a bind program against a particular device. It outputs a trace of the bind program's execution, describing why the driver would or would not bind to the device.

You can run the debugger in the following ways:

  • As a host tool. You provide the bind program source file and a file listing the properties of the device. This is useful during bind program development for testing the outcome of a program against different combinations of device properties.
  • On the target device. You specify the driver path and the device path within the system. This is useful for figuring out why a driver did or did not bind to a particular device.

Note: The debugger can only be used with bind programs written in the bind language described in this page.

Running the debugger as a host tool

You can run the debugger with the --debug option in the bind compiler.

fx bindc \
  --include src/devices/bind/deprecated.usb/deprecated.usb.bind \
  --debug tools/bindc/examples/gizmo.dev \
  tools/bindc/examples/gizmo.bind

The bind program source and the library sources are in the formats described in the bind rules and bind libraries sections, respectively. The --debug option takes a file containing a specification of the device to run the bind program against.

Note: The --debug and --output options are mutually exclusive, so the C header file will not be generated when running the compiler in debug mode.

Device specification file

The debugger takes a file specifying the device to run the bind program against. This specification is simply a list of key-value pairs describing the properties of the device.

Example

This example device specification can be found at //tools/bindc/examples/gizmo.dev.

deprecated.BIND_PROTOCOL = deprecated.usb.BIND_PROTOCOL.DEVICE
deprecated.BIND_USB_VID = deprecated.usb.BIND_USB_VID.REALTEK
deprecated.BIND_USB_CLASS = deprecated.usb.BIND_USB_CLASS.VIDEO
deprecated.BIND_USB_SUBCLASS = deprecated.usb.BIND_USB_SUBCLASS.VIDEO_CONTROL
Grammar
device-specification = ( property )* ;

property = compound-identifier , "=" , value ;

compound-identifier = IDENTIFIER ( "." , IDENTIFIER )* ;

value = compound-identifier | STRING-LITERAL | NUMERIC-LITERAL | "true" | "false" ;

An identifier matches the regex [a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])?.

A string literal matches the regex ”[^”]*”, and a numeric literal matches the regex [0-9]+ or 0x[0-9A-F]+.

The bind compiler will ignore (treat as whitespace) any line prefixed by //, and any multiple lines delimited by /* and */.

Running the debugger on the target device

The debugger is run using its package URL. For example:

fx run fuchsia-pkg://fuchsia.com/bind_debugger#meta/bind_debugger.cmx \
    /system/driver/bt-hci-intel.so \
    class/bt-transport/000

The command takes the path of the driver to debug and the path of the device to debug it against.

Device path

There are two ways to specify the device:

  • Its path within /dev/class, e.g. class/bt-transport/000.
  • Its topological path, e.g. sys/pci/00:14.0/xhci/usb-bus/003/003/ifc-000/bt_transport_usb.

Both of the paths are relative to /dev/.

The topological path can be determined from the ouptut of dm dump. For example, tracing the path to the node [bt_transport_usb] in the output below gives the topological path sys/pci/00:14.0/xhci/usb-bus/003/003/ifc-000/bt_transport_usb.

[root]
   <root> pid=3456
      [null] pid=3456 /boot/driver/builtin.so
      [zero] pid=3456 /boot/driver/builtin.so
   [misc]
      <misc> pid=3525
         [demo-fifo] pid=3525 /boot/driver/demo-fifo.so
         [ktrace] pid=3525 /boot/driver/ktrace.so
   [sys]
      <sys> pid=3369 /boot/driver/platform-bus.so
         [pci] pid=3369 /boot/driver/platform-bus-x86.so
            [00:00.0] pid=3369 /boot/driver/bus-pci.so
            [00:14.0] pid=3369 /boot/driver/bus-pci.so
               <00:14.0> pid=4384 /boot/driver/bus-pci.proxy.so
                  [xhci] pid=4384 /boot/driver/xhci.so
                     [xdc] pid=4384 /boot/driver/xhci.so
                     [usb-bus] pid=4384 /boot/driver/usb-bus.so
                        [001] pid=4384 /boot/driver/usb-bus.so
                           [001] pid=4384 /boot/driver/usb-composite.so
                              [ifc-000] pid=4384 /boot/driver/usb-composite.so
                                 [usb-hid] pid=4384 /boot/driver/usb-hid.so
                                    [hid-device-000] pid=4384 /boot/driver/hid.so
                        [002] pid=4384 /boot/driver/usb-bus.so
                           [002] pid=4384 /boot/driver/usb-composite.so
                              [ifc-000] pid=4384 /boot/driver/usb-composite.so
                                 [usb-hid] pid=4384 /boot/driver/usb-hid.so
                                    [hid-device-000] pid=4384 /boot/driver/hid.so
                        [003] pid=4384 /boot/driver/usb-bus.so
                           [003] pid=4384 /boot/driver/usb-composite.so
                              [ifc-000] pid=4384 /boot/driver/usb-composite.so
                                 [bt_transport_usb] pid=4384 /boot/driver/bt-transport-usb.so
                                    [bt_hci_intel] pid=4384 /system/driver/bt-hci-intel.so
                                       [bt_host] pid=4384 /system/driver/bt-host.so

Debugger output

The output of the debugger is a trace of the bind program's execution. The trace contains information about whether each statement in the bind program succeeded, and why or why not. For example, if a condition statement failed because the device did not have the required value, the debugger will output what the actual value of the device was (or the fact that the device had no value for that property). The trace also includes information about which branches were taken in if statements.

Example

The output of the debugger when running the host tool command above is:

Line 4: Condition statement succeeded: deprecated.BIND_PROTOCOL == deprecated.usb.BIND_PROTOCOL.DEVICE;
Line 6: If statement condition failed: deprecated.BIND_USB_VID == deprecated.usb.BIND_USB_VID.INTEL
    Actual value of `deprecated.BIND_USB_VID` was `deprecated.usb.BIND_USB_VID.REALTEK` [0xbda].
Line 9: If statement condition succeeded: deprecated.BIND_USB_VID == deprecated.usb.BIND_USB_VID.REALTEK
Line 11: Accept statement succeeded.
    Value of `deprecated.BIND_USB_CLASS` was `deprecated.usb.BIND_USB_CLASS.VIDEO` [0xe].
Driver binds to device.

If you run the debugger on the Fuchsia target device, you will see similar output information. However, information such as identifiers and source code snippets may be missing, since the system only stores the bind program bytecode, not the source code.

The trace shows the outcome of each statement which was reached while executing the bind program:

  • The device has the USB device protocol, so the first condition statement is satisfied.
  • The device's vendor ID is REALTEK, so the second branch of the if statement is taken.
  • The device has one of the two accepted classes (video), so the accept statement is satisfied.

The debugger outputs that the driver would successfully bind to a device with these properties.