Practical Zephyr - Kconfig (Part 2)

In this second article of the “Practical Zephyr” series, we’ll explore the kernel configuration system Kconfig by looking at the printk logging option in Zephyr. We won’t explore the logging service as such in detail but instead use it as an excuse to dive deep into Kconfig. Finally, we’ll create our own little application-specific Kconfig configuration.

👉 Find the other parts of the Practical Zephyr series here.

Prerequisites

This article is part of an article series. In case you haven’t read the previous article, please go ahead and have a look since it explains the required installation steps to follow along. If you’re already familiar with Zephyr and have a working installation handy, it should be easy to follow using your own setup.

Note: A full example application including all the configuration files that we’ll see throughout this article is available in the 01_kconfig folder of the accompanying GitHub repository.

Warm up with printk

Go ahead and create a new, empty freestanding application, e.g., using the application from the previous article.

$ tree --charset=utf-8 --dirsfirst
.
├── src
│   └── main.c
├── CMakeLists.txt
└── prj.conf

The simplest way to log text in Zephyr is the printk function. printk can take multiple arguments and format strings, but that’s not what we want to show here, so we’ll just modify the application’s main function to output the string “Message in a bottle.” each time it is called - and thus after each reset:

#include <zephyr/kernel.h>
#define SLEEP_TIME_MS 100U

void main(void)
{
    printk("Message in a bottle.\n");
    while (1)
    {
        k_msleep(SLEEP_TIME_MS);
    }
}

But where does our output end up?

As mentioned in the previous article, we’re using a development kit from Nordic; in my case, the development kit for the nRF52840. For most development kits, the logging output is available via its UART interface and the following settings:

  • Baud rate: 115200 bits/s
  • Data bits: 8
  • No parity, one stop bit

The connection settings for the UART interface are configured using devicetree - which we’ll explore in a later article. Since these default settings are all we need for now, let’s focus on Kconfig. We build and flash the project as follows:

$ west build --board nrf52840dk_nrf52840 --build-dir ../build
$ west flash --build-dir ../build

When connecting the development kit to my computer, it shows up as TTY device /dev/tty.usbmodem<some-serial-number>. The format may vary depending on your development kit and operating system, e.g., on Windows, it’ll show up as a COM port.

Open your serial monitor of choice, e.g., using the screen command, by launching PuTTY or - in case you’re using macOS - maybe you’re also a fan of Serial, and reboot or reflash your device. You should see the following output each time your device starts:

$ screen /dev/tty.usbmodem<some-serial-number> 115200
*** Booting Zephyr OS build v3.3.99-ncs1 ***
Message in a bottle.

$ # use CTRL+A+k to quit the screen command.

Great! But what if we’d like to get rid of this feature entirely without changing our code? We all know that string processing is expensive and slow on an embedded system. Let’s assume that in a production build, we want to get rid of any logging output to reduce our image size and speed up our application. How do we achieve this? This is where the kernel configuration system “Kconfig” comes into play.

Kconfig

We’ve seen that Zephyr uses CMake as one of its build and configuration systems. On top of CMake - just like the Linux kernel - Zephyr uses the kernel configuration system “Kconfig”. We’ve already encountered Kconfig in the previous section, where we had to provide an empty application configuration file prj.conf.

In this article, we’ll explore the practical aspects of Kconfig. As always, much more detailed information about Kconfig in Zephyr can be found in the official documentation. So, what is Kconfig?

In simple words, Kconfig is a configuration system that uses a hierarchy of configuration files (with their own syntax), which in turn define a hierarchy of configuration options, also called symbols. The build system uses these symbols to include or exclude files from the build process and as configuration options in the source code itself. All in all, this allows you to modify your application without modifying the source code.

Configuring symbols

For the time being, let’s ignore how the entire Kconfig hierarchy is built or merged, and let’s just have a look at how the configuration system works in general.

The debugging subsystem of Zephyr defines the Kconfig symbol PRINTK that allows you to enable or disable the printk output. This PRINTK symbol is defined in the Kconfig file zephyr/subsys/debug/Kconfig. We won’t go into detail about the syntax here. It is quite self-explanatory for this example.

For details about Kconfig’s syntax, you can have a look at the Kconfig language specification in the Linux kernel documentation or Zephyr’s Kconfig tips and best practices.

zephyr/subsys/debug/Kconfig

config PRINTK
	bool "Send printk() to console"
	default y

As you can see from the above Kconfig file, unless configured otherwise, PRINTK is enabled by default. We can configure symbols to our needs in the application configuration file prj.conf that we’ve encountered in the previous article. Symbols are assigned their values using the following syntax:

CONFIG_<symbol name>=<value>

Note: At the time of writing, no spaces are allowed before or after the = operator.

Our PRINTK is a symbol of type bool and can be assigned the values y and n (have a look at the official documentation for details). To disable PRINTK, we can therefore add the following to our application configuration file prj.conf:

CONFIG_PRINTK=n

What’s the effect of changing a symbol? As mentioned before, this can affect the build options and can also be used by some subsystem’s implementation. Whatever a symbol does, however, is specific for each symbol, meaning there is no convention for symbol names that always have the same effect. Thus, in the end, it is up to you to dig into the sources and documentation to see which options are available and what effect they have (there’s a Kconfig search that we’ll see later).

Let’s have a short look at our PRINTK symbol. A quick search for CONFIG_PRINTK in our local Zephyr code reveals how the symbol is used in the implementation of the printk debugging subsystem. Notice that the below snippets are simplified versions of the real code:

zephyr/lib/os/printk.h

#ifdef CONFIG_PRINTK
extern void printk(const char *fmt, ...);
#else
static inline void printk(const char *fmt, ...)
{
    ARG_UNUSED(fmt);
}

zephyr/lib/os/printk.c

#ifdef CONFIG_PRINTK
void printk(const char *fmt, ...)
{
    // ...
}
#endif /* defined(CONFIG_PRINTK) */

Notice: In our prj.conf file, we set CONFIG_PRINTK=n, but in the implementation, this translates to a missing definition, not a definition with a value of n. This is simply what a bool symbol translates to in the implementation. For details, I’ll once again have to direct you to the official documentation.

In the header file zephyr/lib/os/printk.h we can see that printk is replaced by a dummy function in case PRINTK is disabled. Thus, all the calls to printk will effectively be removed by the compiler. In the zephyr/lib/os/printk.c source file, we can see that the #if directive is used to conditionally compile the relevant source code only in case PRINTK is enabled. Later in this section, we’ll also see an example of how files can be excluded entirely by the build system if an option is not enabled.

With printk disabled in our application configuration file, let’s try to rerun our application to verify that the output is indeed disabled:

$ west build --board nrf52840dk_nrf52840 --build-dir ../build --pristine
$ west flash --build-dir ../build

Notice: In most cases when changing your Kconfig symbols, you can simply run west build and the build system will rebuild the appropriate source. But in some cases, it is required to use the --pristine option to enforce a complete rebuild of your application. Typically, this is needed when adding new build or configuration files. If you encounter a build error or are missing a configuration change, try rebuilding with --pristine. This in turn also requires specifying the correct --board in case you didn’t configure it using west config.

$ screen /dev/tty.usbmodem<some-serial-number> 115200
*** Booting Zephyr OS build v3.3.99-ncs1 ***
Message in a bottle.

Huh. Even though we’ve disabled PRINTK using CONFIG_PRINTK=n in our prj.conf file, the output is still not disabled. The build passed, and there were no warnings - did we miss something?

As mentioned before, Kconfig uses an entire hierarchy of Kconfig files. The initial configuration is merged from several Kconfig files, including the Kconfig file of the specified board. The exact procedure used by Zephyr for merging the configuration file is explained in great detail in a dedicated section of the official documentation but is not worth repeating here.

Notice: The command line output of west build also shows the order in which Kconfig merges the configuration files. We’ll see this in the section about build types and alternative Kconfig files.

When debugging Kconfig settings, it can be helpful to have a look at the generated output. All Kconfig symbols are merged into a single zephyr/.config file located in the build directory, in our case, build/zephyr/.config. There, we find the following setting:

build/zephyr/.config

#
# Debugging Options
#
# CONFIG_DEBUG is not set
# CONFIG_STACK_USAGE is not set
# CONFIG_STACK_SENTINEL is not set
CONFIG_PRINTK=y

It seems that our CONFIG_PRINTK setting was not accepted. Why?

The reason is that Kconfig files can have dependencies: Enabling one option can automatically also enable other options. Sadly, at the time of writing, Kconfig does not seem to warn about conflicting values for symbols, e.g., from the application configuration file: It’d be great if Kconfig would tell me that some other option overwrites my CONFIG_PRINTK=n setting and has no effect!

Zephyr contains hundreds of so-called Kconfig fragments. It is therefore almost impossible to navigate these files without tool support. Thankfully, West integrates two graphical tools to explore available Kconfig options and their current setting: menuconfig and guiconfig.

Loading menuconfig and guiconfig

For the build command, West has some builtin targets, two of which are used for Kconfig:

$ west build --build-dir ../build -t usage
-- west build: running target usage
...
Kconfig targets:
  menuconfig - Update .config using a console-based interface
  guiconfig  - Update .config using a graphical interface
...

The targets menuconfig and guiconfig can be used to start graphical Kconfig tools which allow modifying the .config file in your build directory. Both tools are essentially identical. The only difference is that menuconfig is a text-based graphical interface that opens directly in your terminal, whereas guiconfig is a “clickable” user interface running in its own window. The screenshots below show the tools launched by the following commands:

$ west build --build-dir ../build -t menuconfig
$ west build --build-dir ../build -t guiconfig

Notice: If you did not build your project or the build system cannot build the required targets due to CMake/Kconfig processing issues, the tool will not load since the zephyr/.config file is missing.

For the sake of simplicity, we’ll use menuconfig in the remainder of this article. In both tools, you can search for configuration symbols. In menuconfig - similar to vim - you can use the forward slash / to start searching, e.g., for our PRINTK symbol. Several results match, but only one of them is the plain symbol PRINTK which also shows the same description that we’ve seen in the Kconfig file:

PRINTK(=y) "Send printk() to console"

Navigate to the symbol using the search or the path Subsystems and OS Services > Debugging Options. You should see the following screen:

Typically, you can toggle bool symbols using Space or Enter, but this does not seem to be possible for PRINTK. You can see that a symbol cannot be changed - which is indicated by the marker * being enclosed by dashes -*- instead of square brackets [*]. Using ?, we can display the symbol’s information:

Name: PRINTK
Prompt: Send printk() to console
Type: bool
Value: y

Help:

  This option directs printk() debugging output to the supported
  console device, rather than suppressing the generation
  of printk() output entirely. Output is sent immediately, without
  any mutual exclusion or buffering.

Default:
  - y

Symbols currently y-selecting this symbol:
  - BOOT_BANNER
...

We found the reason why we can’t disable the symbol at the very bottom of the PRINTK’s symbol information! The symbol BOOT_BANNER is y-selecting the PRINTK symbol, so we can’t disable it, and the application’s configuration has no effect. This simple example should also make you realize that it is hard, if not impossible, to figure out dependencies by navigating Kconfig fragments file by file.

Notice: If you can’t find a Kconfig symbol in menuconfig, you can toggle the Show all mode using Shift+A. This shows hidden Kconfig symbols - your symbol might be one of those!

Let’s fix our configuration:

  • Navigate to the BOOT_BANNER symbol and disable it.
  • Going back to PRINTK it should now be possible to disable it.

Persisting menuconfig and guiconfig changes

menuconfig and guiconfig can be used to test and explore configuration options. Typically, you’d use the Save operation to store the new configuration to the zephyr/.config file in your build directory. You can then also launch menuconfig multiple times and change the configuration to your needs, and the changes will always be reflected in the next build.

The downside of saving the configuration to zephyr/.config in the build directory is that all changes will be lost once you perform a clean build, e.g., using west build --pristine. To persist your changes, you need to place them into your configuration files.

Using diff

The people at Golioth published a short article that shows how to leverage the diff command to find out which configuration options have changed when using the normal Save operation: Before writing the changes to the zephyr/.config file, Kconfig stores the old configuration in build/zephyr/.config.old. Thus, all changes that you make between a Save operation are reflected by the differences between zephyr/.config and build/zephyr/.config.old:

$ west build --board nrf52840dk_nrf52840 -d ../build --pristine
$ west build -d ../build -t menuconfig
# Within menuconfig:
# - Disable BOOT_BANNER and PRINTK.
# - Save the changes to ../build/zephyr/.config using [S].

$ diff ../build/zephyr/.config ../build/zephyr/.config.old
1097c1097
< # CONFIG_BOOT_BANNER is not set
---
> CONFIG_BOOT_BANNER=y
1462c1462
< # CONFIG_PRINTK is not set
---
> CONFIG_PRINTK=y

Notice: The # CONFIG_<symbol> is not set comment syntax is used by zephyr/.config instead of CONFIG_<symbol>=n for historical reasons, as also mentioned in the official documentation. This allows parsing configuration files directly as makefiles.

You can now simply take the changes from the diff and persist them, e.g., by writing them into your application configuration file. Keep in mind that this only works for changes performed between one Save operation. If you Save multiple times, .config.old is always replaced by the current .config. The diff operation must therefore be performed after each Save.

This approach, however, is not always feasible. E.g., try to enable the logging subsystem Subsystems and OS Services > Logging: You’ll notice that the diff is huge even though you’ve only changed one option. This is due to the large number of dependencies of the CONFIG_LOG=y setting.

Save minimal configuration

Both menuconfig and guiconfig have the Save minimal config option. As the name implies, this option exports a minimal Kconfig file containing all symbols that differ from their default values. This does, however, also include symbols that differ from their default values due to other Kconfig fragments than your application’s configuration. E.g., the symbols in the Kconfig files of the chosen board are saved in this minimal configuration file.

Let’s try this out with our settings for the BOOT_BANNER and PRINTK symbols.

$ west build --board nrf52840dk_nrf52840 -d ../build --pristine
$ west build -d ../build -t menuconfig
# Within menuconfig:
# - Disable BOOT_BANNER and PRINTK.
# - Use [D] "Save minimal config" and write the changes to tmp.conf.
$ cat tmp.conf
CONFIG_GPIO=y
CONFIG_PINCTRL=y
CONFIG_SERIAL=y
CONFIG_NRFX_GPIOTE_NUM_OF_EVT_HANDLERS=1
CONFIG_USE_SEGGER_RTT=y
CONFIG_SOC_SERIES_NRF52X=y
CONFIG_SOC_NRF52840_QIAA=y
CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y
# CONFIG_BOOT_BANNER is not set
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
# CONFIG_PRINTK is not set
CONFIG_EARLY_CONSOLE=y

As you can see, this minimal configuration contains a lot more options than we changed within menuconfig. Some of these options come from the selected board, in my case zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840_defconfig, but are still listed since they do not use default values. However, the two changes to PRINTK and BOOT_BANNER are still visible. We can just take those options and write them to our application configuration file:

$ cat prj.conf
CONFIG_PRINTK=n
CONFIG_BOOT_BANNER=n

Now, after rebuilding, the output on our serial interface indeed remains empty. Also, if you look at the output of your build should notice that the flash usage decreased by around 2 KB.

In case you don’t like menuconfig or guiconfig, or just want to browse available configuration options, Zephyr’s documentation also includes a dedicated Kconfig search. E.g., when searching for our PRINTK configuration we’ll be presented with the following output:

While this search does point out that there is a dependency on the BOOT_BANNER symbol, it cannot know our current configuration and therefore can’t tell us that disabling PRINTK won’t have any effect since BOOT_BANNER is also still enabled by default.

Nordic’s Kconfig extension for VS Code

Another graphical user interface for Kconfig is the nRF Kconfig extension for VS Code. This is an extension tailored for use with the nRF Connect SDK and with Nordic’s complete VS Code extension pack, but to some extent, this extension can also be used for a generic Zephyr project and might therefore also be useful if you’re not developing for a target from Nordic:

At the time of writing and for this repository, the following configuration options were necessary to use the extension in a workspace that does not rely on the entire extension set:

{
  "nrf-connect.toolchain.path": "${nrf-connect.toolchain:2.4.0}",
  "nrf-connect.topdir": "${nrf-connect.sdk:2.4.0}",
  "kconfig.zephyr.base": "${nrf-connect.sdk:2.4.0}",
  "nrf-connect.applications": ["${workspaceFolder}/path/to/app"]
}

Note: Let’s hope that the good people at Nordic find the time to change this extension so that it is easily usable for any Zephyr project!

However, as visible in the above screenshot, at the time of writing the extension fails to inform the user about the dependency to BOOT_BANNER, and in contrast to menuconfig and guiconfig it is therefore not visible why the configuration option cannot be disabled. Aside from that, this extension has two major benefits:

  • The Save minimal config option seems to recognize options coming from a _defconfig file and therefore really only exports the configuration options set within the extension.
  • The extension adds auto-completion to your .conf files.

With this last tool to explore Kconfig, let’s have a look at a couple of more options.

Using different configuration files

Until now, we’ve only used the application configuration file prj.conf for setting Kconfig symbols. In addition to this configuration file, Zephyr’s build system automatically picks up additional Kconfig fragments, if provided, and also allows explicitly specifying additional or alternative fragments.

We’ll quickly glance through the most common practices here.

Build types and alternative Kconfig files

Zephyr doesn’t use pre-defined build types such as Debug or Release builds. It is up to the application to decide the optimization options and build types. Different build types in Zephyr are supported using alternative Kconfig files, specified using the CONF_FILE build system variable.

Let’s freshen up on the build system variables: These variables are not direct parameters for the build and configuration systems used by Zephyr, e.g., the --build-dir option for west build, but are available globally. As described in the official documentation on important build system variables, there are multiple ways to pass such variables to the build system. We’ll repeat two of the available options here:

  1. A build system variable may exist as an environment variable.
  2. A build system variable may be passed to west build as a one-time CMake argument after a double dash --.

We’ll stick to the second option and use a one-time CMake argument, as shown in the application development basics in the official documentation. First, let’s go ahead and create a prj_release.conf, which defines our symbols for the release build type:

$ tree --charset=utf-8 --dirsfirst
.
├── src
│   └── main.c
├── CMakeLists.txt
├── prj.conf
└── prj_release.conf

$ cat prj_release.conf
CONFIG_LOG=n
CONFIG_PRINTK=n
CONFIG_BOOT_BANNER=n
CONFIG_EARLY_CONSOLE=n
CONFIG_OVERRIDE_FRAME_POINTER_DEFAULT=y
CONFIG_USE_SEGGER_RTT=n
CONFIG_BUILD_OUTPUT_STRIPPED=y
CONFIG_FAULT_DUMP=0
CONFIG_STACK_SENTINEL=y

# Optimize build for size
CONFIG_SIZE_OPTIMIZATIONS=y

For our release build, we set the symbol CONFIG_SIZE_OPTIMIZATIONS to y and thereby optimize our release build for size: Build optimizations are not set up in your CMake configuration, but instead using Kconfig. Don’t worry about all the other symbols in this file for now. We’ll catch up on them in the section about Kconfig hardening.

We can now instruct West to use this prj_release.conf instead of our prj.conf for the build by using the following command:

$ rm -rf ../build
$ west build --board nrf52840dk_nrf52840 -d ../build -- -DCONF_FILE=prj_release.conf

If you scroll through the output of the west build command, you’ll notice that Kconfig merged prj_release.conf into our final configuration:

Parsing /opt/nordic/ncs/v2.4.0/zephyr/Kconfig
Loaded configuration '/opt/nordic/ncs/v2.4.0/zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840_defconfig'
Merged configuration '/path/to/01_kconfig/prj_release.conf'
Configuration saved to '/path/to/zephyr_practical/build/zephyr/.config'

Thus, in short, Zephyr accepts build types by specifying an alternative Kconfig file that typically uses the file name format prj_<build>.conf, using the CONF_FILE build system variable. The name of the build type is entirely application-specific and does not imply any, e.g., compiler optimizations.

Notice: Zephyr uses Kconfig to specify build types and also optimizations. Thus, CMake options such as CMAKE_BUILD_TYPE are typically not used directly in Zephyr projects. This includes compiler optimization flags such as -Os or -O2, which are set using the CONFIG_COMPILER_OPTIMIZATIONS in Kconfig instead.

Board-specific Kconfig fragments

Aside from the application configuration file prj.conf, Zephyr also automatically picks up board-specific Kconfig fragments. Board fragments are placed in the boards directory in the project root (next to the CMakeLists.txt file) and use the <board>.conf name format.

E.g., throughout this guide, we’re using the nRF52840 development kit, which has the board name nrf52840dk_nrf52840. Thus, the file boards/nrf52840dk_nrf52840.conf is automatically merged into the Kconfig configuration during the build, if present.

Let’s try this by disabling UART as console output using a new fragment boards/nrf52840dk_nrf52840.conf. For the nRF52840 development kit, this symbol is enabled by default. You can go ahead and verify this by checking the default configuration zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840_defconfig - or take my word for it:

tree --charset=utf-8 --dirsfirst
.
├── boards
│   └── nrf52840dk_nrf52840.conf
├── src
│   └── main.c
├── CMakeLists.txt
├── prj.conf
└── prj_release.conf

$ cat boards/nrf52840dk_nrf52840.conf
CONFIG_UART_CONSOLE=n

Then, we perform a pristine build of the project. Notice that a --pristine build is required at this point because otherwise, the build system does not pick up our newly added .conf file. This is one of the very few occasions where a pristine build is actually required.

$ west build --board nrf52840dk_nrf52840 -d ../build --pristine

In the output of west build shown below you can see that the new file boards/nrf52840dk_nrf52840.conf is indeed merged into the Kconfig configuration. You can also see a warning that the Zephyr library drivers__console is excluded from the build since it now has no SOURCES:

Parsing /opt/nordic/ncs/v2.4.0/zephyr/Kconfig
Loaded configuration '/opt/nordic/ncs/v2.4.0/zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840_defconfig'
Merged configuration '/path/to/01_kconfig/prj.conf'
Merged configuration '/path/to/01_kconfig/boards/nrf52840dk_nrf52840.conf'
Configuration saved to '/path/to/build/zephyr/.config'
...
CMake Warning at /opt/nordic/ncs/v2.4.0/zephyr/CMakeLists.txt:838 (message):
  No SOURCES given to Zephyr library: drivers__console
  Excluding target from build.

You can of course verify that the symbol is indeed disabled by checking the zephyr/.config file in the build directory.

Notice: Previous versions of Zephyr used a prj_<board>.conf fragment in the project root directory instead of board fragments in the boards directory. At the time of writing, the build system still merges any such file into the configuration. However, this prj_<board>.conf fragment is deprecated, and boards/<board>.conf should be used instead.

How are board fragments used by our previously created release build type? Let’s try it out and have a look at the build output:

$ rm -rf ../build
$ west build --board nrf52840dk_nrf52840 -d ../build -- -DCONF_FILE=prj_release.conf
Parsing /opt/nordic/ncs/v2.4.0/zephyr/Kconfig
Loaded configuration '/opt/nordic/ncs/v2.4.0/zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840_defconfig'
Merged configuration '/path/to/01_kconfig/prj_release.conf'
Configuration saved to '/path/to/build/zephyr/.config'

Well, this is awkward: Even though we provided a board-specific Kconfig fragment, it is ignored as soon as we switch to our alternative prj_release.conf configuration. This is, however, working as intended: When using alternative Kconfig files in the format prj_<build>.conf, Zephyr looks for a board fragment matching the file name boards/<board>_<build>.conf.

In my case, I need to create the file boards/nrf52840dk_nrf52840_release.conf. Let’s get rid of the warning for the Zephyr library drivers__console by disabling the CONSOLE entirely in our board’s release configuration:

tree --charset=utf-8 --dirsfirst
.
├── boards
│   ├── nrf52840dk_nrf52840.conf
│   └── nrf52840dk_nrf52840_release.conf
├── src
│   └── main.c
├── CMakeLists.txt
├── prj.conf
└── prj_release.conf

$ cat boards/nrf52840dk_nrf52840_release.conf
CONFIG_CONSOLE=n
CONFIG_UART_CONSOLE=n

Now, when we rebuild the project, we see that our board fragment is merged into the Kconfig configuration, and no warning is issued by west build:

$ rm -rf ../build
$ west build --board nrf52840dk_nrf52840 -d ../build -- -DCONF_FILE=prj_release.conf
Parsing /opt/nordic/ncs/v2.4.0/zephyr/Kconfig
Loaded configuration '/opt/nordic/ncs/v2.4.0/zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840_defconfig'
Merged configuration '/path/to/01_kconfig/prj_release.conf'
Merged configuration '/path/to/01_kconfig/boards/nrf52840dk_nrf52840_release.conf'
Configuration saved to '/path/to/build/zephyr/.config'

Extra configuration files

There is one more option to modify the Kconfig, listed among the important build system variables in Zephyr’s official documentation: EXTRA_CONF_FILE. This build system variable accepts one or more additional Kconfig fragments. This option can be useful, e.g., to specify additional configuration options used by multiple build types (normal builds, “release” builds, “debug” builds) in a separate fragment. An example could be adding TLS configs with a tls.conf or adding external NOR configs with qspi_nor.conf

Note: Since Zephyr 3.4.0 the EXTRA_CONF_FILE build system variable replaces the deprecated variable OVERLAY_CONFIG.

Let’s add two Kconfig fragments extra0.conf and extra1.conf:

$ tree --charset=utf-8 --dirsfirst
.
├── boards
│   ├── nrf52840dk_nrf52840.conf
│   └── nrf52840dk_nrf52840_release.conf
├── src
│   └── main.c
├── CMakeLists.txt
├── extra0.conf
├── extra1.conf
├── prj.conf
└── prj_release.conf

$ cat extra0.conf
CONFIG_DEBUG=n

$ cat extra1.conf
CONFIG_GPIO=n

Notice: In an actual project, you’ll need to pick better names for the extra configuration files, e.g., no-debug.conf and no-gpio.conf. I’m using extra0.conf and extra1.conf to explicitly show their use and to make the order in which the files are applied visible, as you’ll see just now.

We can now pass the two extra configuration fragments to the build system using the EXTRA_CONF_FILE variable. The paths are relative to the project root and can either be separated using semicolons or spaces:

$ rm -rf ../build
$ west build --board nrf52840dk_nrf52840 -d ../build -- \
  -DEXTRA_CONF_FILE="extra0.conf;extra1.conf"
Parsing /opt/nordic/ncs/v2.4.0/zephyr/Kconfig
Loaded configuration '/opt/nordic/ncs/v2.4.0/zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840_defconfig'
Merged configuration '/path/to/01_kconfig/prj.conf'
Merged configuration '/path/to/01_kconfig/boards/nrf52840dk_nrf52840.conf'
Merged configuration '/path/to/01_kconfig/extra0.conf'
Merged configuration '/path/to/01_kconfig/extra1.conf'
Configuration saved to '/path/to/build/zephyr/.config'

The fragments specified using the EXTRA_CONF_FILE variable are merged into the final configuration in the given order. E.g., we can create a release build and reverse the order of the fragments:

$ rm -rf ../build
$ west build --board nrf52840dk_nrf52840 -d ../build -- \
  -DCONF_FILE="prj_release.conf" \
  -DEXTRA_CONF_FILE="extra1.conf;extra0.conf"
Parsing /opt/nordic/ncs/v2.4.0/zephyr/Kconfig
Loaded configuration '/opt/nordic/ncs/v2.4.0/zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840_defconfig'
Merged configuration '/path/to/01_kconfig/prj_release.conf'
Merged configuration '/path/to/01_kconfig/boards/nrf52840dk_nrf52840_release.conf'
Merged configuration '/path/to/01_kconfig/extra1.conf'
Merged configuration '/path/to/01_kconfig/extra0.conf'
Configuration saved to '/path/to/build/zephyr/.config'

Kconfig hardening

Another tool worth mentioning when talking about Kconfig is the hardening tool. Just like menuconfig and guiconfig, this tool is a target for west build. It is executed using the -t hardenconfig target in the call to west build. The hardening tool checks the Kconfig symbols against a set of known configuration options that should be used for a secure Zephyr application. It then lists all differences found in the application.

We can use this tool to check our normal and release configurations:

$ # test normal build
$ west build --board nrf52840dk_nrf52840 \
  -d ../build \
  --pristine \
  -t hardenconfig

$ # test release build
$ west build --board nrf52840dk_nrf52840 \
  -d ../build \
  --pristine \
  -t hardenconfig \
  -- -DCONF_FILE=prj_release.conf

For the normal build using prj.conf, the hardening tool displays a table of all symbols whose current values do not match the recommended, secure configuration value. E.g., at the time of writing, this is the output for the normal build in my demo application:

-- west build: running target hardenconfig
[0/1] cd /path/to/zephyr/kconfig/hardenconfig.py /opt/nordic/ncs/v2.4.0/zephyr/Kconfig
                 name                 | current | recommended || check result
==============================================================================
CONFIG_OVERRIDE_FRAME_POINTER_DEFAULT |    n    |      y      ||     FAIL
CONFIG_USE_SEGGER_RTT                 |    y    |      n      ||     FAIL
CONFIG_BUILD_OUTPUT_STRIPPED          |    n    |      y      ||     FAIL
CONFIG_FAULT_DUMP                     |    2    |      0      ||     FAIL
CONFIG_STACK_SENTINEL                 |    n    |      y      ||     FAIL

The symbols in prj_release.conf have been chosen such that at the time of writing all hardening options are fulfilled and the above table is empty.

A custom Kconfig symbol

In the previous sections, we examined Kconfig in detail. To wrap up this article, we’ll create and use a custom, application-specific symbol in our application and build process.

Note: This section borrows from the nRF Connect SDK Fundamentals lesson on configuration files that is freely available in the Nordic Developer Academy. If you’re looking for additional challenges, check out the available courses!

Adding a custom configuration

CMake automatically detects a Kconfig file if it is placed in the same directory of the application’s CMakeLists.txt, and that is what we’ll use for our own configuration file. If you want to place the Kconfig file somewhere else, you can customize this behavior using an absolute path for the KCONFIG_ROOT build system variable.

Similar to the file zephyr/subsys/debug/Kconfig of Zephyr’s debug subsystem, the Kconfig file specifies all available configuration options and their dependencies. We’ll only specify a simple boolean symbol in the Kconfig file in our application’s root directory. Please refer to the official documentation for details about the Kconfig syntax:

$ tree --charset=utf-8 --dirsfirst -L 1
.
├── boards
├── src
├── CMakeLists.txt
├── Kconfig
├── extra0.conf
├── extra1.conf
├── prj.conf
└── prj_release.conf

$ cat Kconfig
mainmenu "Customized Menu Name"
source "Kconfig.zephyr"

menu "Application Options"
config USR_FUN
	bool "Enable usr_fun"
	default n
	help
	  Enables the usr_fun function.
endmenu

The skeleton of this Kconfig file is well explained in step 4 in the “Application CMakeLists.txt” section in the official documentation:

The statement “mainmenu” defines a custom text that is used as our Kconfig main menu. It is shown, e.g., as a title in the build target menuconfig. We’ll see this in a moment when we’ll build our target and launch menuconfig.

The “source” statement essentially includes the top-level Zephyr Kconfig file zephyr/Kconfig.zephyr and all of its symbols (all “source” statements are relative to Zephyr’s root directory). This is necessary since we’re effectively replacing the zephyr/Kconfig file of the Zephyr base that is usually parsed as the first file by Kconfig. We’ll see this below when we look at the build output. The contents of the default root Kconfig file are quite similar to what we’re doing right now:

$ cat $ZEPHYR_BASE/Kconfig
# -- hidden comments --
mainmenu "Zephyr Kernel Configuration"
source "Kconfig.zephyr"

By sourcing the Kconfig.zephr file, we’re loading all Kconfig menus and symbols provided with Zephr. Next, we declare our own menu between the “menu” and “endmenu” statements to group our application symbols. Within this menu, we declare our USR_FUN symbol, which we’ll use to enable a function usr_fun.

Let’s rebuild our application without configuring USR_FUN and have a look at the build output. A --pristine build is required to pick up the new Kconfig file:

$ west build --board nrf52840dk_nrf52840 -d ../build --pristine
Parsing /path/to/01_kconfig/Kconfig
Loaded configuration '/opt/nordic/ncs/v2.4.0/zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840_defconfig'
Merged configuration '/path/to/01_kconfig/prj.conf'
Merged configuration '/path/to/01_kconfig/boards/nrf52840dk_nrf52840.conf'
Configuration saved to '/path/to/build/zephyr/.config'
Kconfig

Have a good look at the very first line of the Kconfig-related output:

  • In our previous builds, this line indicated the use of Zephyr’s default Kconfig file as follows: Parsing /opt/nordic/ncs/v2.4.0/zephyr/Kconfig,
  • Whereas now it uses our newly created Kconfig file: Parsing /path/to/01_kconfig/Kconfig.

The remainder of the output should look familiar. When we look at the generated zephyr/.config file in our build directory, we see that the USR_FUN symbol is indeed set according to its default value n:

$ cat ../build/zephyr/.config
# Application Options
#
# CONFIG_USR_FUN is not set
# end of Application Options

The new, custom main menu name “Customized Menu Name” is only visible in the interactive Kconfig tools, such as menuconfig, and is not used within the generated zephyr/.config file. The interactive tools now also show our newly created menu and symbol.

As you can see in the below screenshot, the main menu has the name “Customized Menu Name” and a new menu “Application Options” is now available as the last entry of options:

$ west build -d ../build -t menuconfig

Notice: It is also possible to source Kconfig.zephyr after defining the application symbols and menus. This would have the effect that your options will be listed before the Zephyr symbols and menus. In this section, we’ve sourced Kconfig.zephr before our own options.

Configuring the application build using Kconfig

Now that we have our own Kconfig symbol, we can use it in the application’s build process. Create two new files usr_fun.c and usr_fun.h in the src directory:

$ tree --charset=utf-8 --dirsfirst
.
├── boards
│   ├── nrf52840dk_nrf52840.conf
│   └── nrf52840dk_nrf52840_release.conf
├── src
│   ├── main.c
│   ├── usr_fun.c
│   └── usr_fun.h
├── CMakeLists.txt
├── Kconfig
├── extra0.conf
├── extra1.conf
├── prj.conf
└── prj_release.conf

We’re defining a simple function usr_fun that prints an additional message on boot using the newly created files:

// Content of usr_fun.h
#pragma once

void usr_fun(void);
// Content of usr_fun.c
#include "usr_fun.h"
#include <zephyr/kernel.h>

void usr_fun(void)
{
    printk("Message in a user function.\n");
}

Now, all that’s left is to tell CMake about our new files. Instead of always including these files when building our application, we can use our Kconfig symbol USR_FUN to only include the files in the build if the symbol is enabled. Add the adding the following to CMakeLists.txt:

target_sources_ifdef(
    CONFIG_USR_FUN
    app
    PRIVATE
    src/usr_fun.c
)

target_sources_ifdef is another CMake extension from zephyr/cmake/modules/extensions.cmake: It conditionally includes our files in the build in case CONFIG_USR_FUN is defined. Since the symbol USR_FUN is disabled by default, our build currently does not include usr_fun.

Using the application’s Kconfig symbol

Let’s first extend our main.c file to use usr_fun regardless of any configuration options:

#include <zephyr/kernel.h>
#include "usr_fun.h"

#define SLEEP_TIME_MS 100U

void main(void)
{
    printk("Message in a bottle.\n");
    usr_fun();

    while (1)
    {
        k_msleep(SLEEP_TIME_MS);
    }
}

Unsurprisingly, building this application fails since usr_fun.c is not included in the build:

$ west build --board nrf52840dk_nrf52840 -d ../build --pristine
[158/168] Linking C executable zephyr/zephyr_pre0.elf
FAILED: zephyr/zephyr_pre0.elf zephyr/zephyr_pre0.map
--snip--
/path/to/ld.bfd: app/libapp.a(main.c.obj): in function `main':
/path/to/main.c:9: undefined reference to `usr_fun'
collect2: error: ld returned 1 exit status
ninja: build stopped: subcommand failed.
FATAL ERROR: command exited with status 1: /path/to/cmake --build /path/to/build

We have two possibilities to fix our build: The first possibility is to enable USR_FUN in our kernel configuration, e.g., in our prj.conf file:

# --snip--
CONFIG_USR_FUN=y

This, however, defeats the purpose of having a configurable build: The build would still fail in case we’re not enabling USR_FUN. Instead, we can also use the USR_FUN symbol within our code to only conditionally include parts of our application:

#include <zephyr/kernel.h>

#ifdef CONFIG_USR_FUN
#include "usr_fun.h"
#endif

#define SLEEP_TIME_MS 100U

void main(void)
{
    printk("Message in a bottle.\n");

#ifdef CONFIG_USR_FUN
    usr_fun();
#endif

    while (1)
    {
        k_msleep(SLEEP_TIME_MS);
    }
}

Now, our build succeeds for either configuration of the USR_FUN symbol. Clearly, we could also use the same approach as printk and provide an empty function or definition for usr_fun instead of adding conditional statements to our application.

Note: The application builds and links, but you won’t see any output on the serial console in case you’ve followed along; The Kconfig symbols responsible for the serial output are still disabled within prj.conf and the board-specific fragment. Feel free to update your configuration, e.g., by enabling the debug output for normal builds while disabling it for the “release” build!

To understand why we can use the CONFIG_USR_FUN definition, we can have a look at the compiler commands used to compile main.c. Conveniently (as mentioned in the previous article), by default, Zephyr enables the CMake variable CMAKE_EXPORT_COMPILE_COMMANDS. The compiler command for main.c is thus captured by the compile_commands.json in our build directory:

{
  "directory": "/path/to/build",
  "command": "/path/to/bin/arm-zephyr-eabi-gcc --SNIP-- -o CMakeFiles/app.dir/src/main.c.obj -c /path/to/main.c",
  "file": "/path/to/main.c"
},

Within the large list of parameters passed to the compiler, there is also the -imacros option specifying the autoconf.h Kconfig header file:

-imacros /path/to/build/zephyr/include/generated/autoconf.h

This header file contains the configured value of the USR_FUN symbol as a macro:

// --snip---
#define CONFIG_USR_FUN 1

Looking at the official documentation of the -imacros option for gcc, you’ll find that this option acquires all the macros of the specified header without also processing its declarations. Thus, all macros within the autoconf.h files are also available at compile time.

Conclusion

In this article, we’ve explored the kernel configuration system Kconfig in great detail. Even if you’ve never used Kconfig before, you should now be able to at least use Kconfig with Zephyr, and - in case you’re stuck - you should be able to easily navigate the official documentation. We’ve seen:

  • how to find and change existing Kconfig symbols,
  • how to analyze dependencies between different symbols,
  • files that are automatically picked up by the build system,
  • files generated by Kconfig,
  • how to define your own build types and board-specific fragments,
  • how to define application-specific Kconfig symbols, and
  • how the build system and the application use Kconfig symbols.

Note: A full example application including all the configuration files that we’ve created throughout this article is available in the 01_kconfig folder of the accompanying GitHub repository.

If you’ve followed along, you may also have noticed that Kconfig files are not something you’d like to explore file by file. While writing this article, I wanted to include an entire include graph showing this, but in the end, it is just too many files. I’d recommend sticking to the available tools!

In your future Zephyr projects, Kconfig will become especially important once you’re starting to build your own device drivers. With this article, I hope I can give you an easy start.

Further reading

The following are great resources when it comes to Zephyr and are worth a read or watch:

Just like in the previous article, I can always warmly recommend browsing through the videos from the Zephyr Development Summit, e.g., the playlists from the 2022 and 2023 Developers Summits.

Finally, have a look at the files in the accompanying GitHub repository and I hope you’ll follow along with the future articles of this series!

Martin Lampacher is a Freelance Embedded Engineer based in the Italian Alps.