查看设备树和生成宏

原始的设备树都是在板子目录下,例如对于NRF5340的板子目录就在ncs_sdk/zephyr/boards/arm/nrf5340dk_nrf5340,在这个目录下就会有两个核的设备树分别为:nrf5340dk_nrf5340_cpuapp.dtsnrf5340dk_nrf5340_cpuappns.dts
在应用程序下,我们可以建立<board>.overlay来覆盖板子的默认设备树。
当工程编译了之后,我们可以直接查看编译目录下的zephyr.dts来查看最终生成的设备树。设备树生成的宏文件为devicetree_unfixed.h,也存储在编译目录下。

从设备树中获取设备结构体

struct device 设备结构体是我们可以调用ZephyrAPI进行设备操作的基础。
例如,对于此设备树片段,您可能希望获取serial@40002000的设备结构体:

  1. / {
  2. soc {
  3. serial0: serial@40002000 {
  4. status = "okay";
  5. current-speed = <115200>;
  6. /* ... */
  7. };
  8. };
  9. aliases {
  10. my-serial = &serial0;
  11. };
  12. chosen {
  13. zephyr,console = &serial0;
  14. };
  15. };

首先我们需要获取节点标识符。可以通过如下方式获取:

  1. /* Option 1: by node label */
  2. #define MY_SERIAL DT_NODELABEL(serial0)
  3. /* Option 2: by alias */
  4. #define MY_SERIAL DT_ALIAS(my_serial)
  5. /* Option 3: by chosen node */
  6. #define MY_SERIAL DT_CHOSEN(zephyr_console)
  7. /* Option 4: by path */
  8. #define MY_SERIAL DT_PATH(soc, serial_40002000)

获取节点标识符之后可以通过以下两种方式获取设备结构体:

  • 通过节点标识符的标签,获取设备结构体

    1. const struct device *uart_dev = device_get_binding(DT_LABEL(MY_SERIAL));
  • 通过节点标识符,直接获取设备结构体

    1. const struct device *uart_dev = DEVICE_DT_GET(MY_SERIAL);

    获取设备结构体之后,需要检测一个设备是否准备就绪,否则如果直接调用API来操作设备会导致死机。

    1. if (!device_is_ready(uart_dev)) {
    2. /* Not ready, do not use */
    3. return -ENODEV;
    4. }

    之后,就可以直接调用Zephyr的外设API来操作外设。

设置设备树覆盖

CMake变量DTC_OVERLAY_FILE包含要使用的覆盖文件的空格分号分隔列表。如果DTC_OVERLAY_FILE指定了多个文件,则C预处理器将按该顺序包含这些文件。
如果未设置DTC_OVERLAY_FILE,则生成系统将执行以下步骤,在应用程序源目录中查找要用作设备树覆盖的文件:

  • 如果boards/<BOARD>.overlay该文件存在,则将使用它。
  • 如果当前主板有多个修订版并且boards/<BOARD>_<revision>.overlay存在,则将使用它。此外,如果两者都存在,还将使用boards/<BOARD>.overlay此文件。
  • 如果在前面的步骤中找到一个或多个文件,则生成系统将停止查找,只使用这些文件。
  • 否则,如果<BOARD>.overlay存在,它将被使用,并且构建系统将停止查找更多文件。
  • 否则,如果app.overlay存在,将使用它。

    DTC_OVERLAY_FILE值存储在CMake缓存中,并在后续生成中使用。

使用设备树覆盖

设备树覆盖主要就是为了覆盖默认的板子的设备树。

修改属性的值

例如,如果BOARD.dts包含如下节点:

  1. / {
  2. soc {
  3. serial0: serial@40002000 {
  4. status = "okay";
  5. current-speed = <115200>;
  6. /* ... */
  7. };
  8. };
  9. };

可以通过以下的方式覆盖current-speed的值:

  1. /* Option 1 */
  2. &serial0 {
  3. current-speed = <9600>;
  4. };
  1. /* Option 2 */
  2. &{/soc/serial@40002000} {
  3. current-speed = <9600>;
  4. };

添加别名和选择节点

可以在设备树覆盖文件中添加别名节点。

  1. / {
  2. aliases {
  3. my-serial = &serial0;
  4. };
  5. };

可以在设备树覆盖文件中添加选择节点。

  1. / {
  2. aliases {
  3. my-serial = &serial0;
  4. };
  5. };

删除属性

可以在设备树覆盖文件中删除一些无用的属性。

  1. &serial0 {
  2. /delete-property/ some-unwanted-property;
  3. };

添加子节点

可以使用设备树覆盖添加子节点。例如,要在现有总线节点上配置SPII2C子器件,请执行如下操作:

  1. /* SPI device example */
  2. &spi1 {
  3. my_spi_device: temp-sensor@0 {
  4. compatible = "...";
  5. label = "TEMP_SENSOR_0";
  6. /* reg is the chip select number, if needed;
  7. * If present, it must match the node's unit address. */
  8. reg = <0>;
  9. /* Configure other SPI device properties as needed.
  10. * Find your device's DT binding for details. */
  11. spi-max-frequency = <4000000>;
  12. };
  13. };
  14. /* I2C device example */
  15. &i2c2 {
  16. my_i2c_device: touchscreen@76 {
  17. compatible = "...";
  18. label = "TOUCHSCREEN";
  19. /* reg is the I2C device address.
  20. * It must match the node's unit address. */
  21. reg = <76>;
  22. /* Configure other I2C device properties as needed.
  23. * Find your device's DT binding for details. */
  24. };
  25. };

其他总线设备可以类似地配置:

  • 将设备创建为父总线的子节点
  • 根据其绑定设置其属性

使用设备树API编写设备驱动程序

  • 定义设备树节点
  • 定义设备树绑定文件
  • 编写设备驱动

设备树节点

定义设备树节点

  1. / {
  2. soc {
  3. mydevice0: dev@0 {
  4. compatible = "vnd,my-device";
  5. };
  6. mydevice1: dev@1 {
  7. compatible = "vnd,my-device";
  8. };
  9. };
  10. };

定义设备树绑定文件

定义和设备树节点的compatible匹配的绑定文件

  1. description: <Human-readable description of your binding>
  2. compatible: "vnd,my-device"
  3. include: base.yaml

编写设备驱动

设备驱动的编写有两中方式:

  • 通过获取节点标识符的标签的方式编写驱动
  • 通过实例的方式编写驱动

实例方式的驱动

通过实例方式获取节点标识符

  1. #define DT_DRV_COMPAT vnd_my_device

定义一个实例化的宏,来注册驱动。

  1. /* my_driver.c */
  2. #include <drivers/some_api.h>
  3. /* Define data (RAM) and configuration (ROM) structures: */
  4. struct my_dev_data {
  5. /* per-device values to store in RAM */
  6. };
  7. struct my_dev_cfg {
  8. uint32_t freq; /* Just an example: initial clock frequency in Hz */
  9. /* other configuration to store in ROM */
  10. };
  11. /* Implement driver API functions (drivers/some_api.h callbacks): */
  12. static int my_driver_api_func1(const struct device *dev, uint32_t *foo) { /* ... */ }
  13. static int my_driver_api_func2(const struct device *dev, uint64_t bar) { /* ... */ }
  14. static struct some_api my_api_funcs = {
  15. .func1 = my_driver_api_func1,
  16. .func2 = my_driver_api_func2,
  17. };
  18. /*
  19. * This instantiation macro is named "CREATE_MY_DEVICE".
  20. * Its "inst" argument is an arbitrary instance number.
  21. *
  22. * Put this near the end of the file, e.g. after defining "my_api_funcs".
  23. */
  24. #define CREATE_MY_DEVICE(inst) \
  25. static struct my_dev_data my_data_##inst = { \
  26. /* initialize RAM values as needed, e.g.: */ \
  27. .freq = DT_INST_PROP(inst, clock_frequency), \
  28. }; \
  29. static const struct my_dev_cfg my_cfg_##inst = { \
  30. /* initialize ROM values as needed. */ \
  31. }; \
  32. DEVICE_DT_INST_DEFINE(inst, \
  33. my_dev_init_function, \
  34. NULL, \
  35. &my_data_##inst, \
  36. &my_cfg_##inst, \
  37. MY_DEV_INIT_LEVEL, MY_DEV_INIT_PRIORITY, \
  38. &my_api_funcs);

最后,将实例化宏传递给DT_INST_FOREACH_STATUS_OKAY()

  1. /* Call the device creation macro for each instance: */
  2. DT_INST_FOREACH_STATUS_OKAY(CREATE_MY_DEVICE)

DT_INST_FOREACH_STATUS_OKAY扩展到对每个已启用节点调用一次CREATE_MY_DEVICE的代码,其兼容性由CREATE_MY_DEVICE确定。它不会在CREATE_MY_DEVICE扩展的末尾附加分号,因此宏的展开必须以分号或函数定义结尾才能支持多个设备。

标签方式的驱动

通过标签方式获取节点标识符

  1. #define MYDEV(idx) DT_NODELABEL(mydevice ## idx)

驱动程序可以使用设备树中的节点标签在特定设备节点上运行:

  1. /* my_driver.c */
  2. #include <drivers/some_api.h>
  3. /* Define data (RAM) and configuration (ROM) structures: */
  4. struct my_dev_data {
  5. /* per-device values to store in RAM */
  6. };
  7. struct my_dev_cfg {
  8. uint32_t freq; /* Just an example: initial clock frequency in Hz */
  9. /* other configuration to store in ROM */
  10. };
  11. /* Implement driver API functions (drivers/some_api.h callbacks): */
  12. static int my_driver_api_func1(const struct device *dev, uint32_t *foo) { /* ... */ }
  13. static int my_driver_api_func2(const struct device *dev, uint64_t bar) { /* ... */ }
  14. static struct some_api my_api_funcs = {
  15. .func1 = my_driver_api_func1,
  16. .func2 = my_driver_api_func2,
  17. };
  18. /*
  19. * This is a convenience macro for creating a node identifier for
  20. * the relevant devices. An example use is MYDEV(0) to refer to
  21. * the node with label "mydevice0".
  22. */
  23. #define MYDEV(idx) DT_NODELABEL(mydevice ## idx)
  24. /*
  25. * Define your instantiation macro; "idx" is a number like 0 for mydevice0
  26. * or 1 for mydevice1. It uses MYDEV() to create the node label from the
  27. * index.
  28. */
  29. #define CREATE_MY_DEVICE(idx) \
  30. static struct my_dev_data my_data_##idx = { \
  31. /* initialize RAM values as needed, e.g.: */ \
  32. .freq = DT_PROP(MYDEV(idx), clock_frequency), \
  33. }; \
  34. static const struct my_dev_cfg my_cfg_##idx = { /* ... */ }; \
  35. DEVICE_DT_DEFINE(MYDEV(idx), \
  36. my_dev_init_function, \
  37. NULL, \
  38. &my_data_##idx, \
  39. &my_cfg_##idx, \
  40. MY_DEV_INIT_LEVEL, MY_DEV_INIT_PRIORITY, \
  41. &my_api_funcs)

最后,手动检测每个已启用的设备树节点,并用于实例化每个设备驱动:

  1. #if DT_NODE_HAS_STATUS(DT_NODELABEL(mydevice0), okay)
  2. CREATE_MY_DEVICE(0)
  3. #endif
  4. #if DT_NODE_HAS_STATUS(DT_NODELABEL(mydevice1), okay)
  5. CREATE_MY_DEVICE(1)
  6. #endif