doc: write documentation about DT-based device instantiation

Add more HOWTO information for the two current devicetree-based device
instantiation styles, and a bit more information on how to create
devices that depend on others.

Point to this from the Kconfig tips page, since it is meant as a
replacement for existing Kconfig practice.

Update macros.bnf.

Signed-off-by: Martí Bolívar <marti.bolivar@nordicsemi.no>
This commit is contained in:
Martí Bolívar 2020-05-01 10:54:26 -07:00 committed by Kumar Gala
commit 6f35d3bd16
4 changed files with 293 additions and 87 deletions

View file

@ -104,8 +104,7 @@ a node identifier and pass it to ``DT_LABEL`` to get the right string to pass
to ``device_get_binding()``.
If you're having trouble, see :ref:`dt-trouble`. The first thing to check is
that the node is enabled (``status = "okay"``) and has a matching binding, like
this:
that the node is has ``status = "okay"`` and has a matching binding, like this:
.. code-block:: c
@ -114,11 +113,11 @@ this:
#if DT_HAS_NODE_STATUS_OKAY(MY_SERIAL)
struct device *uart_dev = device_get_binding(DT_LABEL(MY_SERIAL));
#else
#error "Node is disabled or has no matching binding"
#error "Node is disabled, has no matching binding, or initialization failed"
#endif
If you see the ``#error`` output, something is wrong with either your
devicetree or bindings.
If you see the ``#error`` output, either something is wrong with either your
devicetree or bindings, or the device's initialization function failed.
.. _dts-find-binding:
@ -307,104 +306,263 @@ Assuming you have a suitable device driver associated with the
enable the driver via Kconfig and :ref:`get the struct device <dt-get-device>`
for your newly added bus node, then use it with that driver API.
.. _dt-driver-howto:
.. _dt-create-devices:
Create struct devices in a driver
*********************************
Write device drivers using devicetree APIs
******************************************
If you're writing a device driver, it should be devicetree aware so that
applications can configure it and access devices as described above. In short,
you must create a ``struct device`` for every enabled instance of the
compatible that the device driver supports, and set each device's name to the
``DT_LABEL()`` of its devicetree node.
"Devicetree-aware" :ref:`device drivers <device_model_api>` should create a
``struct device`` for each ``status = "okay"`` devicetree node with a
particular :ref:`compatible <dt-important-props>` (or related set of
compatibles) supported by the driver.
The :file:`devicetree.h` API has helpers for writing device drivers based on
:ref:`DT_INST node identifiers <dt-node-identifiers>` for each of the possible
instance numbers on your SoC.
.. note::
Assuming you're using instances, start by defining ``DT_DRV_COMPAT`` at the top
of the file to the lowercase-and-underscores version of the :ref:`compatible
<dt-important-props>` that the device driver is handling. For example, if your
driver is handling nodes with compatible ``"vnd,my-device"``, you should put
this at the top of your driver:
Historically, Zephyr has used Kconfig options like :option:`CONFIG_SPI_0` and
:option:`CONFIG_I2C_1` to enable driver support for individual devices of
some type. For example, if ``CONFIG_I2C_1=y``, the SoC's I2C peripheral
driver would create a ``struct device`` for "I2C bus controller number 1".
This style predates support for devicetree in Zephyr and its use is now
discouraged. Existing device drivers may be made "devicetree-aware"
in future releases.
Writing a devicetree-aware driver begins by defining a :ref:`devicetree binding
<dt-bindings>` for the devices supported by the driver. Use existing bindings
from similar drivers as a starting point. A skeletal binding to get started
needs nothing more than this:
.. code-block:: yaml
description: <Human-readable description of your binding>
compatible: "foo-company,bar-device"
include: base.yaml
See :ref:`dts-find-binding` for more advice on locating existing bindings.
After writing your binding, your driver C file can then use the devicetree API
to find ``status = "okay"`` nodes with the desired compatible, and instantiate
a ``struct device`` for each one. There are two options for instantiating each
``struct device``: using instance numbers, and using node labels.
In either case:
- Each ``struct device``\ 's name should be set to its devicetree node's
``label`` property. This allows the driver's users to :ref:`dt-get-device` in
the usual way.
- Each device's initial configuration should use values from devicetree
properties whenever practical. This allows users to configure the driver
using :ref:`devicetree overlays <use-dt-overlays>`.
Examples for how to do this follow. They assume you've already implemented the
device-specific configuration and data structures and API functions, like this:
.. code-block:: c
/* my_driver.c */
#include <drivers/some_api.h>
/* Define data (RAM) and configuration (ROM) structures: */
struct my_dev_data {
/* per-device values to store in RAM */
};
struct my_dev_cfg {
u32_t freq; /* Just an example: initial clock frequency in Hz */
/* other configuration to store in ROM */
};
/* Implement driver API functions (drivers/some_api.h callbacks): */
static int my_driver_api_func1(struct device *dev, u32_t *foo) { /* ... */ }
static int my_driver_api_func2(struct device *dev, u64_t bar) { /* ... */ }
static struct some_api my_api_funcs = {
.func1 = my_driver_api_func1,
.func2 = my_driver_api_func2,
};
.. _dt-create-devices-inst:
Option 1: create devices using instance numbers
===============================================
Use this option, which uses :ref:`devicetree-inst-apis`, if possible. However,
they only work when devicetree nodes for your driver's ``compatible`` are all
equivalent, and you do not need to be able to distinguish between them.
To use instance-based APIs, begin by defining ``DT_DRV_COMPAT`` to the
lowercase-and-underscores version of the compatible that the device driver
supports. For example, if your driver's compatible is ``"vnd,my-device"`` in
devicetree, you would define ``DT_DRV_COMPAT`` to ``vnd_my_device`` in your
driver C file:
.. code-block:: c
/*
* Put this near the top of the file. After the includes is a good place.
* (Note that you can therefore run "git grep DT_DRV_COMPAT drivers" in
* the zephyr Git repository to look for example drivers using this style).
*/
#define DT_DRV_COMPAT vnd_my_device
.. important::
The DT_DRV_COMPAT macro should have neither quotes nor special characters.
Remove quotes and convert special characters to underscores.
As shown, the DT_DRV_COMPAT macro should have neither quotes nor special
characters. Remove quotes and convert special characters to underscores
when creating ``DT_DRV_COMPAT`` from the compatible property.
The typical pattern after that is to define the API functions, then define a
macro which creates the device by instance number, and then call it for each
enabled instance. Currently, this looks like this:
Finally, define an instantiation macro, which creates each ``struct device``
using instance numbers. Do this after defining ``my_api_funcs``.
.. code-block:: c
#include <drivers/some_api.h>
#include <devicetree.h>
#define DT_DRV_COMPAT vnd_my_device
/*
* Define RAM and ROM structures:
* This instantiation macro is named "CREATE_MY_DEVICE".
* Its "inst" argument is an arbitrary instance number.
*
* Put this near the end of the file, e.g. after defining "my_api_funcs".
*/
struct my_dev_data {
/* per-device values to store in RAM */
};
struct my_dev_cfg {
u32_t freq; /* Just an example: clock frequency in Hz */
/* other device configuration to store in ROM */
};
/*
* Implement some_api.h callbacks:
*/
struct some_api my_api_funcs = { /* ... */ };
/*
* Now use DT_INST APIs to create a struct device for each enabled node:
*/
#define CREATE_MY_DEVICE(inst) \
static struct my_dev_data my_dev_data_##inst = { \
/* initialize RAM values as needed */ \
}; \
static const struct my_dev_cfg my_dev_cfg_##inst = { \
/* initialize ROM values, usually from devicetree */ \
static struct my_dev_data my_data_##inst = { \
/* initialize RAM values as needed, e.g.: */ \
.freq = DT_INST_PROP(inst, clock_frequency), \
/* ... */ \
}; \
static const struct my_dev_cfg my_cfg_##inst = { \
/* initialize ROM values as needed. */ \
}; \
DEVICE_AND_API_INIT(my_dev_##inst, \
DT_INST_LABEL(inst), \
my_dev_init_function, \
&my_dev_data_##inst, \
&my_dev_cfg_##inst, \
&my_data_##inst, \
&my_cfg_##inst, \
MY_DEV_INIT_LEVEL, MY_DEV_INIT_PRIORITY, \
&my_api_funcs);
/* Call the device creation macro for every compatible node: */
Notice the use of APIs like :c:func:`DT_INST_LABEL` and :c:func:`DT_INST_PROP`
to access devicetree node data. These APIs retrieve data from the devicetree
for instance number ``inst`` of the node with compatible determined by
``DT_DRV_COMPAT``.
Finally, pass the instantiation macro to :c:func:`DT_INST_FOREACH_STATUS_OKAY`:
.. code-block:: c
/* Call the device creation macro for each instance: */
DT_INST_FOREACH_STATUS_OKAY(CREATE_MY_DEVICE)
Notice the use of :c:func:`DT_INST_PROP` and :c:func:`DT_INST_FOREACH`.
These are helpers which rely on ``DT_DRV_COMPAT`` to choose devicetree nodes
of a chosen compatible at a given index.
``DT_INST_FOREACH_STATUS_OKAY`` expands to code which calls
``CREATE_MY_DEVICE`` once for each enabled node with the compatible determined
by ``DT_DRV_COMPAT``. It does not append a semicolon to the end of the
expansion of ``CREATE_MY_DEVICE``, so the macro's expansion must end in a
semicolon or function definition to support multiple devices.
As shown above, the driver uses additional information from
:file:`devicetree.h` to create :ref:`struct device <device_struct>` instances
than just the node label. Devicetree property values used to configure the
device at boot time are stored in ROM in the value pointed to by a
``device->config_info`` field. This allows users to configure your
driver using overlays.
Option 2: create devices using node labels
==========================================
The Zephyr convention is to name each ``struct device`` using its devicetree
node's ``label`` property using ``DT_INST_LABEL()``. This allows applications
to :ref:`dt-get-device`.
Some device drivers cannot use instance numbers. One example is an SoC
peripheral driver which relies on vendor HAL APIs specialized for individual IP
blocks to implement Zephyr driver callbacks. Cases like this should use
:c:func:`DT_NODELABEL` to refer to individual nodes in the devicetree
representing the supported peripherals on the SoC. The devicetree.h
:ref:`devicetree-generic-apis` can then be used to access node data.
For this to work, your :ref:`SoC's dtsi file <dt-input-files>` must define node
labels like ``mydevice0``, ``mydevice1``, etc. appropriately for the IP blocks
your driver supports. The resulting devicetree usually looks something like
this:
.. code-block:: DTS
/ {
soc {
mydevice0: dev@... {
compatible = "vnd,my-device";
};
mydevice1: dev@... {
compatible = "vnd,my-device";
};
};
};
The driver can use the ``mydevice0`` and ``mydevice1`` node labels in the
devicetree to operate on specific device nodes:
.. code-block:: c
/*
* This is a convenience macro for creating a node identifier for
* the relevant devices. An example use is MYDEV(0) to refer to
* the node with label "mydevice0".
*/
#define MYDEV(idx) DT_NODELABEL(mydevice ## idx)
/*
* Define your instantiation macro; "idx" is a number like 0 for mydevice0
* or 1 for mydevice1. It uses MYDEV() to create the node label from the
* index.
*/
#define CREATE_MY_DEVICE(idx) \
static struct my_dev_data my_data_##idx = { \
/* initialize RAM values as needed, e.g.: */ \
.freq = DT_PROP(MYDEV(idx), clock_frequency), \
}; \
static const struct my_dev_cfg my_cfg_##idx = { /* ... */ }; \
DEVICE_AND_API_INIT(my_dev_##idx, \
DT_LABEL(MYDEV(idx)), \
my_dev_init_function, \
&my_data_##idx, \
&my_cfg_##idx, \
MY_DEV_INIT_LEVEL, MY_DEV_INIT_PRIORITY, \
&my_api_funcs)
Notice the use of APIs like :c:func:`DT_LABEL` and :c:func:`DT_PROP` to access
devicetree node data.
Finally, manually detect each enabled devicetree node and use
``CREATE_MY_DEVICE`` to instantiate each ``struct device``:
.. code-block:: c
#if DT_HAS_NODE_STATUS_OKAY(DT_NODELABEL(mydevice0))
CREATE_MY_DEVICE(0)
#endif
#if DT_HAS_NODE_STATUS_OKAY(DT_NODELABEL(mydevice1))
CREATE_MY_DEVICE(1)
#endif
Since this style does not use ``DT_INST_FOREACH_STATUS_OKAY()``, the driver
author is responsible for calling ``CREATE_MY_DEVICE()`` for every possible
node, e.g. using knowledge about the peripherals available on supported SoCs.
.. _dt-drivers-that-depend:
Device drivers that depend on other devices
*******************************************
At times, one ``struct device`` depends on another ``struct device`` and
requires a pointer to it. For example, a sensor device might need a pointer to
its SPI bus controller device. Some advice:
- Write your devicetree binding in a way that permits use of
:ref:`devicetree-hw-api` from devicetree.h if possible.
- In particular, for bus devices, your driver's binding should include a
file like :zephyr_file:`dts/bindings/spi/spi-device.yaml` which provides
common definitions for devices addressable via a specific bus. This enables
use of APIs like :c:func:`DT_BUS` to obtain a node identifier for the bus
node. You can then :ref:`dt-get-device` for the bus in the usual way.
Search existing bindings and device drivers for examples.
.. _dt-apps-that-depend:
Applications that depend on board-specific devices
**************************************************
One way to allow application code to run unmodified on multiple boards is by
supporting a devicetree alias to specify the hardware specific portions, as is
done in the :ref:`blinky-sample`. The application can then be configured in
:ref:`BOARD.dts <devicetree-in-out-files>` files or via :ref:`devicetree
overlays <use-dt-overlays>`.
.. _dt-trouble:
@ -537,4 +695,4 @@ Errors with DT_INST_() APIs
If you're using an API like :c:func:`DT_INST_PROP`, you must define
``DT_DRV_COMPAT`` to the lowercase-and-underscores version of the compatible
you are interested in. See :ref:`dt-driver-howto`.
you are interested in. See :ref:`dt-create-devices-inst`.

View file

@ -46,6 +46,8 @@ node-macro =/ %s"DT_N" path-id %s"_PARENT"
; These are used internally by DT_FOREACH_CHILD, which iterates over
; each child node.
node-macro =/ %s"DT_N" path-id %s"_FOREACH_CHILD"
; The node's status macro
node-macro =/ %s"DT_N" path-id %s"_STATUS_" dt-name
; --------------------------------------------------------------------
; property-macro: a macro related to a node property
@ -132,11 +134,10 @@ prop-suf = 1*( "_" gen-name ["_" dt-name] )
; See examples below.
other-macro = %s"DT_N_" alternate-id
; Total count of enabled instances of a compatible.
other-macro =/ %s"DT_N_INST_" dt-name %s"_NUM"
other-macro =/ %s"DT_N_INST_" dt-name %s"_NUM_OKAY"
; These are used internally by DT_INST_FOREACH, which iterates over
; each enabled instance of a compatible.
other-macro =/ %s"DT_FOREACH_INST_" dt-name
other-macro =/ %s"DT_FOREACH_IMPL_" DIGIT
other-macro =/ %s"DT_FOREACH_OKAY_INST_" dt-name
; E.g.: #define DT_CHOSEN_zephyr_flash
other-macro =/ %s"DT_CHOSEN_" dt-name
; Declares that a compatible has at least one node on a bus.
@ -145,6 +146,8 @@ other-macro =/ %s"DT_CHOSEN_" dt-name
;
; #define DT_COMPAT_vnd_dev_BUS_spi 1
other-macro =/ %s"DT_COMPAT_" dt-name %s"_BUS_" dt-name
; #define DT_COMPAT_HAS_OKAY_vnd_dev 1
other-macro =/ %s"DT_COMPAT_HAS_OKAY_" dt-name
; --------------------------------------------------------------------
; alternate-id: another way to specify a node besides a path-id

View file

@ -37,11 +37,6 @@ Symbols without prompts are called *hidden* or *invisible* symbols, because
they don't show up in ``menuconfig`` and ``guiconfig``. Symbols that have
prompts can also be invisible, when their dependencies are not satisfied.
In Zephyr, Kconfig configuration is done after selecting a machine, so in
general, it does not make sense to put a prompt on a symbol that corresponds to
a fixed machine-specific setting. Usually, such settings should be handled via
devicetree (``.dts``) files instead.
Symbols without prompts can't be configured directly by the user (they derive
their value from other symbols), so less restrictions apply to them. If some
derived setting is easier to calculate in Kconfig than e.g. during the build,
@ -51,6 +46,52 @@ without prompts in mind.
See the `optional prompts`_ section for a way to deal with settings that are
fixed on some machines and configurable on other machines.
What not to turn into Kconfig options
*************************************
In Zephyr, Kconfig configuration is done after selecting a target board. In
general, it does not make sense to use Kconfig for a value that corresponds to
a fixed machine-specific setting. Usually, such settings should be handled via
:ref:`devicetree <dt-guide>` instead.
In particular, avoid adding new Kconfig options of the following types:
Options enabling individual devices
===================================
Existing examples like :option:`CONFIG_SPI_0` and :option:`CONFIG_I2C_1` were
introduced before Zephyr supported devicetree, and new cases are discouraged.
See :ref:`dt-create-devices` for details on how to do this with devicetree
instead.
Options that specify a device in the system by name
===================================================
For example, if you are writing an I2C device driver, avoid creating an option
named ``MY_DEVICE_I2C_BUS_NAME`` for specifying the bus node your device is
controlled by. See :ref:`dt-drivers-that-depend` for alternatives.
Similarly, if your application depends on a hardware-specific PWM device to
control an RGB LED, avoid creating an option like ``MY_PWM_DEVICE_NAME``. See
:ref:`dt-apps-that-depend` for alternatives.
Options that specify fixed hardware configuration
=================================================
For example, avoid Kconfig options specifying a GPIO pin.
An alternative applicable to device drivers is to define a GPIO specifier with
type phandle-array in the device binding, and using the
:ref:`devicetree-gpio-api` devicetree API from C. Similar advice applies to
other cases where devicetree.h provides :ref:`devicetree-hw-api` for referring
to other nodes in the system. Search the source code for drivers using these
APIs for examples.
An application-specific devicetree :ref:`binding <dt-bindings>` to identify
board specific properties may be appropriate. See
:zephyr_file:`tests/drivers/gpio/gpio_basic_api` for an example.
For applications, see :ref:`blinky-sample` for a devicetree-based alternative.
``select`` statements
*********************

View file

@ -11,6 +11,8 @@ Some of these require a special macro named ``DT_DRV_COMPAT`` to be defined
before they can be used; these are discussed individually below. These macros
are generally meant for use within device drivers.
.. _devicetree-generic-apis:
Generic APIs
************
@ -85,6 +87,8 @@ Clocks
.. doxygengroup:: devicetree-clocks
:project: Zephyr
.. _devicetree-gpio-api:
GPIO
====