| # 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](/docs/concepts/drivers/overview.md) 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 |
| ([ddk/binding.h](/src/lib/ddk/include/ddk/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 {#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](/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: |
| |
| ```gn |
| 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](/build/bind/bind.gni). |
| |
| ## Testing |
| The bind compiler supports a data-driven unit test framework for bind rules that allows you to |
| test your bind rules in isolation from the driver. A test case for a bind program consists of a |
| device specification and an expected result, i.e. bind or abort. Test cases are passed to the bind |
| compiler in the form of JSON specification files and the compiler executes each test case by |
| running the debugger. |
| |
| The JSON specification must be a list of test case objects, where each object contains: |
| |
| - `name` A string for the name of the test case. |
| - `expected` The expected result. Must be `“match”` or `“abort”`. |
| - `device` A list of string key value pairs describing the properties of a device. This is |
| similar to the debugger's [device specifications](#device-specification). |
| |
| ### Example |
| |
| This is an example test case, the full set of tests is at `//tools/bindc/examples/test.json`. This |
| case checks that the bind rules match a device with the listed properties, i.e. an Intel USB audio |
| device. |
| |
| ``` |
| [ |
| { |
| "name": "Intel", |
| "expected": "match", |
| "device": { |
| "deprecated.BIND_PROTOCOL": "deprecated.usb.BIND_PROTOCOL.DEVICE", |
| "deprecated.BIND_USB_VID": "deprecated.usb.BIND_USB_VID.INTEL", |
| "deprecated.BIND_USB_CLASS": "deprecated.usb.BIND_USB_CLASS.AUDIO" |
| } |
| } |
| ] |
| ``` |
| |
| ### Build |
| |
| Define a test build target like so |
| |
| ``` |
| bind_test("bind_test") { |
| rules = <bind rules filename> |
| tests = <test specification filename> |
| deps = [ <list of bind library targets> ] |
| } |
| ``` |
| |
| Alternatively, you can simply add a `tests` argument to your existing `bind_rules` to generate a |
| test target. It’s name will be the original target’s name plus `_test`. For example, the following |
| would generate `example_bind_test`. |
| |
| ``` |
| bind_rules("example_test") { |
| rules = "gizmo.bind" |
| output = “gizmo_bind.h” |
| tests = "tests.json" |
| deps = [ "//src/devices/bind/deprecated.usb" ] |
| } |
| ``` |
| |
| ### Run |
| |
| If you have defined a build target for your test then you can run the tests as usual with fx test. |
| |
| ``` |
| fx test example_bind_test |
| ``` |
| |
| Otherwise you can run the bind tool directly. For example: |
| |
| ``` |
| fx bindc test \ |
| tools/bindc/examples/gizmo.bind \ |
| --test-spec tools/bindc/examples/tests.json \ |
| --include src/devices/bind/deprecated.usb/deprecated.usb.bind |
| ``` |
| |
| ## Bind libraries {#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: |
| |
| ```gn |
| bind_library(<library name>) { |
| source = <bind library filename> |
| public_deps = [ <list of bind library targets> ] |
| } |
| ``` |
| |
| For more details, refer to [//build/bind/bind.gni](/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.](#running-the-debugger-host)** 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.](#running-the-debugger-target)** 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 {#running-the-debugger-host} |
| |
| 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](#bind-rules) and [bind libraries](#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 {#device-specification} |
| |
| 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](/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 {#running-the-debugger-target} |
| |
| 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](#running-the-debugger-host) 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. |