Zephyr Devicetree 详解-如何读懂 Devicetree

对于 Zephyr 来说要读懂一个 Devicetree 需要具备以下知识基础:

本文以一个实例说明如何读懂 Zephyr 的 DeviceTree。实例基于 esp32c3_devkitm 开发板,使用 SPI 去驱动 ws2812

west build -b esp32c3_devkitm zephyr/samples/drivers/led_strip

组合完整的 dts 链接到标题

Zephyr 的原生代码中并不存在一个完整的 Devicetree,Zephyr 的 Devicetree 基本是以「片段的形式「存在,这些片段就是前文提到的:.dts, .dtsi,.overlay, 在 Zephyr 构建过程中会从不同的目录获取相应的 Devicetree 片段将其组合到一起。下面是读 esp32c3_devkitm 开发板 Devicetree 的过程。

1 找主板 dts 链接到标题

Zephyr 中的硬件是以开发板为单位,因此从一个开发板的 dts 入手分析,对于 esp32c3_devkitm 开发板的 dts 就在其 board 目录 zephyr/boards/espressif/esp32c3_devkitm/ 下找到 esp32c3_devkitm.dts,开发板的 dts,是在 soc 提供的 dtsi 下添加和板子相关的节点,按照主板的硬件特性对 soc 提供的硬件特性进行覆盖,按照主板/应用的设计对 Zephyr 的默认设备进行选择和覆盖。主板的 dts 内容如下:

/*表明 dts 版本是 v1*/
/dts-v1/;

/* soc 包含文件*/
# include <espressif/esp32c3/esp32c3_mini_n4.dtsi>

/* pin contorl 包含文件*/
# include "esp32c3_devkitm-pinctrl.dtsi"

/* input event 包含文件, 获取头文件中的宏*/
# include <zephyr/dt-bindings/input/input-event-codes.h>

/ {
    /* model 节点通用于描述设备或板子的具体型号*/
    model = "Espressif ESP32C3-DevkitM";

    /* 根的 compatible */
    compatible = "espressif,esp32c3";

    /* chose 节点,为 Zephyr 选择默认设备 */
    chosen {
        zephyr,sram = &sram0;
        zephyr,console = &uart0;
        zephyr,shell-uart = &uart0;
        zephyr,flash = &flash0;
        zephyr,code-partition = &slot0_partition;
        zephyr,bt-hci = &esp32_bt_hci;
    };

    /* aliases 节点, 为应用选择默认设备 */
    aliases {
        sw0 = &user_button1;
        i2c-0 = &i2c0;
        watchdog0 = &wdt0;
    };

    /* 将 gpio0.9 作为 user_button1, 硬件默认上拉,低电平有效,案件值绑定 INPUT_KEY_0 */
    gpio_keys {
        compatible = "gpio-keys";
        user_button1: button_1 {
            label = "User SW1";
            gpios = <&gpio0 9 ( GPIO_PULL_UP | GPIO_ACTIVE_LOW )>;
            zephyr,code = <INPUT_KEY_0>;
        };
    };
};

/* 后面都是对 soc 下的 dtsi 中节点进行修改和覆盖 */
&uart0 {
    status = "okay";
    current-speed = <115200>;
    pinctrl-0 = <&uart0_default>;
    pinctrl-names = "default";
};

&usb_serial {
    /* requires resoldering of resistors on the board */
    status = "okay";
};

&i2c0 {
    status = "okay";
    clock-frequency = <I2C_BITRATE_STANDARD>;
    pinctrl-0 = <&i2c0_default>;
    pinctrl-names = "default";
};

&trng0 {
    status = "okay";
};

&spi2 {
    #address-cells = <1>;
    #size-cells = <0>;
    status = "okay";
    pinctrl-0 = <&spim2_default>;
    pinctrl-names = "default";
};

&gpio0 {
    status = "okay";
};

&wdt0 {
    status = "okay";
};

&timer0 {
    status = "disabled";
};

&timer1 {
    status = "disabled";
};

&twai {
    /* requires external CAN transceiver or jumper on RX and TX pins for loopback testing */
    status = "disabled";
    pinctrl-0 = <&twai_default>;
    pinctrl-names = "default";
};

&flash0 {
    status = "okay";
    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        /* Reserve 60kB for the bootloader */
        boot_partition: partition@0 {
            label = "mcuboot";
            reg = <0x00000000 0x0000F000>;
            read-only;
        };

        /* Reserve 1024kB for the application in slot 0 */
        slot0_partition: partition@10000 {
            label = "image-0";
            reg = <0x00010000 0x00100000>;
        };

        /* Reserve 1024kB for the application in slot 1 */
        slot1_partition: partition@110000 {
            label = "image-1";
            reg = <0x00110000 0x00100000>;
        };

        /* Reserve 256kB for the scratch partition */
        scratch_partition: partition@210000 {
               label = "image-scratch";
               reg = <0x00210000 0x00040000>;
        };

        storage_partition: partition@250000 {
            label = "storage";
            reg = <0x00250000 0x00006000>;
        };
    };
};

&esp32_bt_hci {
    status = "okay";
};

2 回溯覆盖节点 链接到标题

esp32c3_devkitm.dts 中对 soc 的 dtsi 已有的节点进行覆盖,覆盖的节点一般是采用 lable 引用,例如&uart0,可以很清晰的看到覆盖节点有:

  • 设置参数并启用:uart0, i2c0, spi2, flash0
  • 启用设备:usb_serial, trng0, gpio0, wdt0, esp32_bt_hci
  • 禁用设备:timer0, timer1, twai

对于启用设备的 uart0,i2c0,spi2 配置了默认参数并通过 pinctrl 的节点设置引脚复用,引脚复用的 Devicetree 用法请参考 Zephyr-Pinctrl 相关文章。 未覆盖的参数和启用设备的默认参数要通过 soc dtsi 来查找,在本例中 soc dtsi 就是 dts 中包含的 esp32c3_mini_n4.dtsi,内容如下,

# include "esp32c3_common.dtsi"

/* 4MB flash */
&flash0 {
    reg = <0x0 DT_SIZE_M ( 4 )>;
};

这里可以看到 flash0 的大小为 4M,再下一步回溯到 esp32c3_common.dtsi,这就是 esp32c3 soc 的共用 dts 文件,里面可以找到 esp32c3 所支持的片上硬件。一般情况下我们只会关心主板要用的硬件设备,这里这里我们以 spi2 和 flash0 为例来说明如何回溯,在 esp32c3_common.dtsi

spi2: spi@60024000 {
            compatible = "espressif,esp32-spi";
            reg = <0x60024000 DT_SIZE_K ( 4 )>;
            interrupts = <SPI2_INTR_SOURCE>;
            interrupt-parent = <&intc>;
            clocks = <&rtc ESP32_SPI2_MODULE>;
            dma-clk = <ESP32_GDMA_MODULE>;
            dma-host = <0>;
            status = "disabled";
        };

flash: flash-controller@60002000 {
            compatible = "espressif,esp32-flash-controller";
            reg = <0x60002000 0x1000>;

            #address-cells = <1>;
            #size-cells = <1>;

            flash0: flash@0 {
                compatible = "soc-nv-flash";
                reg = <0 0x400000>;
                erase-block-size = <4096>;
                write-block-size = <4>;
            };
        };

esp32c3_devkitm.dtsesp32c3_mini_n4.dtsi 对其进行覆盖得到

        spi2: spi@60024000 {
            compatible = "espressif,esp32-spi";
            reg = <0x60024000 DT_SIZE_K ( 4 )>;
            interrupts = <SPI2_INTR_SOURCE>;
            interrupt-parent = <&intc>;
            clocks = <&rtc ESP32_SPI2_MODULE>;
            dma-clk = <ESP32_GDMA_MODULE>;
            dma-host = <0>;
            status = "okay";
            #address-cells = < 0x1 >;
            #size-cells = < 0x0 >;
            pinctrl-0 = < &spim2_default >;
            pinctrl-names = "default";
        };

        flash: flash-controller@60002000 {
            compatible = "espressif,esp32-flash-controller";
            reg = < 0x60002000 0x1000 >;
            #address-cells = < 0x1 >;
            #size-cells = < 0x1 >;
            flash0: flash@0 {
                compatible = "soc-nv-flash";
                reg = <0x0 DT_SIZE_M ( 4 )>;
                erase-block-size = < 4096 >;
                write-block-size = < 4 >;
                status = "okay";
                partitions {
                    compatible = "fixed-partitions";
                    #address-cells = < 0x1 >;
                    #size-cells = < 0x1 >;
                    /* Reserve 60kB for the bootloader */
                    boot_partition: partition@0 {
                        label = "mcuboot";
                        reg = <0x00000000 0x0000F000>;
                        read-only;
                    };

                    /* Reserve 1024kB for the application in slot 0 */
                    slot0_partition: partition@10000 {
                        label = "image-0";
                        reg = <0x00010000 0x00100000>;
                    };

                    /* Reserve 1024kB for the application in slot 1 */
                    slot1_partition: partition@110000 {
                        label = "image-1";
                        reg = <0x00110000 0x00100000>;
                    };

                    /* Reserve 256kB for the scratch partition */
                    scratch_partition: partition@210000 {
                        label = "image-scratch";
                        reg = <0x00210000 0x00040000>;
                    };

                    storage_partition: partition@250000 {
                        label = "storage";
                        reg = <0x00250000 0x00006000>;
                    };
                };
            };
        };

当几个 Devicestree 文件中有相同属性时,包含文件中属性的值优先级大于被包含的文件中的值,上面 esp32c3_devkitm.dts 包含了 esp32c3_mini_n4.dtsiesp32c3_mini_n4.dtsi 包含了 esp32c3_common.dtsi,那么其属性的覆盖优先级就是 esp32c3_devkitm.dts > esp32c3_mini_n4.dtsi >esp32c3_common.dtsi

处理 overlay 链接到标题

覆盖文件 overlay,用于对主板附加/修改/添加硬件的状况下修改 Devicetree,它是在主板 dts 的基础上进行覆盖操作,本例中基于 esp32c3_devkitm 添加了外部的 spi led 灯带,通过 overlay 在 Devicetree 中添加灯带的硬件,其 overlay 文件是 zephyr/samples/drivers/led_strip/boards/esp32c3_devkitm.overlay, 内容如下

/* 在 spi2 下挂 ws2812 */
&spi2 {
    /* Workaround to support WS2812 driver */
    line-idle-low;

    led_strip: ws2812@0 {
        compatible = "worldsemi,ws2812-spi";

        /* SPI */
        reg = <0>; /* ignored, but necessary for SPI bindings */
        spi-max-frequency = <6400000>;

        /* WS2812 */
        chain-length = <1>; /* arbitrary; change at will */
        spi-cpha;
        spi-one-frame = <0xf0>; /* 11110000:625 ns high and 625 ns low */
        spi-zero-frame = <0xc0>; /* 11000000:312.5 ns high and 937.5 ns low */
        color-mapping = <LED_COLOR_ID_GREEN
                 LED_COLOR_ID_RED
                 LED_COLOR_ID_BLUE>;
    };
};

/* 将 spi2 的 mosi 连接到 gpio8 */
&pinctrl {
    spim2_default: spim2_default {
        group2 {
            pinmux = <SPIM2_MOSI_GPIO8>;
        };
    };
};

/ {
    aliases {
        led-strip = &led_strip;
    };
};

经过 ovelay 的覆盖 spi2 节点就变为如下

        spi2: spi@60024000 {
            compatible = "espressif,esp32-spi";
            reg = <0x60024000 DT_SIZE_K ( 4 )>;
            interrupts = <SPI2_INTR_SOURCE>;
            interrupt-parent = <&intc>;
            clocks = <&rtc ESP32_SPI2_MODULE>;
            dma-clk = <ESP32_GDMA_MODULE>;
            dma-host = <0>;
            status = "okay";
            #address-cells = < 0x1 >;
            #size-cells = < 0x0 >;
            pinctrl-0 = < &spim2_default >;
            pinctrl-names = "default";
            line-idle-low;

            led_strip: ws2812@0 {
                compatible = "worldsemi,ws2812-spi";

                /* SPI */
                reg = <0>; /* ignored, but necessary for SPI bindings */
                spi-max-frequency = <6400000>;

                /* WS2812 */
                chain-length = <1>; /* arbitrary; change at will */
                spi-cpha;
                spi-one-frame = <0xf0>; /* 11110000:625 ns high and 625 ns low */
                spi-zero-frame = <0xc0>; /* 11000000:312.5 ns high and 937.5 ns low */
                color-mapping = <LED_COLOR_ID_GREEN
                        LED_COLOR_ID_RED
                        LED_COLOR_ID_BLUE>;
            };
        };

通过前面的 include 和 overlay,就可以得到一个全面的 dts 文件。在 Zephyr 构建过程中也会产生一个完整 zephyr.dts 最终文件,本例中 zephyr.dts 中的 spi2 如下

spi2: spi@60024000 {
            compatible = "espressif,esp32-spi";
            reg = < 0x60024000 0x1000 >;
            interrupts = < 0x13 >;
            interrupt-parent = < &intc >;
            clocks = < &rtc 0xb >;
            dma-clk = < 0x18 >;
            dma-host = < 0x0 >;
            status = "okay";
            #address-cells = < 0x1 >;
            #size-cells = < 0x0 >;
            pinctrl-0 = < &spim2_default >;
            pinctrl-names = "default";
            line-idle-low;
            led_strip: ws2812@0 {
                compatible = "worldsemi,ws2812-spi";
                reg = < 0x0 >;
                spi-max-frequency = < 0x61a800 >;
                chain-length = < 0x1 >;
                spi-cpha;
                spi-one-frame = < 0xf0 >;
                spi-zero-frame = < 0xc0 >;
                color-mapping = < 0x2 0x1 0x3 >;
            };
        };

和我们自己通过阅读 Devicetree 的几乎一样,只是引用头文件宏的地方被替换成了值,可读性会差一些。

读懂节点 链接到标题

从上一节我们可以整合出一个完整的 Devicetree,而 Devicetree 是由各个节点组成,读懂节点就是读懂了 Devicetree 描述的硬件。这里以 ws2812 为例,一直回溯到根节点,进行分析

/ {
    soc {
        #address-cells = < 0x1 >;
        #size-cells = < 0x1 >;
        compatible = "simple-bus";
        ranges;
        spi2: spi@60024000 {
            compatible = "espressif,esp32-spi";
            reg = <0x60024000 DT_SIZE_K ( 4 )>;
            interrupts = <SPI2_INTR_SOURCE>;
            interrupt-parent = <&intc>;
            clocks = <&rtc ESP32_SPI2_MODULE>;
            dma-clk = <ESP32_GDMA_MODULE>;
            dma-host = <0>;
            status = "okay";
            #address-cells = < 0x1 >;
            #size-cells = < 0x0 >;
            pinctrl-0 = < &spim2_default >;
            pinctrl-names = "default";
            line-idle-low;

            led_strip: ws2812@0 {
                compatible = "worldsemi,ws2812-spi";

                /* SPI */
                reg = <0>; /* ignored, but necessary for SPI bindings */
                spi-max-frequency = <6400000>;

                /* WS2812 */
                chain-length = <1>; /* arbitrary; change at will */
                spi-cpha;
                spi-one-frame = <0xf0>; /* 11110000:625 ns high and 625 ns low */
                spi-zero-frame = <0xc0>; /* 11000000:312.5 ns high and 937.5 ns low */
                color-mapping = <LED_COLOR_ID_GREEN
                        LED_COLOR_ID_RED
                        LED_COLOR_ID_BLUE>;
            };
        };
    }
}

节点的父子关系 /->soc->spi2->ws2812.

soc 的属性 链接到标题

compatible = "simple-bus"; 表示是一个简单总线,Zephyr 并不提供专用的绑定。

ranges; 没有给定地址范围,所以 soc 的子节点使用的地址是绝对地址。

#address-cells = < 0x1 >;#size-cells = < 0x1 >; 用于描述 soc 子节点的寄存器信息,有一个地址和一个长度。

spi2 子节点 链接到标题

spi2: spi@60024000 spi 的总线地址为 0x60024000, lable 为 spi2。

compatible = "espressif,esp32-spi"; 使用 espressif,esp32-spi.yaml 绑定,后续的 spi2 属性解释都要参考 espressif,esp32-spi.yaml。

espressif,esp32-spi.yaml include 了 spi-controller.yaml, pinctrl-device.yaml, spi 节点的解释可以从 3 部分找:

  • spi-controller.yaml SPI 控制器共有部分
  • pinctrl-device.yaml 引脚控制复用部分
  • espressif,esp32-spi.yaml esp32c3 SPI 特有部分

SPI 控制器共有部分 链接到标题

这些属性的含义都从 spi-controller.yaml 中去查找,而 spi-contorller.yaml 又 include base.yaml, 也需要从 base.yaml 中查找:

reg = <0x60024000 DT_SIZE_K ( 4 )>; spi2 的寄存器基地址为 0x60024000 ( 占用 1 个单位对应其父节点 soc 的 #address-cells = < 0x1 >; ) , 寄存器组的大小为 4K ( 占用 1 个单位对应其父节点 soc 的 #size-cells = < 0x1 >; ) 。

interrupts = <SPI2_INTR_SOURCE>; 使用 SPI2_INTR_SOURCE 中断源

interrupt-parent = <&intc>; 中断控制器为 intc

clocks = <&rtc ESP32_SPI2_MODULE> 时钟源为用于 ESP32_SPI2_MODULE 的 rtc

#address-cells = < 0x1 >;#size-cells = < 0 >; 用于描述 SPI 子节点的寄存器信息,有一个地址和一个长度。

引脚控制复用部分 链接到标题

这部分属性的作用从 pinctrl-device.yamlpinctrl-0 = < &spim2_default >; 使用 spim2_default 配置其引脚配置

pinctrl-names = "default"; 默认配置

esp32c3-SPI 特有部分 链接到标题

这部分属性的作用从 espressif,esp32-spi.yamldma-clk = <ESP32_GDMA_MODULE>; DMA clock source

dma-host = <0>; DMA Host - 0 -> SPI2,1 -> SPI3

line-idle-low; Default MISO and MOSI pins GPIO level when idle. Defaults to high by default.

ws2812 子节点 链接到标题

ws2812 挂在 spi 下 led_strip: ws2812@0 ws2812 的总线地址为 0 ( 对于 spi 来说总线地址就是片选,如果 spi2 下还挂有一个就是 ws2812_1@1 ) , lable 为 led_strip。

compatible = "worldsemi,ws2812-spi"; 使用 worldsemi,ws2812-spi.yaml 绑定,后续的 ws2812 属性解释都要参考 worldsemi,ws2812-spi.yaml。

worldsemi,ws2812-spi.yaml include 了 spi-device.yaml, ws2812.yaml, ws2812 节点的解释可以从 3 部分找:

  • spi-device.yaml.yaml SPI 设备共有部分
  • ws2812.yaml ws2812 部分
  • worldsemi,ws2812-spi.yaml ws2812-spi 特有部分

SPI 控制器共有部分 链接到标题

reg = <0>; 表示片选 ( 对应到 spi2 下 #address-cells = < 0x1 >;#size-cells = < 0 >; )

spi-max-frequency = <6400000>; SPI 速度设置为 6.4M, 一个 bit 156.25ns

spi-cpha CPHA 在第二个时钟边沿采样数据

ws2812 部分 链接到标题

chain-length = <1>; The number of devices in the daisy-chain. LED 灯带链上有几个 LED

color-mapping = <LED_COLOR_ID_GREEN LED_COLOR_ID_RED LED_COLOR_ID_BLUE>; Channel to color mapping ( or pixel order ) . 数据通道到颜色的映射,这里是 RGB

ws2812-spi 特有部分 链接到标题

spi-one-frame = <0xf0>; 8-bit SPI frame to shift out for a 1 pulse, 8bit spi 帧对应表示出 1 的 pulse , 0xf0=b11110000:4 个高 bit 625 ns ,4 个低 bit 625 ns ( 4*156.25 )

spi-zero-frame = <0xc0>; 8-bit SPI frame to shift out for a 0 pulse, 8bit spi 帧对应表示出 0 的 pulse 0xc0=b11000000:2 个高 bit 312.5 ns ( 2*156.25 ) 6 个低 bit 937.5 ns ( 6*156.25 )

这这里为什么要将 spi 的速度设置为 6.4M, frame 的数据选择,和 ws2812 的硬件知识相关。

小结 链接到标题

从本文的分析过程可以看到,读懂 Zephyr Devicetree 需要 2 步:第一步是整合得到完整的 Device tree,第二步是根据绑定文件的解释理解每个节点的作用。在这一过程中又依赖 Devicetree 语法,Zephyr Devicetree 绑定和硬件的基础知识。0