Zephyr Devicetree 与 Kconfig 配置指南¶
1 概述¶
Zephyr Configuration System 是 PAN1080 SDK 配置各种 Feature 功能的核心框架。
实际上,zephyr 使用 CMake 进行构建,在生成 Application Image 的过程中,又可分为两大阶段:
配置阶段(Configuration Phase):此阶段下,通过执行相关目录下的
CMakeLists.txt
脚本,对当前的 Kconfig 配置、dts 配置等进行解析,最终产生一些配置输出文件(autoconf.h
、devicetree.h
、make/ninja file
等),供下一阶段使用;构建阶段(Build Phase):此阶段下,在前一阶段的输出文件的基础上,调用
GCC-ARM
编译工具链进行编译链接,最终生成可执行文件供后续烧录调试使用;
本文将介绍 zephyr 配置阶段(Configuration Phase)的基本概念和相关知识,包括 2 大部分:
Devicetree(设备树)配置;
Kconfig 选项配置;
Zephyr Configuration System 整体框架如下图所示:
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 级的配置文件(Kconifg
、Kconfig.defconfig
、Kconfig.soc
)、以及其他分散在各个目录中的相关Kconfig
文件进行解析和合并处理,最终生成 2 个文件:autoconf.h
和.config
;其中:autoconf.h
文件用于后续构建阶段的输入,而.config
文件则用于后续通过 menuconfig / guiconfig 的方式修改配置;
2 Devicetree 配置¶
2.1 基本概念¶
Devicetree 是从 Linux Kernel 借取的概念,其本质是一系列用于描述硬件的分层数据结构。 Zephyr 使用 Devicetree 来描述其支持的 soc 和 board 级硬件,以及各个硬件模块的初始配置。
前面的框图中也提到,Devicetree 有两种输入源文件: devicetree sources 与 devicetree 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 相关文件处理流程如下:
下面举一个简单的例子说明 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):
根节点:
/
根节点的直接子节点
a-node
节点
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
Devicetree 文件(
example.dts
)中,名为bar-device
的节点内,有一个compatible
属性,值为"foo-company,bar-device"
;而上述 Binding 文件,名称为foo-company,bar-device.yaml
,其中包含一个compatible: "foo-company,bar-device"
的键值对;这样就成功将 Devicetree 某个节点与对应的Binding 文件关联了起来;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 的配置。
达成以上目的的方法是(这里以pan108xxb5_evb
board为例):
在当前 App 工程的目录下,新建一个
boards
目录;在此
boards
目录下,新建一个名为pan108xxb5_evb.overlay
的文件;假设当前 App 需要修改串口 Log 打印的波特率:
在板级 Devicetree 配置文件
\zephyr\boards\arm\pan108xxb5_evb\pan108xxb5_evb.dts
中可以看到,默认的串口 Log 打印波特率为 921600:&uart0 { current-speed = <921600>; pinctrl-0 = <&p0_0_uart0_tx &p0_1_uart0_rx>; status = "okay"; };
我们在 App 工程新建的
pan108xxb5_evb.overlay
文件中,直接加入如下的语句,即可将波特率改为115200:&uart0 { current-speed = <115200>; };
更多 Devicetree Overlay 细节请参考 Zephyr 官方文档:
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 官方文档:
2.5 PAN1080 SDK 相关 Devicetree 配置文件¶
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);
board 级 dts 配置:
boards\arm\pan108xxb5_evb\pan108xxb5_evb.dts
:PAN108X-XB5 EVB 板级硬件描述;boards\arm\pan108xxb1_evb\pan108xxb1_evb.dts
:PAN108X-XB1 EVB 板级硬件描述;
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 文件pan108xxb5_evb.dts
,但是在随后的编译过程中发现如下错误:
Error: pan108xxb5_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 中出现了句法错误;我们找到输出目录中名为pan108xxb5_evb.dts.pre.tmp
的文件,定位到 864 行:
观察上下文,可以发现 863 行少了一个分号;
,在pan108xxb5_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 文件具有如下的形式(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_pan108xxb5_evb
,则编译成功后,可以在以下目录中找到autoconf.h
文件:
01_SDK\build\basic_blinky_pan108xxb5_evb
\zephyr\include\generated`
3.3 如何查找 Kconfig 定义¶
在 Zephyr App 开发过程中,我们经常会遇到以下问题:
我们在
autoconf.h
中发现有些可疑的 Kconfig 被使能,需要追踪,但却不知道其在哪里;我们预期的某个 Kconfig 宏应该打开,但在
autoconf.h
中却找不到;
这时候,我们可以利用 Menuconfig(或Gui Config)工具,追踪特定 Kconfig 选项的定义即调用关系。
这里以 Menuconfig 为例演示一种场景:
我们在 autoconf.h 中发现一处感兴趣的配置:
#define CONFIG_SPEED_OPTIMIZATIONS 1
看名字,似乎是将编译优化选项配置为速度优先的方式。
为确认此配置的真实含义,在当前工程的 VS Code 界面中按快捷键
Ctrl + Shift + B
,打开 Build 菜单,然后执行Menu Config
命令:打开的 Menuconfig 主界面如下:
按键盘上的
/
键进入 Symbol 搜索模式:在顶部搜索框中输入想要搜索的 Symbol(
SPEED_OPTIMIZATIONS
,不区分大小写):找到后,按回车键,即可跳转到对应的选项:
向 Menuconfig 工具中输入
?
字符(实际上是按键盘上的Shift + /
)键,跳转至当前 Symbol 的定义:上述界面各字段含义如下:
Name
字段表示当前 Symbol 名称为:SPEED_OPTIMIZATIONS
;Prompt
字段表示当前 Symbol 在 Menuconfig 菜单中的显示名为:Optimize for speed
;Type
字段表示当前 Symbol 类型为:bool
;Name
字段表示当前 Kconfig Symbol 名称为:SPEED_OPTIMIZATIONS
;Value
字段表示当前 Symbol 的使能情况为:y
,表示 yes;Help
字段是当前 Symbol 的帮助信息(字符串),一般用于描述此 Symbol 的功能或用法;Direct dependencies
字段列出了当前 Symbol 的依赖关系,对于SPEED_OPTIMIZATIONS
来说,其直接依赖于<choice>
菜单选项,并不依赖其他任何 Kconfig 选项;At Kconfig.zephyr:291
及之后的 2 行描述,提供了 2 个信息:此 Symbol 在 Kconfig 文件中的定义位置为 zephyr 目录顶层
Kconfig.zephyr
文件的第 291 行;此 Symbol 在 Menuconfig 中的菜单层次为
Menu path: (Top) -> Build and Link Features -> Compiler Options -> Optimization level
;有些 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 源文件中修改配置,也有多种途径可供:
直接修改 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 工程,一般不推荐使用。
在当前 board 目录中的
pan108xxb5_evb_defconfig
文件中,新增CONFIG_SIZE_OPTIMIZATIONS=y
语句,将当前 board 的编译优化配置强制修改为新的配置:注:此方法会影响到某个特定 board 编译的所有 App 工程,仅当确实这种需求的时候才推荐这么修改。
在当前 App 工程目录中的
prj.conf
文件中,新增CONFIG_SIZE_OPTIMIZATIONS=y
语句,将当前 App 工程的编译优化配置强制修改为新的配置:注:此方法仅会影响当前 App 工程,在没有特殊需求的场景下,推荐使用这种方式。
注:更多 Kconfig 配置方法请参考 Zephyr 官方文档:Setting Kconfig configuration values。
5 Devicetree 与 Kconfig 的关系¶
Devicetree 是用来描述 SoC 级和板级硬件资源的易读文本,同时也给系统上电的初始化阶段提供各个硬件的初始化配置;
Kconfig 是用来配置系统各个软件功能模块的一种机制,其中的大部分配置都是与硬件无关的;
但是,有一部分 Kconfig 配置是与硬件有关(也即与 Devicetree 有关),那就是 Zephyr Drivers。对于一个特定的 Zephyr Driver,我们:
使用 Kconfig 来决定是否将此 Driver 源码加入编译系统
在特定 Driver 被加入编译系统的前提下,Driver 内代码才会从 Devicetree 中获取具体的配置,如是否初始化、初始化的各个参数配置等
如果某个 Driver 未在 Kconfig 中使能,则无论 Devicetree 中的配置
例如,为使能 UART Driver,我们需要先在 Kconfig 中配置
CONFIG_SERIAL=y
,然后在 Devicetree 中,将对应 UART 的status
属性 配置为okay
,之后在系统启动过程中,才会按照当前 Devicetree 中的描述,配置 UART 的其他参数。
6 更多相关文档¶
Zephyr Build and Configuration Systems:Zephyr官方对于Build和Configuration的整体流程介绍
Zephyr Kconfig System:Zephyr官方Kconfig介绍
Devicetree Guide:Zephyr官方Devicetree指南
Zephyr Kconfig Options:Zephyr官方支持的所有Kconfig选项列表