Submit to speak at Zephyr Developer Summit in Prague · Oct 7-9 · Submit Proposal
BlogMember Blog

Zephyr Insights: Pay as you Config

By June 4, 2026No Comments
Blog - Zephyr Insights: Pay as you Config, Loïc Domaigné, Doulos GmbH

In the previous article, we have looked into how Zephyr optimizes the code footprint:

  • only the components needed/configured are build,
  • kernel features that are not used won’t be present in the final image.

We normally simply rely on the mechanisms offered in Zephyr (Kconfig, Devicetree) to achieve this, without worrying too much about the details. In this article, we will be taking the “only pay for what you use/configure” mechanism under the microscope.

Notes:

This is an expanded version of my talk at the Zephyr’s meetup in Winterthur.

Kconfig Primers

Kconfig – aka “Kernel configuration” defines which Zephyr you want for your product. It is a compile-time mechanism that determines which software features, drivers and subsystems are enabled and compiled into a Zephyr application.

In practice, “Kconfig” is composed of:

  • text files named “Kconfig*”: they define the available configuration menus, options, data types, and dependency rules that exist within the Zephyr components.
  • configuration files: specifies the exact choices and values an application wants to set for those defined options
  • scripts to resolve dependencies: compute the final value for the options, and generate Kconfig macros that can be used by CMake and C programs for conditional compilation.

Each target board includes a default configuration that serves as a sensible baseline. Applications can then override or extend these settings as needed. A common application configuration file is prj.conf located in the project directory.

In the case of blinky, the GPIO driver is enabled:

CONFIG_GPIO=y

This option is defined in the Kconfig file drivers/gpio/Kconfig:

menuconfig GPIO
    bool "General-Purpose Input/Output (GPIO) drivers"
    help
        Include GPIO drivers in system config

This entry defines the option GPIO. It is a boolean option (y/n) used to specify if the GPIO drivers should be included in the build.

Notes: The name of the Kconfig option is GPIO. Outside the Kconfig files, the option <NAME> is prefixed with CONFIG_ (thus giving CONFIG_<NAME>), which acts as a “namespace” to distinguish Kconfig options from other macros. Therefore the option GPIO is referred to as CONFIG_GPIO in prj.conf.

Prior to building any object files, the build system will run some python scripts to compute the final value for each CONFIG option, checking if dependencies are met. It creates two files in the build folder: .config and autoconf.h:

$ find build -name ".config" -o -name "autoconf.h"
build/zephyr/include/generated/zephyr/autoconf.h
build/zephyr/.config

Let’s unravel how Zephyr uses these files for conditional compilation.

CONFIG Options and Condition Compilation Directives

The autoconf.h header is the C-macro equivalent of our .config file. For instance, if we look for the CONFIG_GPIO_XXX options, we see 3 options set:

  • in .config:
$ grep -rn "CONFIG_GPIO" build/zephyr/.config
47:# CONFIG_GPIO_HOGS is not set
97:CONFIG_GPIO=y
560:CONFIG_GPIO_INIT_PRIORITY=40
887:# CONFIG_GPIO_GET_DIRECTION is not set
888:# CONFIG_GPIO_GET_CONFIG is not set
889:# CONFIG_GPIO_ENABLE_DISABLE_INTERRUPT is not set
890:CONFIG_GPIO_RPI_PICO=y
  • in autoconf.h:
$ grep -rn "CONFIG_GPIO" build/zephyr/include/generated/zephyr/autoconf.h
64:#define CONFIG_GPIO 1
165:#define CONFIG_GPIO_INIT_PRIORITY 40
262:#define CONFIG_GPIO_RPI_PICO 1

The autoconf.h header is used in C/C++ code to have a conditional compiled block depending on the option’s value. For instance:

#if CONFIG_GPIO
printf("GPIO driver is enabled");
#else
printf("GPIO driver is not enabled");
#endif

We don’t need to explicitly include autoconf.h, as this header file is automatically taken care of by the Zephyr build system.

CONFIG Options and CMake

Zephyrs drivers are located in the zephyr/drivers directory and the GPIO drivers are in the
zephyr/drivers/gpio subdirectory. In Zephyr v4.4, 132 different GPIO drivers are available. The GPIO driver for the RPI Pico2 is implemented in the file gpio_rpi_pico.c

CMake uses a hierarchy of CMakeLists.txt files to determine which files should be included in a build. As expected, the Zephyr’s main CMakeLists.txt adds the drivers folder to the build. If you examine drivers/CMakeLists.txt, you will see that the directory gpio is added to the build, only if CONFIG_GPIO is defined:

add_subdirectory_ifdef(CONFIG_GPIO gpio)

And as CONFIG_GPIO=y is set in .config, the gpio folder is therefore included in the build. A similar ifdef is used for the other drivers categories, like I2C, SPI etc.

Thus: if we don’t use GPIO, the gpio folder is not even considered for the build!

Finally, looking at the CMakeLists.txt in drivers/gpio, we see that gpio_rpi_pico.c is added to the library source for the lib gpio only if CONFIG_GPIO_RPI_PICO is defined:

zephyr_library_sources_ifdef(CONFIG_GPIO_RPI_PICO gpio_rpi_pico.c)

This option is indeed defined in .config. But we didn’t set it explicitly. Where does it come from?

Dev Tips: Tracing Kconfig Options

Since all CONFIG_xxx options are coming from some Kconfig files, we could simply perform a systematic search for the string GPIO_RPI_PICO on all the Kconfig* files in our west workspace.

Zephyr v4.3 has introduced traceconfig to trace all configuration symbols, their values, and where those values originated. Since Zephyr v4.4, this information is also available in the HTML dashboard :

$ west build -t dashboard

If you’re developing a product subject to regulatory requirements, dashboard is a very handy tool for traceability.

The table below summarizes the lines for the CONFIG_GPIO_XXX options and their origins:

 

Type Name Value Source Location
int CONFIG_GPIO_INIT_PRIORITY 40 default zephyr/drivers/gpio/Kconfig:50
bool CONFIG_GPIO_RPI_PICO y default zephyr/drivers/gpio/Kconfig.rpi_pico:5
bool CONFIG_GPIO y assigned zephyr/boards/raspberrypi/rpi_pico2/rpi_pico2_rp2350a_m33_defconfig:8

First of all, we see that GPIO is enabled by the default configuration “defconfig” for the pico2 board. For this board, the line CONFIG_GPIO=y in prj.conf is therefore redundant (it may however be needed for other boards).

The CONFIG_GPIO_RPI_PICO option is coming from Kconfig.rpi_pico :

# drivers/gpio/Kconfig.rpi_pico
config GPIO_RPI_PICO
	default y
	depends on DT_HAS_RASPBERRYPI_PICO_GPIO_PORT_ENABLED
	select PICOSDK_USE_GPIO
	select PINCTRL
	bool "Raspberry Pi Pico GPIO driver"

The option GPIO_RPI_PICO is a boolean option for the “Raspberry Pi Pico GPIO driver”. It is enabled by default (default = y) as long as DT_HAS_RASPBERRYPI_PICO_GPIO_PORT_ENABLED is enabled too. But where does this option come from?

Looking again at the Kconfig table, we see its origin:

Type Name Value Source Location
bool CONFIG_DT_HAS_RASPBERRYPI_PICO_GPIO_ENABLED (h) y default build/Kconfig/Kconfig.dts:9658

Kconfig and Devicetree

The file Kconfig.dts is located in the build/Kconfig folder. If you search for the string DT_HAS_RASPBERRYPI_PICO_GPIO_PORT_ENABLED, you will see these strange looking statements:

DT_COMPAT_RASPBERRYPI_PICO_GPIO_PORT := raspberrypi,pico-gpio-port

config DT_HAS_RASPBERRYPI_PICO_GPIO_PORT_ENABLED
    def_bool $(dt_compat_enabled,$(DT_COMPAT_RASPBERRYPI_PICO_GPIO_PORT))

which reads as follows: 

if there is a devicetree node with compatible=”raspberrypi,pico-gpio-port enabled (`status=”okay”`), then enable the option DT_HAS_RASPBERRYPI_PICO_GPIO_PORT_ENABLED (**y**). Otherwise the option is not set (**n**).

If you’re new to devicetree: Devicetree is nothing but a text description of the hardware information required by the drivers/kernel using a special Domain Specific Language (DSL) [1]. Devicetree maps the information found in board reference manuals and schematics.

Devicetree is out of scope for this article. But like Kconfig, Devicetree in Zephyr is a compile time mechanism composed of a set of files and scripts that computes the “final devicetree” zephyr.dts and a corresponding header file representation devicetree_generated.h. We only show the relevant part of build/zephyr/zephyr.dts for blinky:

/dts-v1/;
/{
    aliases {
        /* some other aliases as well */
        led0 = &led0; /* &led0 means: points to node "led0:"
    };
    /*...*/
    leds {
        compatible = "gpio-leds";
        led0: led_0 {                      /* led0 node definition */
            gpios = < &gpio0 0x19 0x0 >;   /* &gpio0: points to the node "gpio0:" */
            label = "LED";
        };
    };
    /*...*/
    gpio0_map: gpio@40028000 {
        compatible = "raspberrypi,pico-gpio";
        /*...*/
        gpio0: gpio0_lo: gpio-port@0 {   /* gpio0 node definition */
                compatible = "raspberrypi,pico-gpio-port";
                reg=<0>;
                /*...*/
                status = "okay";
        };
    /*...*/
}

1.here: an hardware description language

  • The alias led0 [2] refers to the devicetree node “led0”
  • the led0 node tells that the LED is connected to GPIO0, pin 0x19 (25), with flag=0 meaning “Pin is high-voltage when active”. This matches the Schematics.
  • gpio0 node has compatible=”raspberrypi,pico-gpio-port and is enabled (status=”okay”). The GPIO port0 is available at the address 0x40028000. See RP2350 Datasheet 2.2.4 APB Registers.

And here we see the loop closing:

  • The GPIO driver will be selected and built only if it is available in hardware, and enabled.
  • In the case of the RPI Pico 2, the GPIO driver selected is gpio_rpi_pico.c.

Coincidences?

Some of you may ask if it is a coincidence that the option DT_HAS_XXX_ENABLED (where XXX corresponds to the value of the compatible string for the gpio node) is set?

compatible =  "raspberrypi,pico-gpio-port"
option: DT_HAS_RASPBERRYPI_PICO_GPIO_PORT_ENABLED"

Well, it is not! The Kconfig/kconfig.dts file in the build folder is created by the gen_driver_kconfig_dts.py script, which follows a strict template.

Conclusions

Unlike traditional embedded / Real-Time OS that provide just a scheduler and require you to integrate third-party libraries for drivers, file systems or networking, Zephyr offers a generic, modular, integrated, components-based ecosystem.

This modularity is achieved at compile time, by an inter-play of 3 mechanisms:

  • Konfig, which describes “The Zephyr we want for our product”,
  • Devicetree, which describes “The hardware where the Zephyr application firmware runs”
  • The build system (CMake, python scripts), which first computes the “end result” (zephyr.dts, devicetree_generated.h, .config and autoconf.h). These files are used to selectively compile only “what is needed”.

We have just explained most of the build configuration phase available in the Zephyr’s documentation 😎

There’s a last missing piece about how the hardware abstraction still provides the performance required. This will be discussed in another article.

TL;DR:

Zephyr tries hard to stick to the: “You only pay for what you use” saying.

Often, I hear that “Zephyr does a lot of magic behind the scenes”. At Doulos, we aim to teach you that magic – you can then troubleshoot or leverage it to gain a competitive advantage. If you’re interested, check our Zephyr Essentials course.

 

 

[1] here: an hardware description language

[2] in Zephyr, the alias “led0” means the “primary onboard LED”