10 Zephyr Config Options Every Developer Gets Wrong on Their First Project

Common Zephyr RTOS configuration mistakes in prj.conf that trip up new developers

Your code compiles against the Zephyr sample. You copy it into your own project, tweak a few lines in prj.conf, hit west build, and… nothing works. The error messages point to symbols you’ve never heard of. Or worse, it builds clean and crashes at runtime with no output at all.

You’re not bad at this. Zephyr’s configuration system is genuinely unlike anything you’ve used in bare-metal or FreeRTOS. Three systems are stitched together: Kconfig, devicetree, and CMake. Each has its own syntax, its own files, and its own silent failure modes. The learning curve is steep because the system has layers, and the documentation rarely tells you which layer you’re fighting.

Here are 10 mistakes I’ve seen (and made) on first Zephyr projects, pulled from real forum posts, GitHub issues, and late-night debugging sessions. These apply to Zephyr 3.x and later.

How Zephyr Configuration Actually Works (30-Second Version)

Zephyr configuration lives in three layers:

  • Kconfig: Software feature toggles. Lives in your board’s defconfig (base defaults) and your prj.conf (your overrides). Syntax: CONFIG_SOMETHING=y.
  • Devicetree: Hardware description. Lives in .dts files (board defaults) and .overlay files (your overrides). Describes peripherals, pins, memory regions.
  • CMake: Build system glue. CMakeLists.txt ties your source files together and tells west where to find things.
Board defconfig ──→ prj.conf ──→ Devicetree overlays
  (Kconfig base)    (your overrides)   (HW customization)
        \                |                  /
         \               v                 /
          +────→  [ west build ]  ←───────+
                        |
                        v
               Final firmware image

If you don’t know which layer a setting lives in, you’ll put it in the wrong file. That’s the root cause of most first-project pain.

Mistake #1: Not Running a Pristine Build After Config Changes

The mistake: You change a line in prj.conf, run west build, and the old cached configuration persists. Your change does nothing. You stare at it for 20 minutes.

The fix:

west build --pristine

Best practice: Use west build --pristine=auto, which detects config changes and forces a clean rebuild when needed. Make this your default until you understand the build cache well enough to trust it.

Mistake #2: Wrong Syntax in prj.conf (And Zephyr Won’t Tell You)

The mistake: Writing SERIAL=y instead of CONFIG_SERIAL=y. Or using // for comments instead of #. Or putting spaces around the = sign. Zephyr silently ignores malformed lines. No error, no warning. Your setting just doesn’t exist.

# ❌ WRONG
SERIAL=y
BT = y
// enable logging
CONFIG_LOG =y

# ✅ CORRECT
CONFIG_SERIAL=y
CONFIG_BT=y
# enable logging
CONFIG_LOG=y

The fix: Every line needs the CONFIG_ prefix. No spaces around =. Comments use #.

Best practice: Run west build -t menuconfig to browse what’s actually set. If your symbol isn’t there, your prj.conf line got ignored.

Mistake #3: Enabling a Subsystem Without Its Dependencies

The mistake: You set CONFIG_BT=y and expect Bluetooth to work. But your board needs CONFIG_BT_CTLR=y or specific HCI transport options. Or you enable CONFIG_LOG=y without a logging backend. The build might succeed, but the subsystem does nothing at runtime.

The fix: Check the Kconfig dependency tree. In menuconfig, greyed-out options show you exactly what’s missing.

west build -t menuconfig

Best practice: When enabling a major subsystem (Bluetooth, networking, USB), find the closest official Zephyr sample that uses it. Diff its prj.conf against yours. The sample’s config is battle-tested; yours isn’t.

Mistake #4: Undersized Stack and Heap Defaults

The mistake: You leave CONFIG_MAIN_STACK_SIZE at the default (often 1024 bytes), add a Bluetooth stack or networking code, and get silent stack overflows. Or malloc() returns NULL because CONFIG_HEAP_MEM_POOL_SIZE is 0.

# Reasonable starting points for a BLE project
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_HEAP_MEM_POOL_SIZE=8192

The fix: Bump these values early. You can optimize later once things work.

Best practice: During development, enable stack overflow detection:

CONFIG_THREAD_ANALYZER=y
CONFIG_STACK_SENTINEL=y
# or, if your chip has an MPU:
CONFIG_MPU_STACK_GUARD=y

These catch overflows with a clear fault instead of silent corruption.

Mistake #5: Putting Hardware Settings in prj.conf (or Software Settings in Devicetree)

The mistake: You try to configure a pin mux, UART baud rate, or GPIO assignment in prj.conf. It doesn’t work because those are hardware descriptions, and they belong in devicetree. Or you try to enable a software feature in a .overlay file, where it gets ignored.

The fix: Hardware description goes in devicetree (.overlay). Software feature toggles go in Kconfig (prj.conf).

If you’re describing what exists on the board (a UART peripheral at address 0x40002000, a sensor on I2C bus 0), that’s devicetree. If you’re deciding what software should run (enable Bluetooth, turn on logging, set a stack size), that’s Kconfig.

Best practice: When you’re unsure, search the Zephyr docs for the symbol name. Kconfig symbols start with CONFIG_. Devicetree properties don’t.

Mistake #6: Modifying SDK Board Files Instead of Creating an Overlay

The mistake: You need to change a pin assignment, so you edit the .dts file inside the Zephyr SDK tree. It works until you update the SDK, and all your changes vanish. Or a teammate clones the repo and gets different behavior.

The fix: Create a board-specific overlay in your project directory:

my-zephyr-app/
├── CMakeLists.txt
├── prj.conf
├── src/
│   └── main.c
└── boards/
    └── nrf52840dk_nrf52840.overlay

The naming convention matters: the file must match your board name exactly. Zephyr picks it up automatically during the build.

Best practice: Never touch the Zephyr SDK tree. Keep all customizations in your application directory. If you’re working with Hubble-compatible BLE devices, the Zephyr reference application for Hubble shows a clean project structure worth copying.

Mistake #7: Enabling Logging in Code but Not in Config

The mistake: You add LOG_MODULE_REGISTER(my_app, LOG_LEVEL_DBG) in your source code. You sprinkle LOG_INF() calls everywhere. You see zero output. The code compiles fine because the logging macros expand to nothing when the backend isn’t configured.

The fix: Minimum logging config:

CONFIG_LOG=y
CONFIG_LOG_BACKEND_UART=y
CONFIG_LOG_DEFAULT_LEVEL=3

For the shell, you need a similar explicit backend:

CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y

Best practice: Create a separate debug.conf file with all your development-time logging and debug options. Include it during development builds and drop it for production. Keeps your prj.conf clean.

Mistake #8: Using printk() for Everything

The mistake: Coming from bare-metal, you use printk() like printf for all output. It works, but it blocks the calling thread, has no log levels, no filtering, no timestamping, and no way to route output to multiple backends.

The fix: Reserve printk() for early boot messages and crash diagnostics. Use the LOG_* macros for everything else.

/* Boot diagnostics — printk is fine here */
printk("Booting app v1.2.3\n");

/* Application messages — use the log subsystem */
LOG_INF("Sensor reading: %d", value);
LOG_ERR("Failed to connect: %d", err);

Best practice: Set CONFIG_LOG_MODE_DEFERRED=y for production firmware. This queues log messages instead of printing them synchronously, so your time-sensitive code doesn’t stall waiting for UART.

Mistake #9: Following a Tutorial Written for a Different Zephyr Version

The mistake: You follow a blog post from 2021 that targets Zephyr 2.7. You’re running Zephyr 3.5 with SDK 0.16. Config symbols have been renamed, deprecated, or reorganized. You get “unknown symbol” warnings in yellow text that you ignore, and the build either fails or produces broken firmware.

The fix: Always check what Zephyr version a tutorial targets. Use west list to see your current version:

west list | grep zephyr

Best practice: Pin your west.yml manifest to a specific Zephyr release tag. Document the exact SDK and Zephyr version in your project README. When someone on your team (or future you) comes back 6 months later, they’ll know exactly what they’re working with.

Mistake #10: Never Checking the Generated Config

The mistake: You assume prj.conf is the final truth about your configuration. It isn’t. It’s just your overrides. Board defaults, dependency resolution, and Kconfig logic all run during the build; the resolved output lands in build/zephyr/.config. You never look at it.

The fix: After every build, inspect the generated config:

grep CONFIG_BT build/zephyr/.config
grep CONFIG_LOG build/zephyr/.config

Or open it interactively:

west build -t menuconfig

This shows you the resolved state of every symbol, including ones you didn’t explicitly set.

Best practice: When debugging mysterious behavior, diff the .config output between a working build and a broken one. The difference is usually 1–2 lines, and it’ll jump right out. If you’re building devices that ultimately need to integrate with a cloud backend, getting your Kconfig right early prevents cascading issues that surface much later when you’re trying to ship.

Your Config Sanity Checklist

Tape this to your monitor until it’s muscle memory:

ZEPHYR CONFIG SANITY CHECKLIST
-------------------------------
[ ] Pristine build after config changes
[ ] CONFIG_ prefix on every prj.conf line
[ ] Dependencies enabled for each subsystem
[ ] Stack/heap sizes reviewed and increased
[ ] HW settings in .overlay, SW settings in prj.conf
[ ] Board overlay in boards/ dir, SDK untouched
[ ] Logging backend explicitly enabled
[ ] LOG subsystem preferred over raw printk
[ ] west.yml pinned to a Zephyr release tag
[ ] build/zephyr/.config inspected to confirm

Zephyr’s configuration model manages real complexity: dozens of boards, hundreds of subsystems, thousands of tunable parameters. Once you stop fighting the layers and start working with them, you’ll move fast. But that first project is going to bite you. At least now you know where the teeth are.


Hubble Network connects Bluetooth devices directly to satellites—no gateways, no infrastructure headaches. See how it works →