当前文档版本为v0.3.0,您可以访问 开发中 的文档以获取最近可能的更新。

Zephyr Devicetree 与 Kconfig 配置指南

1 概述

Zephyr Configuration System是PAN1080 SDK配置各种Feature功能的核心框架。

实际上,zephyr使用CMake进行构建,在生成Application Image的过程中,又可分为两大阶段:

  1. 配置阶段(Configuration Phase):此阶段下,通过执行相关目录下的CMakeLists.txt脚本,对当前的Kconfig配置、dts配置等进行解析,最终产生一些配置输出文件(autoconf.hdevicetree.hmake/ninja file等),供下一阶段使用;

  2. 构建阶段(Build Phase):此阶段下,在前一阶段的输出文件的基础上,调用GCC-ARM编译工具链进行编译链接,最终生成可执行文件供后续烧录调试使用;

本文将介绍zephyr配置阶段(Configuration Phase)的基本概念和相关知识,包括2大部分:

  • Devicetree(设备树)配置;

  • Kconfig选项配置;

Zephyr Configuration System整体框架如下图所示:

image

Zephyr使用CMake实现了以上所有流程,CMake从App工程目录下的CMakeLists.txt脚本开始执行,在执行过程中,此文件又会引用zephyr根目录下的CMakeLists.txt脚本,在其执行过程中又会转而引用分散在各个待编译目录下的CMakeLists.txt脚本,最终生成一组Ninja(类似于Makefile)文件供后续编译使用。

而在CMake执行过程中,除了生成Ninja文件外,还会调用相关脚本工具,解析Devicetree和Kconfig文件,并生成对应的.h文件供后续使用:

  • Devicetree

    • *.dts (DeviceTree Source)和*.dtsi (DeviceTree Source Include)文件由构建系统从architecture、soc、 board、app等目录中收集;

    • *.dtsi文件由#include的方式包含在*.dts文件中,并由C预处理器进行解析;另一方面,C预处理器同时还会解析*.overlay 文件;在解析的过程中,其中的宏定义也将被展开;

    • 经过预处理后的Devicetree文件将会合并保存至一个名为*.dts.pre.tmp的文件中,此文件一方面会被一个名为DeviceTree Compiler (dtc)的工具检查,确保其中没有语法错误;另一方面会作为源文件,输入给dts工具进行后续处理;

    • 上述dts工具在处理*.dts.pre.tmp文件的同时,还会从zephyr/dts/bindings目录下拉取相关的*.yaml文件,对dts中的各个property值进行检查,检查通过后会生成一个名为zephyr.dts的文件,供后续参考;

    • zephyr.dts文件以外,dts工具还会生成一个名为devicetree_unfixed.h的头文件,此文件会与另一个名为devicetree_fixups.h的头文件一起,被包含在devicetree.h中,作为后续构建阶段的输入;

  • Devicetree fixups

    • 在architecture、soc、 board、app等目录中可能会存在一些名为dts_fixup.h的头文件,这些文件用于重命名devicetree_unfixed.h中的一些宏,以满足的某些app的需求;

      :PAN1080 SDK中所有的app代码均直接使用zephyr.dts对应的devicetree_unfixed.h文件,因此无需关注dts_fixup.h

  • Kconfig

    • Kconfig文件分布在architecture、soc、 board、app等目录中,其定义了各个目录模块中当前的配置选项以及各选项之间的依赖关系;

    • 构建系统调用Kconfig工具,将app级的配置文件(*.conf)、board级的配置文件(*Kconfig.defconfig/Kconfig.board/*_defconfig)、soc级的配置文件(KconifgKconfig.defconfigKconfig.soc)、以及其他分散在各个目录中的相关Kconfig文件进行解析和合并处理,最终生成2个文件:autoconf.h.config;其中:autoconf.h文件用于后续构建阶段的输入,而.config文件则用于后续通过menuconfig/guiconfig的方式修改配置;

2 Devicetree 配置

2.1 基本概念

Devicetree是从Linux Kernel借取的概念,其本质是一系列用于描述硬件的分层数据结构。 Zephyr使用Devicetree来描述其支持的soc和board级硬件,以及各个硬件模块的初始配置。

前面的框图中也提到,Devicetree有两种输入源文件: devicetree sourcesdevicetree bindings,其中devicetree sources是后缀为.dts.dtsi的文件,用于描述Devicetree自身,而 devicetree bindings则是后缀为.yaml的文件,其用于描述 devicetree sources文件中各个属性的定义和数据类型。

Devicetree的最终输出为一个名为devicetree_unfixed.h的头文件,而zephyr提供了一个名为devicetree.h的文件,其在包含devicetree_unfixed.h的同时,还提供了可以访问devicetree的API接口(函数式宏),它们都以DT_开头,zephyr在编译时会默认包含此文件,保证devicetree中的内容可以在各个C文件中访问到。

一个简化的devicetree相关文件处理流程如下:

image

下面举一个简单的例子说明devicetree的基本语法。

正如其名,devicetree本质上是一个树(tree)状的数据结构,其对应一种名为DTS (DeviceTree Source)的易读文本格式(关于此格式的SPEC细节可参考 Devicetree specification)。

一个简单而完整的DTS文件内容如下:

/dts-v1/;

/ {
        a-node {
                subnode_label: a-sub-node {
                        foo = <3>;
                };
        };
};

其中:

  • 第1行 /dts-v1/;表示当前DTS格式的版本号为version 1(用于区分已经废弃的version 0版本);

  • 其后的内容表示此树(tree)包含3个节点(nodes):

    1. 根节点: /

    2. 根节点的直接子节点a-node

    3. 节点a-node的直接子节点 a-sub-node,

节点可以有一个**唯一(不重名)**的标签(labels),在dts文件的其他位置中可以通过标签名的方式快速引用其他节点。例如,上述dts文件中,a-sub-node的即有一个名为subnode_label的标签。一个节点可以有0个、1个或多个标签。

除标签以外,还可以使用类似Unix文件系统中的完整路径来描述节点,上述dts文件中,a-sub-node 的完整路径为/a-node/a-sub-node

Devicetree节点中可以定义属性(properties),属性实际上是键值对(name/value pairs)。属性的值可以是任意字节串(属性值的具体含义需要在当前节点对应的dts bindings文件中定义)。

上述dts文件中 a-sub-node 节点中包含了一个名为foo的属性,其数据是一个int类型,值为3

:更多Devicetree基本概念介绍请参考Zephyr官方文档:Introduction to devicetree

2.2 Devicetree Bindings

Devicetree设备树本身只是Zephyr硬件描述机制的半壁江山,因为其是一种非结构化(unstructured)的格式;换句话说,仅用.dts文件是无法完整准确描述硬件的,原因是其虽然包含了完整的节点(Node)和属性(Property)描述,但其自身是无法确保这些描述准确无误的。于是Zephyr硬件描述机制的另外半壁江山:Devicetree Bindings就应运而生了。

Devicetree Bindings文件(*.yaml)用于描述Devicetree文件(*.dts)中各个节点(Node)及其中各个属性(Property)的准确含义,为它们提供必要的语义信息(semantic information)。

需要注意的是,Zephyr devicetree bindings文件是一组自定义内容的YAML格式文件,并不与Linux kernel共用的dt-schema工具。

下面举一个简单的例子阐述构建系统是如何将Devicetree与Devicetree Bingdings关联起来的。

一个简单的Devicetree文件中(example.dts)含有如下所示的节点:

/* Node in a DTS file */
bar-device {
     compatible = "foo-company,bar-device";
     num-foos = <3>;
};

而其对应的bindings文件(foo-company,bar-device.yaml)内容如下:

# A YAML binding matching the node

compatible: "foo-company,bar-device"

properties:
  num-foos:
    type: int
    required: true
  1. Devicetree文件(example.dts)中,名为bar-device的节点内,有一个compatible属性,值为"foo-company,bar-device";而上述Binding文件,名称为foo-company,bar-device.yaml,其中包含一个compatible: "foo-company,bar-device"的键值对;这样就成功将Devicetree某个节点与对应的Binding文件关联了起来;

  2. Devicetree文件中的bar-device的节点还描述了一个名为num-foos的节点,其值为3;同时我们看到对应的Binding文件中,描述了一个名为num-foos的属性,其类型(type)为int,表示整型数据,而需求(required)为true,表示此属性为必需:当构建系统在当前dts节点中发现此属性未赋值时会直接报错;

:更详细的Devicetree Bindings介绍请参考官方文档:Devicetree bindings

2.3 Devicetree Overlays

普通的Devicetree文件(.dts/.dtsi),一般用于描述SoC级板级硬件参数,但有时候我们会遇到这种场景:

板级Devicetree文件中配置的硬件参数,可适用于大多数App工程;但某个特殊的App工程,需要做额外的修改才能正常工作。

我们不希望专门为某个特殊的App需求直接修改板级Devicetree文件,因为这可能会影响其他App的工作,这时候,我们可以在此特殊的App工程下,加入一个Devicetree Overlay文件,用来覆盖板级Devicetree的配置。

达成以上目的的方法是(这里以pan1080a_afld_evb board为例):

  1. 在当前App工程的目录下,新建一个boards目录;

  2. 在此boards目录下,新建一个名为pan1080a_afld_evb.overlay的文件;

  3. 假设当前App需要修改串口Log打印的波特率:

    • 在板级Devicetree配置文件\zephyr\boards\arm\pan1080a_afld_evb\pan1080a_afld_evb.dts中可以看到,默认的串口Log打印波特率为921600:

      &uart1 {
      	current-speed = <921600>;
      	pinctrl-0 = <&p0_6_uart1_tx &p0_7_uart1_rx>;
      	status = "okay";
      };
      
    • 我们在App工程新建的pan1080a_afld_evb.overlay文件中,直接加入如下的语句,即可将波特率改为115200:

      &uart1 {
      	current-speed = <115200>;
      };
      

更多Devicetree Overlay细节请参考Zephyr官方文档:

  1. Set Devicetree Overlays

  2. Use Devicetree Overlays

2.4 如何从C代码中获取 Driver 的 device 结构体指针

在开发App的过程中,经常会需要调用Zephyr Driver API操作SoC的外设,而所有的Zephyr Driver API接口均需要传入一个类型为device的结构体指针,而此指针只能通过调用Devicetree相关API进行间接获取。

这里我们介绍获取device的结构体指针的方法。

假设我们有如下的Devicetree文件片段,我们需要获取UART节点serial@40002000对应的device结构体指针:

 {
        soc {
                serial0: serial@40002000 {
                        status = "okay";
                        current-speed = <115200>;
                        /* ... */
                };
        };

        aliases {
                my-serial = &serial0;
        };

        chosen {
                zephyr,console = &serial0;
        };
};

我们可以先在C源文件中使用如下方式之一,定义一个名为MY_SERIAL的宏,获取UART0节点的标识符(Identifier):

/* 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)

接着,我们可是有如下2种方式之一获取UART0对应的device结构体指针:

/* Option 1: use device_get_binding() */
#if DT_NODE_HAS_STATUS(MY_SERIAL, okay)
const struct device *uart_dev = device_get_binding(DT_LABEL(MY_SERIAL));
#else
#error "Node is disabled"
#endif

/* Option 2: use DEVICE_DT_GET() */
const struct device *uart_dev = DEVICE_DT_GET(MY_SERIAL);

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

UART0对应的device结构体指针获取成功后,即可调用UART Driver API接口进行后续操作。

:更多C代码中访问Devicetree的介绍请参考Zephyr官方文档:

  1. Get a struct device from a devicetree node

  2. Devicetree access from C/C++

  3. Devicetree API

2.5 PAN1080 SDK 相关 Devicetree 配置文件

  1. soc级dts配置:

    • zephyr\dts\arm\panchip\pan1080\pan1080.dtsi:PAN1080硬件描述;

    • zephyr\dts\arm\panchip\pan1080\pan1080xb1.dtsi:PAN1080XB1 SoC硬件描述;

    • zephyr\dts\arm\panchip\pan1080\pan1080xb5.dtsi:PAN1080XB5 SoC硬件描述;

    • zephyr\dts\arm\panchip\pan1080\pan1080xx1_pinctrl.dtsi :PAN1080XX1 SoC对应的PINMUX硬件描述(32 Pin);

    • zephyr\dts\arm\panchip\pan1080\pan1080xx5_pinctrl.dtsi:PAN1080XX5 SoC对应的PINMUX硬件描述(64 Pin);

  2. board级dts配置:

    • boards\arm\pan1080a_afld_evb\pan1080a_afld_evb.dts:PAN1080A_AFLD_EVB板级硬件描述;

    • boards\arm\pan1080a_afx_evb\pan1080a_afx_evb.dts:PAN1080A_AFX_EVB板级硬件描述;

  3. dts-bindings:

    • dts\bindings\<driver>\*.yaml:各个硬件模块的dts属性对应数据定义;

2.6 如何调试 Devicetree 编译错误

有时候我们在修改Devicetree(*.dts)文件的过程中会出现错误,zephyr提供了一些方法,用于帮助我们找到错误的原因。

前面介绍Zephyr Configuration System整体框架的时候提到,在生成zephyr.dts的过程中,会先产生一个临时的用于调试的文件*.dts.pre.tmp,如果出现dts编译错误,可能无法成功生成zephyr.dts,但*.dts.pre.tmp文件仍然会生成,这时候只需阅读编译错误的Log,即可在*.dts.pre.tmp文件中找到错误位置。

例如,我们尝试修改板级dts文件pan1080a_afld_evb.dts,但是在随后的编译过程中发现如下错误:

Error: pan1080a_afld_evb.dts.pre.tmp:864.1-8 syntax error
FATAL ERROR: Unable to parse input tree
CMake Error at D:/PDK/pan1080-dk-internal/01_SDK/zephyr/cmake/dts.cmake:225 (message):
  command failed with return code: 1
Call Stack (most recent call first):
  D:/PDK/pan1080-dk-internal/01_SDK/zephyr/cmake/app/boilerplate.cmake:541 (include)
  D:/PDK/pan1080-dk-internal/01_SDK/zephyr/share/zephyr-package/cmake/ZephyrConfig.cmake:24 (include)
  D:/PDK/pan1080-dk-internal/01_SDK/zephyr/share/zephyr-package/cmake/ZephyrConfig.cmake:40 (include_boilerplate)
  CMakeLists.txt:4 (find_package)

从第一行提示可知,dts中出现了句法错误;我们找到输出目录中名为pan1080a_afld_evb.dts.pre.tmp的文件,定位到864行:

image

观察上下文,可以发现863行少了一个分号;,在pan1080a_afld_evb.dts中修复后,重新编译app工程,即可发现问题解决。

:更多Devicetree调试技巧请参考Zephyr官方文档:Troubleshooting devicetree

3 Kconfig 配置

3.1 基本概念

Zephyr的内核(Kernel)和子系统(Subsystem)支持的Features均可以根据实际应用需求,在构建阶段进行配置,而配置的方法是通过一个名为Kconfig的机制进行的。正如其名,Zephyr Kconfig机制在概念上与Linux Kernel Kconfig是基本相同的。

配置选项(通常也称为 symbols)在一系列Kconfig文件中定义,其中还描述了各个配置选项之间的依赖关系和层次关系等信息。

Kconfig机制的最终有效输出是一个名为autoconf.h的头文件,此文件会在编译阶段作为最优先的头文件包含在各个C源文件中,供各个文件编译使用。autoconf.h中名为CONFIG_XXX的宏定义,其在Kconfig文件中的对应定义是config XXX

Kconfig Symbol名称与autoconf.h中宏定义的关系为:

一个典型的Kconfig文件具有如下的形式(zephyr/Kconfig.zephyr文件片段):

...
source "boards/Kconfig"
source "soc/Kconfig"
source "arch/Kconfig"
source "kernel/Kconfig"
source "dts/Kconfig"
source "drivers/Kconfig"
source "lib/Kconfig"
source "subsys/Kconfig"

osource "$(TOOLCHAIN_KCONFIG_DIR)/Kconfig"

...

menu "Linker Options"

choice
	prompt "Optimization level"
	default NO_OPTIMIZATIONS    if COVERAGE
	default DEBUG_OPTIMIZATIONS if DEBUG
	default SPEED_OPTIMIZATIONS
	help
	  Note that these flags shall only control the compiler
	  optimization level, and that no extra debug code shall be
	  conditionally compiled based on them.

config SIZE_OPTIMIZATIONS
	bool "Optimize for size"
	help
	  Compiler optimizations will be set to -Os independently of other
	  options.

config SPEED_OPTIMIZATIONS
	bool "Optimize for speed"
	help
	  Compiler optimizations will be set to -O2 independently of other
	  options.

config DEBUG_OPTIMIZATIONS
	bool "Optimize debugging experience"
	help
	  Compiler optimizations will be set to -Og independently of other
	  options.

config NO_OPTIMIZATIONS
	bool "Optimize nothing"
	help
	  Compiler optimizations will be set to -O0 independently of other
	  options.

endchoice
...

:更详细的Kconfig基本概念请参考Zephyr官方文档:Zephyr Kconfig System

3.2 如何确认当前App开启了哪些Kconfig

根据前面介绍,所有使能的Kconfig选项,最终会合并输出到一个名为autoconf.h的头文件中。因此在成功编译App后,可以从生成目录中找到autoconf.h文件,即可了解当前哪些Kconfig选项被使能了。

这里以编译blinky例程为例,假设输出目录为01_SDK\build\basic_blinky_pan1080a_afld_evb,则编译成功后,可以在以下目录中找到autoconf.h文件:

  • 01_SDK\build\basic_blinky_pan1080a_afld_evb\zephyr\include\generated`

image

3.3 如何查找Kconfig定义

在Zephyr App开发过程中,我们经常会遇到以下问题:

  1. 我们在autoconf.h中发现有些可疑的Kconfig被使能,需要追踪,但却不知道其在哪里;

  2. 我们预期的某个Kconfig宏应该打开,但在autoconf.h中却找不到;

这时候,我们可以利用Menuconfig(或Gui Config)工具,追踪特定Kconfig选项的定义即调用关系。

这里以Menuconfig为例演示一种场景:

  1. 我们在autoconf.h中发现一处感兴趣的配置:

    #define CONFIG_SPEED_OPTIMIZATIONS 1
    

    看名字,似乎是将编译优化选项配置为速度优先的方式。

  2. 为确认此配置的真实含义,在当前工程的VS Code界面中按快捷键Ctrl + Shift + B,打开Build菜单,然后执行Menu Config命令:

    image

  3. 打开的Menuconfig主界面如下:

    image

  4. 按键盘上的/键进入Symbol搜索模式:

    image

  5. 在顶部搜索框中输入想要搜索的Symbol(SPEED_OPTIMIZATIONS,不区分大小写):

    image

  6. 找到后,按回车键,即可跳转到对应的选项:

    image

  7. 向Menuconfig工具中输入?字符(实际上是按键盘上的Shift + /)键,跳转至当前Symbol的定义:

    image

    上述界面各字段含义如下:

    1. Name字段表示当前Symbol名称为:SPEED_OPTIMIZATIONS

    2. Prompt字段表示当前Symbol在Menuconfig菜单中的显示名为:Optimize for speed

    3. Type字段表示当前Symbol类型为:bool

    4. Name字段表示当前Kconfig Symbol名称为:SPEED_OPTIMIZATIONS

    5. Value字段表示当前Symbol的使能情况为:y,表示yes;

    6. Help字段是当前Symbol的帮助信息(字符串),一般用于描述此Symbol的功能或用法;

    7. Direct dependencies字段列出了当前Symbol的依赖关系,对于SPEED_OPTIMIZATIONS来说,其直接依赖于<choice>菜单选项,并不依赖其他任何Kconfig选项;

    8. At Kconfig.zephyr:291及之后的2行描述,提供了2个信息:

    9. 此Symbol在Kconfig文件中的定义位置为zephyr目录顶层Kconfig.zephyr文件的第291行;

    10. 此Symbol在Menuconfig中的菜单层次为Menu path: (Top) -> Build and Link Features -> Compiler Options -> Optimization level

    11. 有些Symbol Info比较多,一页无法显示完全,此时可以按键盘的上下键和翻页键进行翻页查看;翻页后可以看到再后面是当前Symbol在Kconfig源文件中的完整定义信息:

      config SPEED_OPTIMIZATIONS
      	bool "Optimize for speed"
      	help
      	  Compiler optimizations will be set to -O2 independently of other
      	  options.
      

    :更多Menuconfig使用技巧请参考Zephyr官方文档:Interactive Kconfig interfaces

3.4 如何修改Kconfig

在实际开发过程中,我们经常需要修改Kconfig配置,以满足各种不同的需求。

这里仍然以前面编译的blinky例程为例,分别介绍2种比较常用的修改Kconfig配置的方法,将此App的编译优化选项由SPEED_OPTIMIZATIONS修改为SIZE_OPTIMIZATIONS

3.4.2 直接编辑 config 相关源文件进行修改(长久方式)

我们可以使用另一种更直接的方式修改Kconfig配置:直接修改config相关的源文件。

从config源文件中修改配置,也有多种途径可供:

  1. 直接修改Kconfig定义中的default值,例如,直接将Kconfig.zephyr中的 choice改为default SIZE_OPTIMIZATIONS

    choice
    	prompt "Optimization level"
    	default NO_OPTIMIZATIONS    if COVERAGE
    	default DEBUG_OPTIMIZATIONS if DEBUG
    	default SIZE_OPTIMIZATIONS
    	help
    	  Note that these flags shall only control the compiler
    	  optimization level, and that no extra debug code shall be
    	  conditionally compiled based on them.
    

    :此方法会影响到所有board编译的App工程,一般不推荐使用。

  2. 在当前board目录中的pan1080a_afld_evb_defconfig文件中,新增CONFIG_SIZE_OPTIMIZATIONS=y语句,将当前board的编译优化配置强制修改为新的配置:

    image

    :此方法会影响到某个特定board编译的所有App工程,仅当确实这种需求的时候才推荐这么修改。

  3. 在当前App工程目录中的prj.conf文件中,新增CONFIG_SIZE_OPTIMIZATIONS=y语句,将当前App工程的编译优化配置强制修改为新的配置:

    image

    :此方法仅会影响当前App工程,在没有特殊需求的场景下,推荐使用这种方式。

:更多Kconfig配置方法请参考Zephyr官方文档:Setting Kconfig configuration values

5 更多相关文档

  1. Zephyr Build and Configuration Systems:Zephyr官方对于Build和Configuration的整体流程介绍

  2. Zephyr Kconfig System:Zephyr官方Kconfig介绍

  3. Devicetree Guide:Zephyr官方Devicetree指南

  4. Zephyr Kconfig Options:Zephyr官方支持的所有Kconfig选项列表