Zephyr Devicetree 详解-如何写 Devicetree

写 Zephyr Devicetree 的方法就是读 Devicetree 的过程反过来:

  1. 根据硬件内容添加单个节点
  2. 根据绑定文件和硬件配置特性写节点属性

什么时候需要写 Devicetree 链接到标题

在 Zephyr 中有下面几种情况需要写 Devicetree

  1. 添加 SOC
  2. 添加新的主板
  3. 硬件调整和修改
  4. 同硬件不同应用功能

SOC 的 Devicetree 是依据 SOC 的硬件特性进行编写,一般情况下是由 SOC 供应商来提供。因此本文的内容描述情况 2~4。

写主板 Devicetree 链接到标题

写主板设备树最快速的方法就是「抄」,找一个使用了相同 SOC 的主板复制过来然后修改即可。为了对写主板 Devicetree 有一个认识,本文则是基于 SOC 开始写。

本文以 ESP32-C3-LCDkit 的硬件为例说明如何建立主板 Devicetree。 ESP32-C3-LCDkit 的原理图参考:https://docs.espressif.com/projects/esp-dev-kits/zh_CN/latest/_static/esp32-c3-lcdkit/schematics/SCH_ESP32-C3-C6-LCDkit-MB_V1.1_20230417.pdf

外部设备有 链接到标题

  • 外部 UART 接口: TXD0,RXD0, 使用 uart0
  • USB: IO18,19, 使用片上 usb
  • EC11 旋转编码器:IO6,9,10
  • SPI LCD: IO0-2,5,7,18,19,22,23,使用 spi2
  • 按键: IO9
  • 按键: EN, EN 为 reset,不需要出现在 Devicetree
  • WS2812C LED: IO8, SPI 被 LCD 占用,RMT 无驱动,因此不用该设备
  • IR: IO4, Zephyr 驱动目前不支持红外遥控,因此不用该设备
  • PA 功放: IO3, 使用 I2S 的 PDM 模式,Zephyr 驱动目前不支持 esp32c3 的 i2s,因此不用该设备

外部 UART 接口 链接到标题

使用 uart1, 波特率默认配置为 115200,将其作为 console 输出

/ {
    chosen {
        zephyr,console = &uart0;
    };
}

&uart0 {
    status = "okay";
    current-speed = <115200>;
    pinctrl-0 = <&uart0_default>;
    pinctrl-names = "default";
};
  • current-speed - 设置默认波特率
  • pinctrl-0/pinctrl-names - 为uart0配置引脚,关于 pinctrl 之后文统一说明。

USB 链接到标题

esp32c3 的 USB 只含有 CDC-ACM 虚拟串口及 JTAG 适配器功能,这里将其作为 usb 串口,并且 shell 使用该串口通信

/ {
    chosen {
        zephyr,shell-console = &usb_serial;
    };
}
&usb_serial {
    status = "okay";
};

EC11 旋转编码器 链接到标题

旋转编码器使用 EC11,硬件连线如下:

  • A-IO10
  • B-IO6
  • D-IO9
  • C, E-GND

其中 AB,作为旋转编码器的编码线用于检查相位信号变化, D 单独作为按键。

在 Zephyr 中有旋转编码器的驱动,找到对应的绑定文件 zephyr/dts/bindings/input/gpio-qdec.yaml, 参考说明添加

# include <zephyr/dt-bindings/input/input-event-codes.h>
/{
    ec11_qdec: qdec{
            compatible = "gpio-qdec";
            gpios = <&gpio0 6 ( GPIO_PULL_UP | GPIO_ACTIVE_HIGH )>,
                    <&gpio0 10 ( GPIO_PULL_UP | GPIO_ACTIVE_HIGH )>;
            steps-per-period = <4>;
            zephyr,axis = <INPUT_REL_X>;
            sample-time-us = <2000>;
            idle-timeout-ms = <200>;
    };

}

在绑定中的属性描述并不一定能理解属性的作用,这时需要具备 ec11 硬件驱动的知识和去看旋转编码器的驱动 zephyr/drivers/input/input_gpio_qdec.c,但这不是本文的重点,这里就简要说明一下:

  • steps-per-period
    • 旋转编码器有两个输出信号,称为 A 和 B。这两个信号在编码器旋转时会产生方波,它们之间有 90 度的相位差。编码器的旋转方向和步长就可以通过这两个信号的变化来确定。
    • 信号在一个周期内有一次上升和一次下降,A/B 两条信号线就是两次上升,两次下降,steps-per-period 可取的值有
      • 4: A 上升 -> B 上升 -> A 下降 -> B 下降,每个边沿计数,边沿出现的顺序确定方向,也叫做「Full-period mode」
      • 2: A 上升 -> A 下降, 只有信号 A 的变化被计数, 信号 B 的高低电平用于确定方向,也叫做"Half-period mode"
      • 1: A 上升,只计数 A 信号的上升沿,B 信号仍用于确定方向, 也叫做「Quarter-period mode」
Full-period mode:
B: ¯¯¯|___|¯¯¯|___|¯¯¯
A: _|¯¯¯|___|¯¯¯|___|
    1 2 3 4 1 2 3 4 ( 计数点 )

Half-period mode:
B: ¯¯¯|___|¯¯¯|___|¯¯¯
A: _|¯¯¯|___|¯¯¯|___|
    1   2   1   2 ( 计数点 )

Quarter-period mode:
A: ¯¯¯|___|¯¯¯|__|¯¯¯
B: _|¯¯¯|___|¯¯¯|___|
    1       1 ( 计数点 )
  • zephyr,axis - 旋转编码器对应的 input event INPUT_REL_X
  • sample-time-us - 从边沿中断产生开始多长时间后对 A/B 电平进行采样
  • idle-timeout-ms - 据边沿中断产生多长时间后无信号变化就当作是旋转停止

旋转编码器的 D 作为普通按键,连接到 IO9,使用 gpio-keys 绑定,为其关联 input event INPUT_KEY_0

/{
    gpio_keys {
            compatible = "gpio-keys";
            ec11_btn: btn {
                label = "EC11 BTN";
                gpios = <&gpio0 9 ( GPIO_PULL_UP | GPIO_ACTIVE_LOW )>;
                zephyr,code = <INPUT_KEY_0>;
            };
        };

}

SPI 屏 链接到标题

SPI 屏使用 1.28 寸 TFT 屏幕,硬件连线如下: https://docs.espressif.com/projects/esp-dev-kits/zh_CN/latest/_static/esp32-c3-lcdkit/schematics/SCH_ESP32-C3-LCDkit-DB_V1.0_20230329.pdf

  • LCD_BL_CTRL-IO5
  • LCD_D/C-IO2
  • LCD_CS-IO7
  • LCD_SCL-IO1
  • LCD_SDA-IO2

屏的规格如下: https://docs.espressif.com/projects/esp-dev-kits/zh_CN/latest/_static/esp32-c3-lcdkit/datasheets/1.28_TFT_240x240_SPI_%E5%B1%8F.pdf 与设备树相关的参数如下:

  • Driver: GC9A01
  • 大小: 240*240
  • 写时钟最小周期:10ns
  • 读时钟最小周期:150ns

Zephyr 所有的 SPI 屏幕都纳入到 MIPI-DBI 模型下,因此需要查看绑定文件 zephyr/dts/bindings/mipi-dbi/zephyr,mipi-dbi-spi.yaml,屏的驱动使用 GC9A01,还要查看绑定文件 zephyr/dts/bindings/display/galaxycore,gc9x01x.yaml

/{
    /* MIPI DBI */
    mipi_dbi {
        compatible = "zephyr,mipi-dbi-spi";
        spi-dev = <&spi2>;
        dc-gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
        write-only;
        #address-cells = <1>;
        #size-cells = <0>;

        gc9a01: gc9a01@0 {
            status = "okay";
            compatible = "galaxycore,gc9x01x";
            reg = <0>;
            mipi-max-frequency = <100000000>;
            pixel-format = <PANEL_PIXEL_FORMAT_RGB_888>;
            display-inversion;
            width = <240>;
            height = <240>;
        };
    };
}

&spi2 {
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;
    pinctrl-0 = <&spim2_default>;
    pinctrl-names = "default";
    cs-gpios = <&gpio0 7 GPIO_ACTIVE_LOW>;
};
  • spi-dev 表示选择使用的 SPI 设备
  • dc-gpios 数据命令选择信号
  • write-only 只写不读
  • gc9a01 作为子节点挂在 mipi dbi 下
  • mipi-max-frequency 通信频率
    • 由于使用的是 SPI,因此这里就算指 SPI SCL 的频率,因为只写,而规格书写的最小写时钟周期为 10ns,换算为频率就是 100M
  • pixel-format 颜色格式为 RGB888
  • display-inversion 显示反转模式。从帧存储器到显示器的每个位都反转
  • width 显示器的宽
  • height 显示器的高

由于 MIPI DBI 使用了 spi2,也要对 spi2 进行配置

  • pinctrl-0/pinctrl-names 引脚的复用配置
  • cs-gpios 片选引脚

除了以上内容外,SPI 屏还有一条背光线 LCD_BL_CTRL-IO5, 我们使用 pwm 可以将其驱动起来,在 zephyr/dts/riscv/espressif/esp32c3/esp32c3_common.dtsi 已经添加了 pwm 节点 ledc0,我们按照 zephyr/dts/bindings/pwm/espressif,esp32-ledc.yaml 对其添加参数既可以。PWM LED 要参考 zephyr/dts/bindings/led/pwm-leds.yaml 绑定

/ {
    pwmleds {
        compatible = "pwm-leds";
        pwm_lcd_backlight: pwm_led_gpio0_5 {
            label = "LCD BACKLIGHT";
            pwms = <&ledc0 0 1000 PWM_POLARITY_NORMAL>;
        };
    };
};

&ledc0 {
    pinctrl-0 = <&ledc0_default>;
    pinctrl-names = "default";
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;
    channel0@0 {
        reg = <0x0>;
        timer = <0>;
    };
};
  • pwmleds 节点是将一组使用 pwd 的 led 集合在一起,其子节点 pwm_lcd_backlight 才是实际的 lcd 背光节点
  • label - 指定节点的 LABLE
  • pwms - 说明 LCB 背光关联的是哪个 pwm,参数为多少
  • pinctrl-0/pinctrl-names - 引脚的复用配置,后文说明

channel0 下

  • reg 指定使用 channel 0 的 pwm 通道
  • timer 使用 timer 0

内部设备 链接到标题

内部设备选用

  • 内置的 flash
  • 蓝牙
  • WIFI

Flash 配置 链接到标题

这个项目 flash 有 4M,不使用 mcuboot,并且要引入 nvs 和 lfs,具体如下

  • 最开始 1.5M 放 image
  • 中间的 0.5M 放 nvs
  • 最后 2M 放 lfs

在 flash0 的节点下添加 partitions 子节点,写法参考 zephyr/dts/bindings/mtd/fixed-partitions.yaml。添加 3 个 partition。 通过 zephyr,flash 指定 zephyr 使用 flash0,zephyr,code-partition 指定用 image_partition 放可执行代码。 添加 fstab 节点,用于放置文件系统节点 lfs, 写法参考 zephyr/dts/bindings/fs/zephyr,fstab,littlefs.yaml

/{
    chosen {
        zephyr,flash = &flash0;
        zephyr,code-partition = &image_partition;
    };

    fstab {
        compatible = "zephyr,fstab";
        lfs: lfs {
            compatible = "zephyr,fstab,littlefs";
            mount-point = "/lfs";
            partition = <&lfs_part>;
            automount;
            read-size = <16>;
            prog-size = <16>;
            cache-size = <64>;
            lookahead-size = <32>;
            block-cycles = <512>;
        };
    };
}

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

        image_partition: partition@0 {
            label = "image";
            reg = <0x00000000 0x00180000>;
            read-only;
        };

        storage_partition: partition@180000 {
            label = "storage";
            reg = <0x00180000 0x00080000>;
        };

        lfs_part: partition@220000 {
            label = "lfs";
            reg = <0x00200000 0x00200000>;
        };
    };
};
  • mount-point - mount 路径
  • partition - 指定文件系统使用的 partition
  • automount - 开机自动 mount
  • read-size - 文件系统读取的单位大小
  • prog-size - 文件系统写的单位大小
  • cache-size - 缓存大小
  • lookahead-size - 前瞻缓冲区的大小 ( 以字节为单位 ) 。
  • block-cycles - 将数据移至另一个块之前的擦除周期数,用于磨损均衡;

蓝牙 链接到标题

soc 的 dtsi 中已存在蓝牙节点 esp32_bt_hci, 直接启用,然后将 zephyr,bt-hci 指定使用该节点既可。

/{
    chosen {
        zephyr,bt-hci = &esp32_bt_hci;
    };

&esp32_bt_hci {
    status = "okay";
};
}

WIFI 链接到标题

soc 的 dtsi 中已存在 WIFI 节点 wifi, 直接启用既可。

&wifi {
    status = "okay";
};

引脚配置 链接到标题

前面的串口,SPI, PWM 引用了引脚配置

  • uart0_default
  • spim2_default
  • ledc0_default

这些我们可以建立一个单独的文件 esp32_c3_lcdkit-pinctrl.dtsi 进行,具体的写法可以参考 Zerphyr-pinctrl

#include <zephyr/dt-bindings/pinctrl/esp-pinctrl-common.h>
#include <dt-bindings/pinctrl/esp32c3-pinctrl.h>
#include <zephyr/dt-bindings/pinctrl/esp32c3-gpio-sigmap.h>

    uart0_default: uart0_default {
        group1 {
            pinmux = <UART0_TX_GPIO21>;
            output-high;
        };
        group2 {
            pinmux = <UART0_RX_GPIO20>;
            bias-pull-up;
        };
    };

    spim2_default: spim2_default {
        group1 {
            pinmux = <SPIM2_MOSI_GPIO2>,
                 <SPIM2_SCLK_GPIO1>,
                 <SPIM2_CSEL_GPIO7>;
        };
    };

    ledc0_default: ledc0_default {
        group1 {
            pinmux = <LEDC_CH0_GPIO5>;
            output-enable;
        };
    };

Devicetree 整合 链接到标题

实际操作过程中会建立一个 esp32_c3_lcdkit.dts,依次添加各个片段,整合到一起,新的节点放到根节点 /{} 下,修改/覆盖 soc 中已经存在的节点和 /{} 同级。此外为 zephyr,sram 指定使用 sram0,最后再加上 include 的 dtsi 就完成了。

/* esp32c3 内置封装 4M Flash soc 的 dtsi*/
# include <espressif/esp32c3/esp32c3_mini_n4.dtsi>

/* 引脚配置的 dtsi*/
# include "esp32_c3_lcdkit-pinctrl.dtsi"

/* 引用按键值 */
# include <zephyr/dt-bindings/input/input-event-codes.h>

/ {
    chosen {
        zephyr,sram = &sram0;
        zephyr,flash = &flash0;
        zephyr,code-partition = &image_partition;
        zephyr,console = &uart0;
        zephyr,shell-console = &usb_serial;
        zephyr,display = &gc9a01;
        zephyr,bt-hci = &esp32_bt_hci;
    };

    /* 旋转编码器旋转轴 */
    ec11_qdec: qdec{
        compatible = "gpio-qdec";
        gpios = <&gpio0 6 ( GPIO_PULL_UP | GPIO_ACTIVE_HIGH )>,
                <&gpio0 10 ( GPIO_PULL_UP | GPIO_ACTIVE_HIGH )>;
        steps-per-period = <4>;
        zephyr,axis = <INPUT_REL_X>;
        sample-time-us = <2000>;
        idle-timeout-ms = <200>;
    };

    /* 旋转编码器按键 */
    gpio_keys {
        compatible = "gpio-keys";
        ec11_btn: btn {
            label = "EC11 BTN";
            gpios = <&gpio0 9 ( GPIO_PULL_UP | GPIO_ACTIVE_LOW )>;
            zephyr,code = <INPUT_KEY_0>;
        };
    };

    /* SPI LCD 屏 */
    mipi_dbi {
        compatible = "zephyr,mipi-dbi-spi";
        spi-dev = <&spi2>;
        dc-gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
        write-only;
        #address-cells = <1>;
        #size-cells = <0>;

        gc9a01: gc9a01@0 {
            status = "okay";
            compatible = "galaxycore,gc9x01x";
            reg = <0>;
            mipi-max-frequency = <100000000>;
            pixel-format = <PANEL_PIXEL_FORMAT_RGB_888>;
            display-inversion;
            width = <240>;
            height = <240>;
        };
    };

    /* SPI LCD 屏背光 LED */
    pwmleds {
        compatible = "pwm-leds";
        pwm_lcd_backlight: pwm_led_gpio0_5 {
            label = "LCD BACKLIGHT";
            pwms = <&ledc0 0 1000 PWM_POLARITY_NORMAL>;
        };
    };

    /* 文件系统表挂载 lfs */
    fstab {
        compatible = "zephyr,fstab";
        lfs: lfs {
            compatible = "zephyr,fstab,littlefs";
            mount-point = "/lfs";
            partition = <&lfs_part>;
            automount;
            read-size = <16>;
            prog-size = <16>;
            cache-size = <64>;
            lookahead-size = <32>;
            block-cycles = <512>;
        };
    };
}

/* 启用 USB 串口 */
&usb_serial {
    status = "okay";
};

/* 启用串口,并配置默认波特率和引脚 */
&uart0 {
    status = "okay";
    current-speed = <115200>;
    pinctrl-0 = <&uart0_default>;
    pinctrl-names = "default";
};

/* 为 LCD 启用 spi2,并配置引脚 */
&spi2 {
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;
    pinctrl-0 = <&spim2_default>;
    pinctrl-names = "default";
    cs-gpios = <&gpio0 7 GPIO_ACTIVE_LOW>;
};

/* 为 LCD 背光启用 pwm,并配置引脚和使用的 pwm 通道 */
&ledc0 {
    pinctrl-0 = <&ledc0_default>;
    pinctrl-names = "default";
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;
    channel0@0 {
        reg = <0x0>;
        timer = <0>;
    };
};

/* 启用蓝牙 */
&esp32_bt_hci {
    status = "okay";
};

/* 启用 WIFI */
&wifi {
    status = "okay";
};

应对硬件调整 链接到标题

Zephyr应对硬件调整非常简单,就是修改Devicetree,例如我调整了SPI LCD的连线

  • LCD_CS-IO13
  • LCD_SCL-IO14
  • LCD_SDA-IO15

只用对esp32_c3_lcdkit-pinctrl.dtsi中spi2的引脚配置进行修改

    spim2_default: spim2_default {
        group1 {
            pinmux = <SPIM2_MOSI_GPIO15>,
                 <SPIM2_SCLK_GPIO14>,
                 <SPIM2_CSEL_GPIO13>;
        };
    };

同硬件不同应用功能 链接到标题

硬件不变的情况下,针对不同应用,我想做一些调整,例如:

  • 不使用蓝牙
  • 不使用uart0
  • 将usb_serial作为console

这种情况下适合使用覆盖文件,建立lcdkid_cut.overlay,内容如下

/ {
    chosen {
        /*删除属性 zephyr,bt-hci*/
        /delete-property/ zephyr,bt-hci;
    };
};

/ {
    chosen {
            /* 修改属性 */
            zephyr,console = &usb_serial;
    };
};

/* 禁用 uart0 */
&uart0 {
    status = "disabled";
}; 

/* 禁用 蓝牙 */
&esp32_bt_hci {
    status = "disabled";
};