Part 1
요약·해설
공식 코드 26개와 device readiness 진단, overlay 자동 검색·우선순위, instance·node-label driver 생성 방식을 보존했습니다.
zephyr.dts · header
GET · ready
검색 · 우선순위
instance · label
Part 2
접을 수 있는 영어 원문 전체
영어 원문 전체 펼치기
EE93EF0DCC4274084558C0F8151AC3DF293DA5AB15B4364F98F3697907502545
.. _dt-howtos:
Devicetree HOWTOs
#################
This page has step-by-step advice for getting things done with devicetree.
.. tip:: See :ref:`dt-trouble` for troubleshooting advice.
.. _get-devicetree-outputs:
Get your devicetree and generated header
****************************************
A board's devicetree (:ref:`BOARD.dts <devicetree-in-out-files>`) pulls in
common node definitions via ``#include`` preprocessor directives. This at least
includes the SoC's ``.dtsi``. One way to figure out the devicetree's contents
is by opening these files, e.g. by looking in
``dts/<ARCH>/<vendor>/<soc>.dtsi``, but this can be time consuming.
If you just want to see the "final" devicetree for your board, build an
application and open the :file:`zephyr.dts` file in the build directory.
.. tip::
You can build :ref:`hello_world` to see the "base" devicetree for your board
without any additional changes from :ref:`overlay files <dt-input-files>`.
For example, using the :ref:`qemu_cortex_m3` board to build :ref:`hello_world`:
.. code-block:: sh
# --cmake-only here just forces CMake to run, skipping the
# build process to save time.
west build -b qemu_cortex_m3 samples/hello_world --cmake-only
You can change ``qemu_cortex_m3`` to match your board.
CMake prints the input and output file locations like this:
.. code-block:: none
-- Found BOARD.dts: .../zephyr/boards/arm/qemu_cortex_m3/qemu_cortex_m3.dts
-- Generated zephyr.dts: .../zephyr/build/zephyr/zephyr.dts
-- Generated devicetree_generated.h: .../zephyr/build/zephyr/include/generated/zephyr/devicetree_generated.h
The :file:`zephyr.dts` file is the final devicetree in DTS format.
The :file:`devicetree_generated.h` file is the corresponding generated header.
See :ref:`devicetree-in-out-files` for details about these files.
.. _dt-get-device:
Get a struct device from a devicetree node
******************************************
When writing Zephyr applications, you'll often want to get a driver-level
:ref:`struct device <device_model_api>` corresponding to a devicetree node.
For example, with this devicetree fragment, you might want the struct device
for ``serial@40002000``:
.. code-block:: devicetree
/ {
soc {
serial0: serial@40002000 {
status = "okay";
current-speed = <115200>;
/* ... */
};
};
aliases {
my-serial = &serial0;
};
chosen {
zephyr,console = &serial0;
};
};
Start by making a :ref:`node identifier <dt-node-identifiers>` for the device
you are interested in. There are different ways to do this; pick whichever one
works best for your requirements. Here are some examples:
.. code-block:: c
/* Option 1: by node label */
#define MY_SERIAL DT_NODELABEL(serial0)
/* Option 2: by alias */
#define MY_SERIAL DT_ALIAS(my_serial)
/* Option 3: by chosen node */
#define MY_SERIAL DT_CHOSEN(zephyr_console)
/* Option 4: by path */
#define MY_SERIAL DT_PATH(soc, serial_40002000)
Once you have a node identifier there are two ways to proceed. One way to get a
device is to use :c:func:`DEVICE_DT_GET`:
.. code-block:: c
const struct device *const uart_dev = DEVICE_DT_GET(MY_SERIAL);
if (!device_is_ready(uart_dev)) {
/* Not ready, do not use */
return -ENODEV;
}
There are variants of :c:func:`DEVICE_DT_GET` such as
:c:func:`DEVICE_DT_GET_OR_NULL`, :c:func:`DEVICE_DT_GET_ONE` or
:c:func:`DEVICE_DT_GET_ANY`. This idiom fetches the device pointer at
build-time, which means there is no runtime penalty. This method is useful if
you want to store the device pointer as configuration data. But because the
device may not be initialized, or may have failed to initialize, you must verify
that the device is ready to be used before passing it to any API functions.
(This check is done for you by :c:func:`device_get_binding`.)
In some situations the device cannot be known at build-time, e.g., if it depends
on user input like in a shell application. In this case you can get the
``struct device`` by combining :c:func:`device_get_binding` with the device
name:
.. code-block:: c
const char *dev_name = /* TODO: insert device name from user */;
const struct device *uart_dev = device_get_binding(dev_name);
You can then use ``uart_dev`` with :ref:`uart_api` API functions like
:c:func:`uart_configure`. Similar code will work for other device types; just
make sure you use the correct API for the device.
If you're having trouble, see :ref:`dt-trouble`. The first thing to check is
that the node has ``status = "okay"``, like this:
.. code-block:: c
#define MY_SERIAL DT_NODELABEL(my_serial)
#if DT_NODE_HAS_STATUS(MY_SERIAL, okay)
const struct device *const uart_dev = DEVICE_DT_GET(MY_SERIAL);
#else
#error "Node is disabled"
#endif
If you see the ``#error`` output, make sure to enable the node in your
devicetree. In some situations your code will compile but it will fail to link
with a message similar to:
.. code-block:: none
...undefined reference to `__device_dts_ord_N'
collect2: error: ld returned 1 exit status
This likely means there's a Kconfig issue preventing the device driver from
being built, resulting in a reference that does not exist. If your code compiles
successfully, the last thing to check is if the device is ready, like this:
.. code-block:: c
if (!device_is_ready(uart_dev)) {
printk("Device not ready\n");
}
If you find that the device is not ready, it likely means that the device's
initialization function failed. Enabling logging or debugging driver code may
help in such situations. Note that you can also use :c:func:`device_get_binding`
to obtain a reference at runtime. If it returns ``NULL`` it can either mean that
device's driver failed to initialize or that it does not exist.
.. _dts-find-binding:
Find a devicetree binding
*************************
:ref:`dt-bindings` are YAML files which declare what you can do with the nodes
they describe, so it's critical to be able to find them for the nodes you are
using.
If you don't have them already, :ref:`get-devicetree-outputs`. To find a node's
binding, open the generated header file, which starts with a list of nodes in a
block comment:
.. code-block:: c
/*
* [...]
* Nodes in dependency order (ordinal and path):
* 0 /
* 1 /aliases
* 2 /chosen
* 3 /flash@0
* 4 /memory@20000000
* (etc.)
* [...]
*/
Make note of the path to the node you want to find, like ``/flash@0``. Search
for the node's output in the file, which starts with something like this if the
node has a matching binding:
.. code-block:: c
/*
* Devicetree node:
* /flash@0
*
* Binding (compatible = soc-nv-flash):
* $ZEPHYR_BASE/dts/bindings/mtd/soc-nv-flash.yaml
* [...]
*/
See :ref:`missing-dt-binding` for troubleshooting.
.. _set-devicetree-overlays:
Set devicetree overlays
***********************
Devicetree overlays are explained in :ref:`devicetree-intro`. The CMake
variable :makevar:`DTC_OVERLAY_FILE` contains a space- or semicolon-separated
list of overlay files to use. If :makevar:`DTC_OVERLAY_FILE` specifies multiple
files, they are included in that order by the C preprocessor. A file in a
Zephyr module can be referred to by escaping the Zephyr module dir variable
like ``\${ZEPHYR_<module>_MODULE_DIR}/<path-to>/dts.overlay``
when setting the DTC_OVERLAY_FILE variable.
You can set :makevar:`DTC_OVERLAY_FILE` to contain exactly the files you want
to use. Here is an :ref:`example <west-building-dtc-overlay-file>` using
``west build``.
If you don't set :makevar:`DTC_OVERLAY_FILE`, the build system will follow
these steps, looking for files in your application configuration directory to
use as devicetree overlays:
#. If the file :file:`socs/<SOC>_<BOARD_QUALIFIERS>.overlay` exists, it will be used.
#. If the file :file:`boards/<BOARD>.overlay` exists, it will be used in addition to the above.
#. If the current board has :ref:`multiple revisions <porting_board_revisions>`
and :file:`boards/<BOARD>_<revision>.overlay` exists, it will be used in addition to the above.
#. If one or more files have been found in the previous steps, the build system
stops looking and just uses those files.
#. Otherwise, if :file:`<BOARD>.overlay` exists, it will be used, and the build
system will stop looking for more files.
#. Otherwise, if :file:`app.overlay` exists, it will be used.
Extra devicetree overlays may be provided using ``EXTRA_DTC_OVERLAY_FILE`` which
will still allow the build system to automatically use devicetree overlays
described in the above steps.
The build system appends overlays specified in ``EXTRA_DTC_OVERLAY_FILE``
to the overlays in ``DTC_OVERLAY_FILE`` when processing devicetree overlays.
This means that changes made via ``EXTRA_DTC_OVERLAY_FILE`` have higher
precedence than those made via ``DTC_OVERLAY_FILE``.
All configuration files will be taken from the application's configuration
directory except for files with an absolute path that are given with the
``DTC_OVERLAY_FILE`` or ``EXTRA_DTC_OVERLAY_FILE`` argument.
See :ref:`Application Configuration Directory <application-configuration-directory>`
on how the application configuration directory is defined.
Using :ref:`shields` will also add devicetree overlay files.
The :makevar:`DTC_OVERLAY_FILE` value is stored in the CMake cache and used
in successive builds.
The :ref:`build system <build_overview>` prints all the devicetree overlays it
finds in the configuration phase, like this:
.. code-block:: none
-- Found devicetree overlay: .../some/file.overlay
.. _use-dt-overlays:
Use devicetree overlays
***********************
See :ref:`set-devicetree-overlays` for how to add an overlay to the build.
Overlays can override node property values in multiple ways.
For example, if your BOARD.dts contains this node:
.. code-block:: devicetree
/ {
soc {
serial0: serial@40002000 {
status = "okay";
current-speed = <115200>;
/* ... */
};
};
};
These are equivalent ways to override the ``current-speed`` value in an
overlay:
.. Disable syntax highlighting as this construct does not seem supported by pygments
.. code-block:: none
/* Option 1 */
&serial0 {
current-speed = <9600>;
};
/* Option 2 */
&{/soc/serial@40002000} {
current-speed = <9600>;
};
We'll use the ``&serial0`` style for the rest of these examples.
You can add aliases to your devicetree using overlays: an alias is just a
property of the ``/aliases`` node. For example:
.. code-block:: devicetree
/ {
aliases {
my-serial = &serial0;
};
};
Chosen nodes work the same way. For example:
.. code-block:: devicetree
/ {
chosen {
zephyr,console = &serial0;
};
};
To delete a property (in addition to deleting properties in general, this is
how to set a boolean property to false if it's true in BOARD.dts):
.. code-block:: devicetree
&serial0 {
/delete-property/ some-unwanted-property;
};
You can add subnodes using overlays. For example, to configure a SPI or I2C
child device on an existing bus node, do something like this:
.. code-block:: devicetree
/* SPI device example */
&spi1 {
my_spi_device: temp-sensor@0 {
compatible = "...";
label = "TEMP_SENSOR_0";
/* reg is the chip select number, if needed;
* If present, it must match the node's unit address. */
reg = <0>;
/* Configure other SPI device properties as needed.
* Find your device's DT binding for details. */
spi-max-frequency = <4000000>;
};
};
/* I2C device example */
&i2c2 {
my_i2c_device: touchscreen@76 {
compatible = "...";
label = "TOUCHSCREEN";
/* reg is the I2C device address.
* It must match the node's unit address. */
reg = <76>;
/* Configure other I2C device properties as needed.
* Find your device's DT binding for details. */
};
};
Other bus devices can be configured similarly:
- create the device as a subnode of the parent bus
- set its properties according to its binding
Assuming you have a suitable device driver associated with the
``my_spi_device`` and ``my_i2c_device`` compatibles, you should now be able to
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-create-devices:
Write device drivers using devicetree APIs
******************************************
"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.
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 <zephyr/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 {
uint32_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(const struct device *dev, uint32_t *foo) { /* ... */ }
static int my_driver_api_func2(const struct device *dev, uint64_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::
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.
Finally, define an instantiation macro, which creates each ``struct device``
using instance numbers. Do this after defining ``my_api_funcs``.
.. code-block:: c
/*
* 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".
*/
#define CREATE_MY_DEVICE(inst) \
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_DT_INST_DEFINE(inst, \
my_dev_init_function, \
NULL, \
&my_data_##inst, \
&my_cfg_##inst, \
MY_DEV_INIT_LEVEL, MY_DEV_INIT_PRIORITY, \
&my_api_funcs);
Notice the use of APIs like :c:func:`DT_INST_PROP` and
:c:func:`DEVICE_DT_INST_DEFINE` 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)
``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.
Option 2: create devices using node labels
==========================================
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:: devicetree
/ {
soc {
mydevice0: dev@0 {
compatible = "vnd,my-device";
};
mydevice1: dev@1 {
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_DT_DEFINE(MYDEV(idx), \
my_dev_init_function, \
NULL, \
&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_PROP` and
:c:func:`DEVICE_DT_DEFINE` 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_NODE_HAS_STATUS(DT_NODELABEL(mydevice0), okay)
CREATE_MY_DEVICE(0)
#endif
#if DT_NODE_HAS_STATUS(DT_NODELABEL(mydevice1), okay)
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 :zephyr:code-sample:`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>`.
Part 3
한국어 전문 번역
Devicetree HOWTO
이 페이지는 Devicetree로 자주 수행하는 작업을 단계별로 설명합니다. 문제가 생기면 Devicetree troubleshooting 문서도 함께 확인하십시오.
최종 devicetree와 생성 header 얻기
Board의 BOARD.dts는 C preprocessor의 #include로 공통 node 정의를 불러오며 최소한 SoC의 .dtsi를 포함합니다. dts/<ARCH>/<vendor>/<soc>.dtsi 등을 직접 따라갈 수도 있지만 시간이 많이 걸립니다.
최종 결과만 보려면 application을 build한 뒤 build directory의 zephyr.dts를 여십시오. Overlay 변경이 없는 board 기본 devicetree는 hello_world를 build해 확인할 수 있습니다.
# --cmake-only here just forces CMake to run, skipping the
# build process to save time.
west build -b qemu_cortex_m3 samples/hello_world --cmake-only--cmake-only는 CMake configuration만 실행하고 실제 compile을 생략해 시간을 줄입니다. qemu_cortex_m3는 사용 중인 board 이름으로 바꿉니다. CMake는 입력과 출력 위치를 다음처럼 표시합니다.
-- Found BOARD.dts: .../zephyr/boards/arm/qemu_cortex_m3/qemu_cortex_m3.dts
-- Generated zephyr.dts: .../zephyr/build/zephyr/zephyr.dts
-- Generated devicetree_generated.h: .../zephyr/build/zephyr/include/generated/zephyr/devicetree_generated.hzephyr.dts가 DTS 형식의 최종 devicetree이고 devicetree_generated.h가 대응하는 생성 header입니다.
Devicetree node에서 struct device 얻기
Application은 devicetree node에 대응하는 driver-level struct device가 자주 필요합니다. 다음 예에서는 serial@40002000을 가져옵니다.
/ {
soc {
serial0: serial@40002000 {
status = "okay";
current-speed = <115200>;
/* ... */
};
};
aliases {
my-serial = &serial0;
};
chosen {
zephyr,console = &serial0;
};
};먼저 필요한 node의 identifier를 만듭니다. 요구 사항에 맞게 node label, alias, chosen, path 중 하나를 고릅니다.
/* Option 1: by node label */
#define MY_SERIAL DT_NODELABEL(serial0)
/* Option 2: by alias */
#define MY_SERIAL DT_ALIAS(my_serial)
/* Option 3: by chosen node */
#define MY_SERIAL DT_CHOSEN(zephyr_console)
/* Option 4: by path */
#define MY_SERIAL DT_PATH(soc, serial_40002000)Identifier를 얻었으면 DEVICE_DT_GET으로 build-time device pointer를 만들고 사용 전에 readiness를 검사합니다.
const struct device *const uart_dev = DEVICE_DT_GET(MY_SERIAL);
if (!device_is_ready(uart_dev)) {
/* Not ready, do not use */
return -ENODEV;
}DEVICE_DT_GET_OR_NULL, DEVICE_DT_GET_ONE, DEVICE_DT_GET_ANY variant도 있습니다. Pointer는 build-time에 결정되어 runtime 비용이 없고 configuration data에 저장하기 좋습니다. 그러나 장치 초기화가 끝나지 않았거나 실패했을 수 있으므로 API에 넘기기 전에 반드시 device_is_ready를 확인합니다. device_get_binding은 이 검사를 내부에서 수행합니다.
Shell의 사용자 입력처럼 build-time에 장치를 알 수 없으면 runtime 이름과 device_get_binding을 조합합니다.
const char *dev_name = /* TODO: insert device name from user */;
const struct device *uart_dev = device_get_binding(dev_name);이후 uart_configure 같은 해당 device class API를 사용합니다.
문제 확인 순서
첫째, node의 status가 okay인지 compile-time에 확인합니다.
#define MY_SERIAL DT_NODELABEL(my_serial)
#if DT_NODE_HAS_STATUS(MY_SERIAL, okay)
const struct device *const uart_dev = DEVICE_DT_GET(MY_SERIAL);
#else
#error "Node is disabled"
#endif#error가 나오면 devicetree에서 node를 enable합니다. Compile은 되지만 다음 link error가 나면 해당 device driver가 Kconfig 때문에 build되지 않아 device symbol이 없는 경우가 많습니다.
...undefined reference to `__device_dts_ord_N'
collect2: error: ld returned 1 exit statusCompile과 link가 성공하면 마지막으로 readiness를 확인합니다.
if (!device_is_ready(uart_dev)) {
printk("Device not ready\n");
}Ready가 아니면 보통 device initialization function이 실패한 것입니다. Logging을 켜거나 driver를 debug하십시오. Runtime device_get_binding이 NULL이면 driver 초기화 실패 또는 장치 부재 중 하나입니다.
Devicetree binding 찾기
Binding은 node에서 사용할 수 있는 property를 선언하므로 반드시 찾을 수 있어야 합니다. 먼저 생성 output을 만든 뒤 devicetree_generated.h를 엽니다. 파일 앞의 block comment에는 dependency 순서의 ordinal과 node path가 나옵니다.
/*
* [...]
* Nodes in dependency order (ordinal and path):
* 0 /
* 1 /aliases
* 2 /chosen
* 3 /flash@0
* 4 /memory@20000000
* (etc.)
* [...]
*/원하는 path, 예를 들어 /flash@0을 기억하고 그 node의 output을 검색합니다. Matching binding이 있으면 다음 comment에 compatible과 YAML 경로가 표시됩니다.
/*
* Devicetree node:
* /flash@0
*
* Binding (compatible = soc-nv-flash):
* $ZEPHYR_BASE/dts/bindings/mtd/soc-nv-flash.yaml
* [...]
*/표시되지 않으면 missing binding troubleshooting 절차를 따릅니다.
Devicetree overlay 지정
CMake 변수 DTC_OVERLAY_FILE은 사용할 overlay 파일을 공백 또는 semicolon으로 구분한 목록입니다. 여러 파일은 적힌 순서대로 C preprocessor가 포함합니다. Zephyr module 파일은 ${ZEPHYR_<module>_MODULE_DIR}/<path-to>/dts.overlay처럼 module directory 변수를 escape해 지정할 수 있습니다.
DTC_OVERLAY_FILE을 직접 설정하지 않으면 application configuration directory에서 다음 순서로 찾습니다.
socs/<SOC>_<BOARD_QUALIFIERS>.overlay가 있으면 사용boards/<BOARD>.overlay가 있으면 앞 파일에 추가- Board revision이 있고
boards/<BOARD>_<revision>.overlay가 있으면 추가 - 앞 단계에서 하나라도 찾았으면 검색을 멈추고 그 파일들만 사용
- 그렇지 않고
<BOARD>.overlay가 있으면 사용하고 검색 중지 - 그마저 없으면
app.overlay사용
EXTRA_DTC_OVERLAY_FILE은 자동 검색을 유지하면서 추가 overlay를 제공합니다. 이 파일들은 DTC_OVERLAY_FILE 뒤에 append되므로 더 높은 우선순위를 갖습니다. Absolute path argument가 아닌 모든 configuration 파일은 application configuration directory를 기준으로 찾습니다. Shield도 overlay를 추가합니다.
DTC_OVERLAY_FILE 값은 CMake cache에 저장되어 다음 build에도 사용됩니다. Configuration 단계에서 발견한 overlay는 다음과 같이 출력됩니다.
-- Found devicetree overlay: .../some/file.overlayDevicetree overlay 사용
Overlay는 여러 방식으로 node property를 덮어쓸 수 있습니다. 먼저 board DTS가 다음 node를 가진다고 가정합니다.
/ {
soc {
serial0: serial@40002000 {
status = "okay";
current-speed = <115200>;
/* ... */
};
};
};Node label과 absolute path reference는 current-speed를 같은 방식으로 바꿉니다.
/* Option 1 */
&serial0 {
current-speed = <9600>;
};
/* Option 2 */
&{/soc/serial@40002000} {
current-speed = <9600>;
};이후 예제는 간결한 &serial0 형식을 사용합니다. Alias는 /aliases node의 property이므로 overlay에서 다음처럼 추가합니다.
/ {
aliases {
my-serial = &serial0;
};
};Chosen node도 같은 방식입니다.
/ {
chosen {
zephyr,console = &serial0;
};
};Property 삭제는 /delete-property/를 사용합니다. Board DTS에서 참인 boolean property를 false로 만드는 방법도 property를 삭제하는 것입니다.
&serial0 {
/delete-property/ some-unwanted-property;
};기존 bus node에 SPI 또는 I2C child device도 추가할 수 있습니다.
/* SPI device example */
&spi1 {다른 bus device도 parent bus의 subnode로 만들고 binding에 맞춰 property를 설정합니다. Compatible에 연결된 driver가 있다면 Kconfig로 driver를 enable하고 새 bus child의 struct device를 얻어 driver API로 사용할 수 있습니다.
Devicetree API를 사용하는 device driver 작성
Devicetree-aware driver는 자신이 지원하는 compatible을 가진 status = "okay" node마다 struct device를 생성해야 합니다. 먼저 비슷한 driver의 기존 binding을 참고해 지원 장치 binding을 만듭니다. 최소 골격은 다음과 같습니다.
description: <Human-readable description of your binding>
compatible: "foo-company,bar-device"
include: base.yaml그 다음 driver C 파일은 원하는 compatible의 enabled node를 찾아 각각 device를 생성합니다. Instance number 방식과 node label 방식 두 가지가 있습니다. 어느 방식을 사용하든 device 이름은 devicetree node의 label을 사용하고, 초기 configuration은 가능한 한 devicetree property에서 가져와 overlay로 설정할 수 있게 해야 합니다.
다음 공통 골격은 device별 RAM data, ROM configuration, driver API callback을 정의합니다.
/* my_driver.c */
#include <zephyr/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 {
uint32_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(const struct device *dev, uint32_t *foo) { /* ... */ }
static int my_driver_api_func2(const struct device *dev, uint64_t bar) { /* ... */ }
static struct some_api my_api_funcs = {
.func1 = my_driver_api_func1,
.func2 = my_driver_api_func2,
};방법 1: Instance number로 device 생성
가능하면 instance API를 사용합니다. 다만 같은 compatible의 node가 모두 동등하고 서로 구분할 필요가 없을 때만 적합합니다. Driver가 지원하는 "vnd,my-device"를 lowercase와 underscore 형태인 vnd_my_device로 바꿔 DT_DRV_COMPAT을 정의합니다.
/*
* 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_deviceAPI 함수 정의 뒤에 instance number를 받는 device 생성 macro를 만듭니다.
/*
* 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".
*/
#define CREATE_MY_DEVICE(inst) \
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_DT_INST_DEFINE(inst, \
my_dev_init_function, \DT_INST_PROP과 DEVICE_DT_INST_DEFINE은 DT_DRV_COMPAT이 정한 compatible의 instance inst에서 값을 가져옵니다. 마지막으로 enabled instance마다 macro를 호출합니다.
/* Call the device creation macro for each instance: */
DT_INST_FOREACH_STATUS_OKAY(CREATE_MY_DEVICE)DT_INST_FOREACH_STATUS_OKAY는 enabled node마다 CREATE_MY_DEVICE를 한 번 확장합니다. 호출 뒤 semicolon을 자동 추가하지 않으므로 생성 macro 자체가 semicolon 또는 function definition으로 끝나야 여러 device에서 올바르게 이어집니다.
방법 2: Node label로 device 생성
개별 IP block에 특화된 vendor HAL을 호출하는 SoC peripheral driver처럼 instance를 서로 구분해야 하면 DT_NODELABEL과 generic API를 사용합니다. SoC의 .dtsi가 mydevice0, mydevice1 같은 label을 정의해야 합니다.
/ {
soc {
mydevice0: dev@0 {
compatible = "vnd,my-device";
};
mydevice1: dev@1 {
compatible = "vnd,my-device";
};
};
};Driver는 index에서 label을 만드는 helper와 node별 생성 macro를 정의합니다.
/*
* 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) \여기서는 DT_PROP과 DEVICE_DT_DEFINE이 특정 node label의 값을 사용합니다. 각 가능한 node의 status를 수동 검사해 device를 생성합니다.
#if DT_NODE_HAS_STATUS(DT_NODELABEL(mydevice0), okay)
CREATE_MY_DEVICE(0)
#endif
#if DT_NODE_HAS_STATUS(DT_NODELABEL(mydevice1), okay)
CREATE_MY_DEVICE(1)
#endif이 방식은 DT_INST_FOREACH_STATUS_OKAY를 쓰지 않으므로 driver 작성자가 지원 SoC의 가능한 peripheral을 알고 모든 node에 대해 CREATE_MY_DEVICE 호출을 작성해야 합니다.
다른 device에 의존하는 driver
Sensor가 SPI controller pointer를 필요로 하는 것처럼 한 struct device가 다른 device에 의존할 수 있습니다. 가능하면 binding을 devicetree.h의 hardware-specific API를 사용할 수 있게 설계합니다. Bus device binding은 spi-device.yaml 같은 공통 bus 정의를 include해 DT_BUS로 parent bus node identifier를 얻을 수 있게 해야 합니다. 이후 일반적인 방식으로 bus의 device pointer를 얻습니다. 기존 binding과 driver가 좋은 예제입니다.
Board별 device에 의존하는 application
Application code를 수정하지 않고 여러 board에서 실행하려면 Blinky처럼 hardware별 부분을 devicetree alias로 선택하게 만듭니다. 각 board의 BOARD.dts 또는 overlay에서 alias 대상을 설정할 수 있습니다.
Source
출처
원문 파일의 단락, directive, 표, 코드, symbol, 경로는 영어 원문 영역에 그대로 보존했습니다.