Digging (a little bit) into KConfig and device tree
Understanding program and board configurations
In order to program a Zephyr RTOS application for a specific target device, you
need to use different configuration tools: Kconfig and DTS
. This codelab
introduces these tools and explains the basic principles behind them. The goal
is not to bring an in-depth understanding of all application and board
configurations, but rather to enable you to configure an application for a board
fully supported by the Zephyr RTOS ecosystem.
What you’ll build
- How to configure an application and compile it using the different Zephyr Development Environment and Zephyr RTOS.
- How to add specific application and board configuration parameters.
What you’ll learn
- The basic principles behind Kconfig and
DTS
. - How to use some tools that help with the configuration.
What you’ll need
- Zephyr Development Environment for developing and debugging C++ code snippets.
- The getting started codelab is a prerequisite for this codelab.
Digging into Kconfig
As you learned earlier, Zephyr RTOS applications can be configured using the application configuration file, which is named “prf.conf” by default. This file contains the definition of the symbols used to configure the build process.
The symbols for which values are defined in the “prf.conf” file are declared in Kconfig files. Kconfig is a concept borrowed from the Linux kernel configuration system. It uses a hierarchy of configuration files that ultimately results in the declaration of a hierarchy of configuration options or symbols. The build system uses these symbols to include or exclude files from the build process. It also uses the symbols in the source code itself as symbols used by the precompiler.
With Zephyr RTOS, west uses Kconfig as part of the build process.
Visualizing the Configuration Options using menuconfig or guiconfig
To configure the options of a Zephyr RTOS application, the developer must navigate through the hierarchy of Kconfig files to understand the hierarchy of configuration symbols. This is a tedious task, and west provides an interactive Kconfig interface to facilitate this task. To run the interface for a specific application (here the “blinky” application), you will need to:
- Run
west build -b nrf5340dk/nrf5340/cpuapp blinky
. - Run
west build -t menuconfig
orwest build -t guiconfig
. Both commands provide an interface that makes configuration much easier. With the use of these interfaces the understanding of each symbol and of the symbol hierarchy is made much easier.
The guiconfig
interface is illustrated below:
Learning Kconfig with an Example
To explain how Kconfig works in detail, a good example is the Zephyr RTOS logging subsystem that we learned how to use earlier. Part of the logging subsystem Kconfig definition is shown below:
menu "Logging"
config LOG
bool "Logging"
select PRINTK if USERSPACE
help
Global switch for the logger, when turned off log calls will not be
compiled in.
if LOG
config LOG_CORE_INIT_PRIORITY
int "Log Core Initialization Priority"
range 0 99
default 0
rsource "Kconfig.mode"
...
This definition can be understood as follows:
-
menu
is the definition of the menu name displayed when usingmenuconfig
orguiconfig
, as shown in Figure 1. -
config LOG
is the definition of theLOG
symbol. In the Kconfig nomenclature, it is a menu entry. -
A menu entry can have a number of attributes. For the
config LOG
menu entry:-
A type definition: The symbol is defined as a
bool
and an application can define the use of the symbol asCONFIG_LOG=y
(to enable logging) orCONFIG_LOG=n
(to disable logging). -
A reverse dependency:
select PRINTK if USERSPACE
forces the value of thePRINTK
symbol to the logicalAND
of the value of the menu symbolLOG
and of the symbolUSERSPACE
. If both values are true, thenPRINTK
will be set to true. -
help
contains the explanation of the symbol as shown in Figure 1.
-
-
The Kconfig file above contains further menu items that are defined only if the value of the
LOG
menu item is true (usingif LOG
).config LOG_CORE_INIT_PRIORITY
is one of these menu items. -
The
config LOG_CORE_INIT_PRIORITY
menu item contains the following attributes:- A type definition
int
that defines the menu item to be an integer. - A
range
attribute that specifies acceptable values for the menu item. -
A
default
attribute that specifies the default value if it is not specified in the application prj.conf file. -
rsource "Kconfig.mode"
is a Kconfig extension defined in the Kconfiglib. It tells the build system to include the file specified with a path relative to the Kconfig file.
- A type definition
More details on the Kconfig language can be found here. Details about the Kconfig extensions used by west can be found here.
How are Kconfig Definitions Used?
For a better understanding of the use of the configuration parameters, it is useful to
have a look at the definition of the CONFIG_PRINTK
symbol, which is also used in the logging
subsystem. The declaration of the printk()
function depends on the definition of the CONFIG_PRINTK
,
as shown in the source code of the Zephyr RTOS printk()
function (simplified here):
...
#ifdef CONFIG_PRINTK
void printk(const char *fmt, ...);
#else
static inline void printk(const char *fmt, ...)
{
ARG_UNUSED(fmt);
}
#endif
...
CONFIG_PRINTK
is not defined, printk()
will be replaced by a dummy
function and any call to printk()
will be removed by the compiler. To verify
this behaviour, you can
- Add a call to
printk()
in themain()
function. - Add the option
CONFIG_PRINTK=n
in the prj.conf file. - Build the application again with the command
west build -b nrf5340dk/nrf5340/cpuapp blinky --pristine
. - Flash your board using the
west flash
command.
You would expect the printk()
call not to print anything in the console, but even though
printk()
is disabled, the message is still displayed! Why is this happening? From the
source code, the only possible explanation is that the CONFIG_PRINTK
definition ended up with CONFIG_PRINTK=y
, not as configured in our prj.conf
file. The explanation lies in how the hierarchy of Kconfig files is
combined to build the hierarchy of options.
How are Kconfig files combined?
To understand the printk()
behaviour explained above, you can observe the following:
- When building an application, west generates a number of output files.
One of these is the file containing all the Kconfig settings
(“build/zephyr/.config”). If you look for
CONFIG_PRINTK
in this file, you will see that it is defined asbuild/zephyr/.config... CONFIG_PRINTK=y ...
- If you look at the console output while building the application, you will see
a warning message:
console
warning: PRINTK (defined at subsys/debug/Kconfig:220) was assigned the value 'n' but got the value 'y'.
Both of these observations confirm that the CONFIG_PRINTK
symbol was not set
as expected from the prj.conf file. The reasons is two-fold:
- In the Zephyr RTOS system, there are hundreds of Kconfig files (called fragments in the Zephyr RTOS nomenclature) that are combined at build time. The way the configuration options are finally built is explained in detail in the official Zephyr RTOS documentation and will not be repeated here. It is important to note that the prj.conf file is only a small part of how configuration parameters are built.
- As explained earlier, Kconfig files contain menu items, each of which
can have dependencies (using
select
orimply
). These dependencies may enable some options that conflict with the options set at the application level.
The easiest way to understand how the CONFIG_PRINTK
option is finally set to
CONFIG_PRINTK=y
is to start the guiconfig by running west build -t
guiconfig
. If you do this, you will see that the PRINTK
option is set to y
,
and that the reason for this is that the BOOT_BANNER
symbol has a dependency
on the PRINTK
symbol, and that it selects
it, as shown in Figure 2.
This dependency is visible in the kernel Kconfig file
...
config BOOT_BANNER
bool "Boot banner"
default y
select PRINTK
select EARLY_CONSOLE
help
This option outputs a banner to the console device during boot up.
printk()
and therefore needs to set a dependency on the PRINTK
option. If we really want to disable PRINTK
, we need to add the following line
to our prj.conf file.
CONFIG_BOOT_BANNER=n
Note that you may also need to disable all logging options to prevent any
warnings at build time. If you make this change and reflash your board, you will
see that the boot banner and the printk()
message are no longer displayed.
Searching for Kconfig options
There exists another online
tool
provided by Zephyr RTOS to browse the available configuration options and
understand their meaning, type and dependencies. If you search for
CONFIG_PRINTK
, then the search system will display the following result:
Using Kconfig for Specifying Compiler Optimizations
When building any application written in C++, the compiler may apply different
optimization rules such as -O1
or -Oz
. Although this is possible, the
compiler optimization options are usually not defined in the CMakeLists.txt
file using the zephyr_library_compile_options
definition. The preferred way to
define different optimisation options and build types is to use alternative
application configuration files.
If you search for CONFIG_COMPILER_OPTIMIZATIONS
on the Kconfig
search tool,
you will see the following output:
This explains how the CONFIG_COMPILER_OPTIMIZATIONS
option can be set and its
dependencies. If you want to use a different compiler optimisation, such as a
release build type, you can copy the existing prj.conf file, rename it and
add the following configuration:
CONFIG_COMPILER_OPTIMIZATIONS=CONFIG_SPEED_OPTIMIZATIONS
You can then start another build by specifying the alternate configuration file
with the command west build -b nrf5340dk/nrf5340/cpuapp blinky --pristine --
-DCONF_FILE=prj_release.conf
. As explained in the official
documentation,
additional arguments can be passed to the CMake invocation performed by west
build after a --
at the end of the west build command line.
Using Kconfig for Board Specific Configuration
As explained in the getting started codelab, it is also possible to define configuration parameters that are specific to a board. To do so, add a “{board}.conf” file to the application’s “boards” directory. For example, if you want to define configuration parameters that apply only to your nrf5340dk/nrf5340/cpuapp device, you may add a “nrf5340dk/nrf5340/cpuapp.conf” to the “boards” folder. When building the application, you should see an output similar to the following in the terminal:
Parsing D:/aes/blinky/Kconfig
Loaded configuration 'D:/aes/deps/zephyr/boards/nordic/nrf5340dk/nrf5340dk_nrf5340_cpuapp_defconfig'
Merged configuration 'D:/aes/blinky/prj.conf'
Merged configuration 'D:/aes/blinky/boards/nrf5340dk_nrf5340_cpuapp.conf'
Configuration saved to 'D:/aes/build/zephyr/.config'
Kconfig header saved to 'D:/aes/build/zephyr/include/generated/zephyr/autoconf.h'
Device Tree Basics
devicetree
is a data structure used in Linux and Zephyr RTOS to describe the hardware layout of a board.
It provides a hardware description that is separate from code, enabling reusable and portable drivers.
A devicetree
describes:
- SoC (System-on-Chip) peripherals.
- Memory layout (Flash, RAM).
- On-board sensors, LEDs, buttons, etc.
- External components connected via I²C/SPI/UART.
A devicetree
is described in so-called devicetree source
(DTS
) files. The Zephyr RTOS toolchain
parses the DTS
files at build time and generates C macros and
defines to configure drivers and applications.
DTS
Files
Zephyr RTOS uses the standard .dts
and .dtsi
file format. The key file types are:
.dts
(DeviceTree Source)
The main file describing the board’s hardware..dtsi
(DeviceTree Include)
Shared fragments included by other.dts
files (like SoC-level definitions)..overlay
Application-specific modifications or additions to the board’s base.dts
file.
When building an Zephyr RTOS for a specific board, the toolchain searches for
the board specific DTS
file. For our nrf5340dk/nrf5340/cpuapp device, this file is the “zephyr/boards/nordic/nrf5340dk/nrf5340dk_nrf5340_cpuapp.dts” file.
This file includes other files from the following hierarchy:
- “dts/arm/nordic/*.dtsi”: contains all “.dtsi” files included in Nordic board specific “.dts” files.
- “dts/arm/
.dtsi”: contains the SoC-level description for the board, here “armv8-m.dtsi”.
Node Structure in DTS
Files
As the name indicates, a devicetree
is a tree. The text format for specifying a devicetree
is
DTS
.
An example of a DTS
file is:
/dts-v1/;
/ {
a-node {
subnode_nodelabel: a-sub-node {
foo = <3>;
};
};
};
/dts-v1/;
specifies the version of the DTS syntax that is used.
The remaining of the file specifies a tree/hierarchy of nodes. In this example, the hierarchy is:
- a root node specified by ‘/’.
- a node named
a-node
, child of the root node. - a node named
a-sub-node
, child of thea-node
node.
It is important to note the following:
- node labels can be assigned to nodes, as shown with
subnode_nodelabel
in the example. Labels can be used for referring to the node elsewhere in theDTS
file. - Each
devicetree
node has a path that identifies its location in the tree, similarly to file system paths. In our example, the full path to thea-sub-node
node is “/a-node/a-sub-node”. - Each node in the tree can have properties, expressed as
name/value
pairs. The value can be any sequence of bytes or an array of so-calledcells
. In the example above, thea-sub-node
node has a property namedfoo
, whose value is a cell with value3
.
Nodes Reflecting Hardware
In a DTS
file, each node represents a hardware component. For example, let
us consider a board with I2C peripherals. The DTS
file for this board should
thus contain an I2C controller and I2C peripherals, as illustrated below:
soc {
i2c1@40003000 {
compatible = "nordic,nrf-twim";
reg = <0x40003000 0x1000>;
status = "okay";
clock-frequency = <100000>;
apds9960@39 {
compatible = "avago,apds9960";
reg = <0x39>;
};
}
}
The fields in this example can be explained as follows:
soc
represents the system on chip used on the board.i2c@40003000
represents the I2C controller (with unit address40003000
)compatible
represents the name of the hardware that the node represents in the format “vendor,device”, here “nordic,nrf-twim”.reg
represents the information used to address the device, as a sequence of “address,length” pairs. It is device specific.status
represents whether the device is “okay”, “disabled” or in any other status.clock-frequency
represents a custom property, here used for the I2C controller.apds9960@39
represents an I2C peripheral attached to this I2C controller.
In the context of this lecture, we do not address the concept of unit addresses in more details.
Binding Files
Each compatible
string must have a binding file describing its properties.
Bindings are found under “zephyr/dts/bindings”. They are written in YAML format and include:
- The required and optional properties.
- The Property types.
- Child node requirements.
The binding file related to the I2C controller in the example above is shown here (in a simplified version):
compatible: "nordic,nrf-twim"
properties:
reg:
required: true
clock-frequency:
type: int
default: 100000
How Zephyr Uses DTS
The following happens during the build process:
- The
devicetree
is compiled using dtc (DeviceTree Compiler) into a.dtb
(DeeviceTree blob). dtc is used only for validating that no error and no warning are present in the.dts
files. - Then, Zephyr converts
.dts
files todevicetree_generated.h
usinggen_defines.py
. The filedevicetree_generated.h
contains a bunch of macros used to access hardware. The file is available inbuild/zephyr/include/generated/zephyr/devicetree_generated.h
- Application and driver code use
devicetree_generated.h
macros to access configuration.
An example of generated macros is given below:
#define DT_NODELABEL_i2c1 0x... /* Reference to node */
#define DT_PROP(DT_NODELABEL_i2c1, clock_frequency) 100000
Using DeviceTree in a Zephyr RTOS Application
The basic principles for using a hardware component in a Zephyr RTOS application are the following:
-
Referencing Nodes in code is done as follows:
#include <zephyr/devicetree.h> #define I2C0_NODE DT_NODELABEL(i2c1) const struct device *i2c1_dev = DEVICE_DT_GET(I2C1_NODE);
-
Checking device available at execution time is implemented as follows:
if (!device_is_ready(i2c1_dev)) { return; }
-
Accessing device properties is implemented as follows:
uint32_t freq = DT_PROP(I2C1_NODE, clock_frequency);
Kconfig and DTS
in Practice
Now, to experiment with the Kconfig and DTS
concepts introduced in this
codelab, we will create a new application that uses the BME280 sensor
included in your development kit. The datasheet for this sensor is available
here.
In order to use an external sensor with the board, you must override both the
configuration and the DTS
so that the software can access the sensor
properly. Zephyr RTOS provides a driver for the BME280 sensor, located in
“zephyr/drivers/sensor/bosch/bme280/”. Its corresponding devicetree
bindings can be
found in “zephyr/dts/bindings/sensor/bosch,bme280-i2c.yaml”.
To create the application, you must:
- Create a new directory called sensor_bme280 in your workspace.
- Using the Getting started Codelab as a reference, set up your application inside this directory. Alternatively, you can duplicate the Blinky application and modify it as needed.
- In
sensor_bme280/src/main.cpp
, paste the following code:sensor_bme280/src/main.cpp// stl #include <chrono> // zpp-lib #include "zpp_include/thread.hpp" #include "zpp_include/this_thread.hpp" #include "zpp_include/digital_out.hpp" // zephyr #include <zephyr/logging/log.h> #include <zephyr/drivers/sensor.h> LOG_MODULE_REGISTER(sensor_bm280, CONFIG_APP_LOG_LEVEL); #define BME280_NODE DT_INST(0, bosch_bme280) void read_sensor() { const struct device* _sensorDevice = DEVICE_DT_GET(DT_INST(0, bosch_bme280)); using namespace std::literals; static std::chrono::milliseconds readInterval = 1000ms; if (!device_is_ready(_sensorDevice)){ LOG_ERR("Device %s not found", _sensorDevice->name); return; } struct sensor_value temperature_sv, humidity_sv, pressure_sv; while (true) { sensor_sample_fetch(_sensorDevice); sensor_channel_get(_sensorDevice, SENSOR_CHAN_AMBIENT_TEMP, &temperature_sv); sensor_channel_get(_sensorDevice, SENSOR_CHAN_HUMIDITY, &humidity_sv); sensor_channel_get(_sensorDevice, SENSOR_CHAN_PRESS, &pressure_sv); LOG_INF("T=%.2f [deg C] P=%.2f [kPa] H=%.1f [%%]", sensor_value_to_double(&temperature_sv), sensor_value_to_double(&pressure_sv), sensor_value_to_double(&humidity_sv)); zpp_lib::ThisThread::sleep_for(readInterval); } } int main(void) { LOG_DBG("Running on board %s", CONFIG_BOARD_TARGET); zpp_lib::Thread thread(zpp_lib::PreemptableThreadPriority::PriorityNormal, "Blinky"); auto res = thread.start(read_sensor); if (! res) { return -1; } res = thread.join(); if (! res) { LOG_ERR("Could not join thread: %d", (int) res.error()); return -1; } return 0; }
- Connect the BME280 sensor as illustrated below - be careful and plug the connector in the correct sense:
- Build this application with the
west build sensor_bme280 --pristine
command. You should get errors similar to these ones:
error: '__device_dts_ord_DT_N_INST_0_bosch_bme280_ORD' was not declared in this scope
96 | #define DEVICE_NAME_GET(dev_id) _CONCAT(__device_, dev_id)
This behavior is expected, because the build system has not yet been told how or
where to find the BME280 sensor. The macro
__device_dts_ord_DT_N_INST_0_bosch_bme280_ORD
is generated only when the
board’s DTS
file defines a BME280 node. This is not the case by default.
In more details, the code to access the sensor is the one below:
const struct device* _sensorDevice = DEVICE_DT_GET(DT_INST(0, bosch_bme280));
DTS
file yet, the macro
doesn’t exist, and the compilation fails.
Fix the devicetree
Definition
As mentionned earlier, Zephyr RTOS applications can add specific hardware that
is not defined in the board specific DTS
file. This is accomplished by
adding a board specific overlay
file.
The steps for adding the BME280 sensor to a Zephyr RTOS application are as follows:
- In the “sensor_bme280” folder, create a “boards” folder. Add a file named “nrf5340dk_nrf5340_cpuapp.overlay” in this folder.
- Knowing that the BME280 sensor is connected to the
p1.02
andp1.03
pins, check whether the I2C1 controller is using these pins. Open the “zephyr/boards/nordic/nrf5340dk/nrf5340_cpuapp_common-pinctrl.dtsi” and check the following definitions:
&pinctrl {
..
i2c1_default: i2c1_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 2)>,
<NRF_PSEL(TWIM_SCL, 1, 3)>;
};
};
i2c1_sleep: i2c1_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 2)>,
<NRF_PSEL(TWIM_SCL, 1, 3)>;
low-power-enable;
};
};
...
};
DTS
files
that the BME280 sensor is attached to the I2C1 controller. This is done in
the next step.
- In the “sensor_bme280/boards/nrf5340dk_nrf5340_cpuapp.overlay”, override the
i2c1
node as follows:
&i2c1 {
status = "okay";
bme280@77 {
compatible = "bosch,bme280";
reg = <0x77>;
};
};
Fix the Configuration
Finally, update the application configuration to enable support for I2C
, SENSOR
, and BME280
, and to allow floating-point formatting when printing measurements to the console.
This can be done by adding the following lines to the prj.conf file:
# Enable floating point formatting when printing
CONFIG_PICOLIBC=y
CONFIG_PICOLIBC_IO_FLOAT=y
# Sensor
CONFIG_I2C=y
CONFIG_SENSOR=y
CONFIG_BME280=y
At this point, if you build and flash your application using west build
sensor_bme280 --pristine
followed by west flash
, you should see the following
in the serial console:
*** Booting Zephyr OS build v4.2.0 ***
[00:00:00.266,693] <dbg> sensor_bm280: main: Running on board nrf5340dk/nrf5340/cpuapp
[00:00:00.338,745] <inf> sensor_bm280: T=24.70 [deg C] P=95.37 [kPa] H=61.8 [%]
[00:00:01.359,161] <inf> sensor_bm280: T=24.70 [deg C] P=95.37 [kPa] H=61.8 [%]
[00:00:02.383,331] <inf> sensor_bm280: T=24.70 [deg C] P=95.37 [kPa] H=62.2 [%]
[00:00:03.403,808] <inf> sensor_bm280: T=24.70 [deg C] P=95.37 [kPa] H=62.2 [%]
[00:00:04.427,978] <inf> sensor_bm280: T=24.70 [deg C] P=95.37 [kPa] H=62.3 [%]
Wrap-Up
By the end of this codelab, you should have completed the following steps:
- Understand what Kconfig is used for and how you can define application specific configuration parameters.
- Understand what
DTS
is used for and how you can add a specific hardware to your Zephyr RTOS application. - You can build the sensor_bme280 application and see the sensor values in the console.