Developing embedded applications often means balancing flexibility, performance, and resource usage. With the nRF Connect SDK (NCS) and Zephyr RTOS, this balance is achieved through a highly configurable build system powered by Kconfig.
If you’re new to Zephyr or NCS, you’ve probably encountered the mysterious prj.conf
file or run into the menuconfig
command. These tools can feel confusing at first, but once you understand how they work together, they become one of the most powerful parts of the SDK.
In this post, we’ll explore:
- What Kconfig is and how it fits into the build system
- How to configure your project with
prj.conf
- How to explore options with
menuconfig
- Common configurations (logging, peripherals, Bluetooth Low Energy)
- A practical example: enabling debug logging and tuning BLE stack parameters
- Best practices for managing build configurations
By the end, you’ll be comfortable navigating and customizing build options in your own projects.
Why Build Configurations Are Important
When working on microcontrollers, every byte of flash and RAM matters. You don’t want unused features bloating your firmware image or draining power.
Build configurations allow you to:
- Enable only the features you need (e.g., I2C but not SPI)
- Adjust resource usage (e.g., Bluetooth buffer sizes, number of simultaneous connections)
- Control debugging and logging levels to trade performance for visibility
- Switch between development and production builds without rewriting code
Instead of hardcoding constants in source files, you use Kconfig-driven build configurations that make your project modular and flexible.
Kconfig: The Backbone of Configuration
The Kconfig system originated in the Linux kernel and was adopted by Zephyr. It provides a declarative way to define build-time options.
Each subsystem — whether it’s logging, Bluetooth, or a peripheral driver — comes with a Kconfig file that lists available options. These options are exposed to the user through either:
- Text-based files (
prj.conf
, overlays) - Interactive menus (
menuconfig
)
During the build process, Kconfig generates:
.config
→ the resolved configuration with all options (including defaults)autoconf.h
→ a header file that lets your code use#ifdef CONFIG_OPTION
for conditional compilation
This means your firmware is literally built around the options you’ve enabled.
Configuring with prj.conf
The simplest way to configure your project is via the prj.conf
file, which lives in the root of your application directory.
This file uses a key=value format to define Kconfig symbols. Here’s an example:
# Enable logging
CONFIG_LOG=y
# Set logging level to debug
CONFIG_LOG_DEFAULT_LEVEL=4
# Enable Bluetooth stack with peripheral role
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="My_Nordic_Device"
The syntax is simple:
CONFIG_FEATURE=y
→ enable a featureCONFIG_FEATURE=n
→ disable a featureCONFIG_FEATURE=value
→ set an integer or string
Because prj.conf
is just a plain text file, it’s easy to track changes in version control.
Configuring with menuconfig
Sometimes you don’t know what options are available or what their possible values are. That’s where menuconfig
comes in.
Run:
west build -t menuconfig
This opens an ncurses-based menu interface where you can navigate through categories such as:
- Kernel
- Device drivers
- Logging
- Networking
- Bluetooth
Inside, you can toggle options, change integer values, or set strings.
When you save and exit, your changes are written back into a .config
file and can optionally be exported into a prj.conf
overlay. This way, you can experiment interactively and then lock down your configuration in source control.
If you prefer a graphical UI, there’s also guiconfig
:
west build -t guiconfig
Common Configurations You’ll Use
Let’s explore some frequently used build options.
Logging
Logging is one of the first features you’ll configure. It’s essential for debugging but comes with trade-offs in memory and performance.
# Enable logging
CONFIG_LOG=y
# Default logging level
CONFIG_LOG_DEFAULT_LEVEL=3 # Info
CONFIG_LOG_DEFAULT_LEVEL=4 # Debug
# Choose a backend
CONFIG_LOG_BACKEND_RTT=y # Use Segger RTT
CONFIG_LOG_BACKEND_UART=y # Use UART
Log levels:
0
→ None1
→ Error2
→ Warning3
→ Info4
→ Debug
Peripherals
Peripherals are optional and must be explicitly enabled. For example:
# Enable I2C driver
CONFIG_I2C=y
# Enable UART
CONFIG_SERIAL=y
CONFIG_UART_NRFX_UART0=y
This ensures unused drivers don’t waste memory.
Bluetooth Low Energy (BLE)
The BLE stack is one of the most configurable parts of NCS. You can control device roles, connection limits, security, and data throughput.
# Enable BLE and peripheral role
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
# Set device name
CONFIG_BT_DEVICE_NAME="My_Nordic_Device"
# Maximum simultaneous connections
CONFIG_BT_MAX_CONN=2
# Increase ATT MTU size for higher throughput
CONFIG_BT_BUF_ACL_RX_SIZE=251
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_DATA_LEN_UPDATE=y
These options are critical when tuning applications for speed, reliability, or power efficiency.
Practical Example: Debug Logging + BLE Tuning
Let’s combine what we’ve learned into a real-world scenario.
Imagine you’re developing a BLE peripheral device and need:
- Verbose debug logging during development
- Larger BLE buffers for faster data transfers
- Support for two simultaneous connections
Here’s what your prj.conf
might look like:
# Logging
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=4 # Debug
CONFIG_LOG_BACKEND_RTT=y # Use RTT for efficient logging
# Bluetooth
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Debug_Device"
# BLE performance tuning
CONFIG_BT_MAX_CONN=2 # Allow 2 connections
CONFIG_BT_BUF_ACL_RX_SIZE=251
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_DATA_LEN_UPDATE=y # Enable data length extensions
With this setup:
- RTT provides non-intrusive logging without blocking UART.
- Debug-level logs give you full insight during development.
- BLE throughput improves with larger buffer sizes and data length extensions.
- The device can handle two connections simultaneously (useful for testing with multiple phones).
If you later want a release build, you can create a separate config overlay prj_release.conf
that disables debug logging and shrinks BLE buffers to save memory.
Best Practices for Build Configurations
Managing configurations can get messy as projects grow. Here are some guidelines:
- Keep
prj.conf
minimal: Only include options relevant to your project. Defaults handle the rest. - Use overlays for variants: Maintain
prj_debug.conf
,prj_release.conf
, etc. and select them withwest build -c -b nrf52840dk_nrf52840 -- -DCONF_FILE=prj_debug.conf
. - Version control configs: Track
prj.conf
and overlays in Git to ensure reproducible builds. - Leverage
menuconfig
to explore options: Especially when you’re learning or tuning advanced features. - Understand the trade-offs: Every feature has a cost in flash, RAM, and power consumption.
Conclusion
The combination of Kconfig, prj.conf
, and menuconfig
gives you complete control over your application’s build in the nRF Connect SDK.
By learning how to configure logging, peripherals, and Bluetooth options, you can create firmware that’s optimized for both development and production.
Once you’re comfortable with these tools, you’ll find yourself quickly iterating on features, debugging efficiently, and building lean, production-ready applications.
Mastering build configurations isn’t just about toggling switches — it’s about making conscious design choices for your embedded system.