May 26 / Sebastian Helmut

Demystifying Zephyr RTOS: How Kconfig and Devicetree Actually Work Together

Zephyr RTOS splits configuration between Kconfig and Devicetree, and how to master the build flow without fighting it



1. Introduction

When you run west build in a Zephyr project, something more complex than a simple compilation happens. Before a single line of your application code is compiled, Zephyr runs two configuration systems-Kconfig and devicetree-that together determine exactly what hardware your firmware targets and exactly which software features get compiled in. Only after both systems have finished processing does the actual C compilation begin.

This is different from anything you encounter in a bare-metal project or a FreeRTOS project. In bare-metal you edit a header file. In FreeRTOS you edit FreeRTOSConfig.h. In Zephyr you edit two separate files-prj.conf and a .overlay file-and the build system generates the right C headers and board definitions automatically.

Understanding why Zephyr needs two configuration systems, and how each one works, is what separates a developer who fights the build system from one who uses it fluently. This article explains both systems from the ground up, using an nRF52840 DK as the concrete target throughout.



2. Kconfig: Configuring the Software

2.1 What Kconfig is

Kconfig comes from the Linux kernel-it is the same configuration system that has controlled the Linux kernel build since 2002. Zephyr adopted it wholesale because it solves a hard problem: how do you let developers enable and disable hundreds of features, subsystems, and drivers at compile time, with proper dependency tracking between them, without turning a single header file into an unmaintainable mess?

The answer is a hierarchy of Kconfig files, one per subsystem, that declare configuration symbols, their types, their default values, and their dependencies. You never edit these files directly. Instead you set values in your prj.conf and Kconfig resolves the full dependency tree, generating a .config file that the compiler sees as a set of #define macros.

2.2 The prj.conf file

prj.conf is your project's Kconfig fragment. It is the only file you normally touch:

# prj.conf-nRF52840 DK application configuration

# Enable GPIO driver

CONFIG_GPIO=y

# Enable Bluetooth LE peripheral role

CONFIG_BT=y

CONFIG_BT_PERIPHERAL=y

CONFIG_BT_DEVICE_NAME="MyDevice"

# Enable logging at level 3 (info)

CONFIG_LOG=y

CONFIG_LOG_DEFAULT_LEVEL=3

# Set main thread stack size

CONFIG_MAIN_STACK_SIZE=2048

Each line sets one Kconfig symbol. =y enables a boolean feature. =n disables it. String and integer values are set without quotes for integers and with quotes for strings.

When you run west build, Kconfig reads prj.conf, merges it with the board's default configuration and any SoC-level defaults, resolves all dependencies, and writes the final .config into your build/ directory. From that point on, the compiler sees symbols like CONFIG_BT=1 as ordinary #define macros.

2.3 Browsing all options with menuconfig

The full list of Kconfig options for an nRF52840 project runs to several thousand symbols. You cannot memorize them. The right tool is menuconfig:

west build -b nrf52840dk/nrf52840--t menuconfig

This opens a terminal-based interactive menu-identical to make menuconfig in the Linux kernel-where you can browse every available option, see its current value, read its help text, and understand its dependencies. Any change you make in menuconfig is written back to your prj.conf.

2.4 Real example: enabling UART logging on nRF52840

The most common first task in a new project is enabling logging over UART so you can see printk output. Here is the minimal prj.conf:

CONFIG_LOG=y

CONFIG_LOG_BACKEND_UART=y

CONFIG_UART_CONSOLE=y

CONFIG_SERIAL=y

CONFIG_LOG enables the logging subsystem. CONFIG_LOG_BACKEND_UART routes log output to UART. CONFIG_UART_CONSOLE connects the console to UART. CONFIG_SERIAL enables the UART driver itself. Kconfig enforces that if you enable CONFIG_LOG_BACKEND_UART, CONFIG_SERIAL must also be enabled-if you forget it, the build will tell you.

                  Fig 1. The Kconfig flow



Struggling to implement this for a professional project? If you need to master full-scale firmware architecture, security, and build automation, join our 1-D or 4-Day Live Implemnetation Workshops. I'll show you the exact direct path to production-ready firmware without the trial and error.


3. Devicetree: Describing the Hardware

3.1 What devicetree is

Devicetree is a data format for describing hardware. Like Kconfig, Zephyr inherits it from the Linux kernel. A devicetree source (.dts) file describes the physical hardware on a board: which peripherals exist, at which addresses, on which buses, with which interrupt lines and GPIO pins.

The critical point is that devicetree describes what exists, not what is enabled. That is Kconfig's job. Devicetree answers the question "where is the UART?"-Kconfig answers "should the UART driver be compiled in?". Both answers are needed to produce working firmware.

Every supported board in Zephyr ships a .dts file. For the nRF52840 DK it lives at:

zephyr/boards/nordic/nrf52840dk/nrf52840dk_nrf52840.dts

You never edit this file. Instead you write an overlay that extends or overrides specific nodes.

3.2 Devicetree nodes, properties and labels

A devicetree is a tree of nodes. Each node represents a hardware component. Each node has properties that describe its configuration:

/* From the nRF52840 DK board DTS-LED0 */

leds {

compatible = "gpio-leds";

led0: led_0 {

gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;

label = "Green LED 0";

};

};


The led0 before the colon is a label-a unique name you use to reference this node from C code or from your overlay. The gpios property says: connect to gpio0 controller, pin 13, active low.

In your C code you reference the node by its label, never by a raw register address or GPIO number:


#include <zephyr/drivers/gpio.h>



/* Get the GPIO spec from the devicetree at compile time */

static const struct gpio_dt_spec led =

GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);



int main(void) {

gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);

gpio_pin_toggle_dt(&led);

return 0;

}


If you port this code to an STM32 board, only the board .dts changes. The C code is identical.

3.3 Writing a devicetree overlay

An overlay is a small .dts fragment that extends or overrides the board's default devicetree. You place it in your application directory and name it after your board:

App/

├── CMakeLists.txt

├── prj.conf

├── boards/

│ └── nrf52840dk_nrf52840.overlay <-- your overlay

└── src/

└── main.c

A real overlay example-adding a custom I2C sensor on the nRF52840 DK:

dts

/* nrf52840dk_nrf52840.overlay */



&i2c0 {

status = "okay";

clock-frequency = <I2C_BITRATE_FAST>;



my_sensor: bme280@76 {

compatible = "bosch,bme280";

reg = <0x76>;

label = "BME280";

};

};

This overlay:

1. Enables i2c0 (sets status = "okay")

2. Sets the clock to 400 kHz

3. Adds a BME280 sensor node at address 0x76

Zephyr merges your overlay with the board DTS at build time. The merged result is what the compiler sees.

You can also pass an overlay explicitly on the command line:

west build -b nrf52840dk/nrf52840--DDTC_OVERLAY_FILE=my_sensor.overlay



       Fig 2. Devicetree overlay flow



4. How Kconfig and Devicetree Work Together

4.1 The full build flow

When you run west build -b nrf52840dk/nrf52840, the following happens in order:

west build

├── CMake configuration phase

│ ├── reads CMakeLists.txt

│ ├── Kconfig processing

│ │ ├── board defconfig

│ │ ├── SoC defconfig

│ │ ├── prj.conf

│ │ └── → build/zephyr/.config

│ │

│ └── Devicetree processing

│ ├── board .dts

│ ├── SoC .dtsi

│ ├── your .overlay

│ └── → build/zephyr/include/generated/devicetree.h

└── Ninja compilation

├── Zephyr kernel sources

├── driver sources (selected by Kconfig)

├── your application sources

└── → build/zephyr/zephyr.elf

build/zephyr/zephyr.hex

build/zephyr/zephyr.bin

Kconfig and devicetree run in parallel during the CMake configuration phase. Neither depends on the other-they produce independent outputs. The compiler then sees both: .config symbols as #define macros, and the generated devicetree.h as a set of compile-time node accessors.

4.2 How a driver uses both systems

A GPIO driver is the clearest example of Kconfig and devicetree working together. The driver needs two things:

- Kconfig: CONFIG_GPIO=y-tells the build system to compile the GPIO driver source files

- Devicetree: a gpio-controller node with status = "okay"-tells the driver which hardware instance to manage and at which address

If you enable CONFIG_GPIO=y but the board DTS has status = "disabled" on the GPIO controller, the driver is compiled but finds no hardware to bind to. If the DTS has the node enabled but CONFIG_GPIO is not set, the driver source files are never compiled. Both must be correct.

This is the mental model: Kconfig is the software switch, devicetree is the hardware map. A driver only works when both are set correctly.

4.3 Common mistakes and how to diagnose them

Mistake 1: enabling a Kconfig option without the right devicetree node

error: Bluetooth requires a uart device with DT label "bt_hci_uart"

Fix: add the correct UART node to your overlay with the right label.

Mistake 2: adding a devicetree node without enabling its Kconfig driver

The node exists in the tree but the driver never binds to it. Your code calls device_is_ready() and gets false. Fix: add CONFIG_<DRIVER>=y to prj.conf.

Mistake 3: overlay not found

warning: No overlay file found for board nrf52840dk_nrf52840

Fix: name your overlay exactly boards/nrf52840dk_nrf52840.overlay-the board name must match exactly including underscores.

Diagnosis tool: west build with -v prints every file processed. For the devicetree specifically:


west build -b nrf52840dk/nrf52840 -v 2>&1 | grep "overlay"



Fig 3. The complete west build flow.


5. Conclusion

The Zephyr build system has two configuration systems because it solves two different problems. Kconfig controls what software gets compiled. Devicetree describes what hardware exists. Neither can do the other's job-a configuration system that tries to handle both ends up doing neither well.

The three things to remember:

- prj.conf is your Kconfig fragment. Set CONFIG_X=y to enable a feature, CONFIG_X=n to disable it. Use menuconfig to discover what options exist and what their dependencies are.

- Your overlay extends the board DTS. Never edit the board .dts directly. Write a .overlay in your boards/ directory and Zephyr merges it at build time.

- A driver needs both. Kconfig compiles the driver. Devicetree tells the driver where the hardware is. Get one wrong and the driver silently does nothing.


Want to master this? Here are your next steps:



Created with