Kconfig
Kconfig is the configuration system Zephyr inherited from the Linux kernel. It controls which drivers, subsystems, and features are compiled into your firmware.
Every Zephyr feature — I2C, BLE, USB, logging — is gated behind a CONFIG_ symbol. If CONFIG_I2C=n, the I2C driver is not compiled and takes zero flash space.
Your project's prj.conf
# Enable the I2C bus and sensor subsystem (for BME280)
CONFIG_I2C=y
CONFIG_SENSOR=y
# Logging
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3 # 0=off 1=err 2=warn 3=info 4=debug
How are Kconfig values resolved?
Zephyr merges multiple Kconfig files in order:
- SoC defaults — from the chip definition (
esp32s3.conf) - Board defaults — from the board definition (
esp32s3_devkitc.conf) - Your
prj.conf— your application overrides - Driver Kconfig — for every symbol you enable (e.g.
CONFIG_SENSOR=y), every matching sensor driver in the tree becomes available. The overlay decides which one actually compiles in.
Values in prj.conf override everything upstream. If you set CONFIG_LOG_DEFAULT_LEVEL=4, it overrides the board default.
After a build, cat build/zephyr/.config shows every resolved Kconfig value — the definitive record of what is compiled into your firmware.
Dependency-driven Kconfig — the BME280 example
Some symbols have depends on — if a driver requires another subsystem, you must enable the full chain or the build will fail.
To understand what a driver requires, read its Kconfig file directly. For BME280 at zephyr/drivers/sensor/bosch/bme280/Kconfig:
menuconfig BME280
bool "BME280/BMP280 sensor"
default y
depends on DT_HAS_BOSCH_BME280_ENABLED
select I2C if $(dt_compat_on_bus,$(DT_COMPAT_BOSCH_BME280),i2c)
select SPI if $(dt_compat_on_bus,$(DT_COMPAT_BOSCH_BME280),spi)
help
Enable driver for BME280 I2C-based or SPI-based temperature
and pressure sensor.
Three things to notice:
default y—BME280is auto-enabled the moment its dependency is met. You don't writeCONFIG_BME280=yyourself.depends on DT_HAS_BOSCH_BME280_ENABLED— this symbol is auto-generated by Zephyr when it finds a DTS node withcompatible = "bosch,bme280"in your overlay. The overlay triggers the Kconfig.select I2C if … on-bus i2c— the driver pulls in the I2C subsystem only if your BME280 is actually on an I2C bus. Wire it via SPI and it pulls in SPI instead. This isselectdoing conditional work.
This is why CONFIG_I2C=y + CONFIG_SENSOR=y is enough to bring up the BME280 — everything driver-specific resolves automatically from your overlay.
Anatomy of a Kconfig file
A Kconfig file is a plain-text description of available config symbols, written in a small declarative language. The real BME280 Kconfig file is the clearest example — every keyword Kconfig supports appears at least once:
menuconfig BME280
bool "BME280/BMP280 sensor"
default y
depends on DT_HAS_BOSCH_BME280_ENABLED
select I2C if $(dt_compat_on_bus,$(DT_COMPAT_BOSCH_BME280),i2c)
help
Enable driver for BME280 I2C- or SPI-based environmental sensor.
if BME280
choice BME280_MODE
prompt "BME280 sampling mode"
default BME280_MODE_NORMAL
config BME280_MODE_NORMAL
bool "normal"
config BME280_MODE_FORCED
bool "forced"
endchoice
config BME280_TEMP_OVER
int "Temperature oversampling factor"
default 2
range 1 16
endif # BME280
Five things to notice:
menuconfigvsconfig—menuconfig BME280doubles as a collapsible group header inmenuconfig. Children sit under it visually.if BME280 … endif— applies an implicitdepends on BME280to every symbol inside. Sub-options disappear entirely ifBME280isn.choice … endchoice— defines a mutually-exclusive group. Exactly one ofBME280_MODE_NORMAL/BME280_MODE_FORCEDmay bey.- Indentation is convention, not syntax — but every Kconfig file follows the same 4-space style.
- A
Kconfigfile rarely lives alone — each subsystem has its ownKconfig, andsourcelines higher up in the tree pull them together.
File-level directives
These structure the file itself:
| Directive | Purpose |
|---|---|
config NAME | Declare a single symbol (CONFIG_NAME) |
menuconfig NAME | Declare a symbol that also acts as a collapsible menu header in menuconfig |
choice … endchoice | Mutually-exclusive group — exactly one option may be y |
menu "Title" … endmenu | Group symbols under a labeled section in the UI |
if EXPR … endif | Apply a shared depends on to all enclosed symbols |
source "path/Kconfig" | Include another Kconfig file (absolute from ZEPHYR_BASE) |
rsource "Kconfig" | Same as source, but relative to the current file |
comment "Text" | Static text shown in menuconfig (e.g. section dividers) |
mainmenu "Title" | Root menu title (only valid in the top-level Kconfig) |
Config block attributes
These appear inside a config or menuconfig block:
| Attribute | Example | What it does |
|---|---|---|
| type | bool / int / hex / string / tristate | One per block. Determines what values are valid. |
prompt "Text" | bool "Enable LED strip" (combined form) | User-facing label shown in menuconfig. Without a prompt, the symbol is hidden and only settable via defaults or select. |
default VALUE | default 0x200 if FOO | Initial value. Multiple default lines pick the first matching condition. |
depends on EXPR | depends on I2C && !ZEPHYR_QEMU | Symbol disappears unless EXPR is true. The symbol cannot be y if its dependencies aren't met. |
select OTHER | select I2S | When this symbol is y, force OTHER to y — ignores OTHER's own depends on. Use sparingly. |
imply OTHER | imply USB_KEYBOARD | Suggest OTHER=y, but respect its dependencies and allow the user to override. The polite version of select. |
range LOW HIGH | range 0 99 | Constrain int/hex to a valid range. |
help | (multi-line block) | Documentation shown in menuconfig and the Kconfig HTML reference. |
visible if EXPR | visible if EXPERT | Hide the prompt unless EXPR is true, but still let defaults apply. |
select vs imply vs depends on — the one thing that trips people up
These three keywords all relate one symbol to another, and getting them wrong is the most common source of "why is this driver not building" headaches.
| Direction | Respects target's depends on? | Override-able? | |
|---|---|---|---|
depends on | "I need X to be on" | n/a (it's the constraint) | n/a |
select | "Force X to be on" | No — can cause invalid configs | No |
imply | "Suggest X be on" | Yes | Yes — user can set X=n |
Use depends on for hard requirements (HW driver requires its bus subsystem to be selectable). Use imply when you'd like a feature on by default but it's not critical. Reach for select only when the target symbol has no user-facing prompt and exists solely to be turned on by other symbols.
selecting a symbol that has its own depends on can produce a config where the target is y but its dependency is n — Kconfig will warn but still build, and you'll get cryptic link errors. If you find yourself doing this, the right answer is almost always depends on instead.
The Kconfig file family in a Zephyr build
You won't write a Kconfig file very often — but you'll encounter half a dozen related files. Knowing which is which saves a lot of time:
| File | Lives in | Purpose |
|---|---|---|
Kconfig | Every subsystem and driver directory | Defines symbols (declarative — the schema) |
Kconfig.defconfig | Boards and SoCs | Sets defaults for invisible symbols — invisible to menuconfig but applied at build time |
<board>_defconfig | boards/<vendor>/<board>/ | Initial values for visible board symbols (the board's "factory settings") |
prj.conf | Your app root | Your config overrides — what you actually edit day-to-day |
boards/<board>.conf | Your app root, optional | Board-specific overrides that merge with prj.conf only when building for that board |
sysbuild.conf | Your app root, Sysbuild only | Cross-image settings (e.g. SB_CONFIG_BOOTLOADER_MCUBOOT=y) |
.config | build/zephyr/ (generated) | The fully-resolved result of merging everything above |
Precedence (later wins): SoC Kconfig.defconfig → board Kconfig.defconfig → <board>_defconfig → prj.conf → boards/<board>.conf → CLI -DCONFIG_* flags.
Common Kconfig types
| Symbol type | Example | Meaning |
|---|---|---|
bool | CONFIG_BME280=y | Enable/disable a feature |
int | CONFIG_BME280_TEMP_OVER=2 | Numeric value |
string | CONFIG_BOOT_BANNER_STRING="my fw" | String value |
hex | CONFIG_SRAM_SIZE=0x40000 | Hex value |
Reference: Kconfig — Tips and Best Practices and the Kconfig setting guide in the Zephyr docs.
Browsing available symbols
# Launch the Kconfig GUI (requires Python and tkinter)
west build -t menuconfig
# Or the simpler ncurses version
west build -t guiconfig
Both show the full tree of available symbols with their current values and help text.
Next: actually read the BME280
The four layers are now in place — overlay, binding, Kconfig, and the in-tree driver they pull in. Time to use it. The next page, Sensors, calls sensor_sample_fetch against the BME280 you've just configured.