ukasol

Blog Zephyr Series · 03/07

Why I use Zephyr

Every part of Zephyr leans on the others, and that is deliberate. One blinky, three boards, three drivers: what that coherence buys you, and where it costs.

I saw how powerful Zephyr is on a real project, and I have chosen it for every project since, whenever I could.

What surprised me when I started learning it was how all the subsystems complement each other and work together. In fact, you cannot really think of one without the others. For example, the reason native_sim works so smoothly is that Device Tree, the generic APIs and CMake are already part of every project. That is no accident; those are deliberate design decisions.

Once I noticed that coherence, the way I looked at Zephyr changed.

“Coherence” is an abstract word, so here it is in concrete form first, and then what it means for each of the features people usually list.

One application, three boards, three drivers#

Take Zephyr’s blinky sample and build the same source for three very different targets:

$ west build -b nrf52840dk/nrf52840 samples/basic/blinky
$ west build -b nucleo_f401re samples/basic/blinky
$ west build -b native_sim samples/basic/blinky

With Zephyr 4.4.2, all three build from the same main.c, without a single #ifdef. What differs is underneath:

Board led0 in the devicetree GPIO driver the build compiled
Nordic nRF52840 DK P0.13, active low gpio_nrfx.c
STM32 Nucleo F401RE PA5, active high gpio_stm32.c
native_sim pin 0 of an emulated controller gpio_emul.c

Nobody chose those drivers by hand. The chain that picks them runs through three subsystems, and it is worth following once:

  1. The application asks the devicetree for the led0 alias and gets a pin on a GPIO controller node.
  2. That controller node carries a compatible string: "nordic,nrf-gpio", "st,stm32-gpio" or "zephyr,gpio-emul".
  3. Every enabled compatible becomes a Kconfig symbol. On the Nucleo build, .config contains CONFIG_DT_HAS_ST_STM32_GPIO_ENABLED=y.
  4. The driver’s own Kconfig option defaults to on when that symbol is set.
  5. CMake compiles gpio_stm32.c because CONFIG_GPIO_STM32=y, and the driver registers itself behind the generic GPIO API that main.c calls.

Step 4 is four lines in drivers/gpio/Kconfig.stm32:

config GPIO_STM32
    bool "GPIO Driver for STM32 family of MCUs"
    default y
    depends on DT_HAS_ST_STM32_GPIO_ENABLED || DT_HAS_ST_STM32MP2_GPIO_ENABLED
main.cgpio_pin_toggle_dt(&led) GPIO APIone interface, any controller gpio_nrfx.cnRF52840 DK, P0.13"nordic,nrf-gpio" gpio_stm32.cNucleo F401RE, PA5"st,stm32-gpio" gpio_emul.cnative_sim, pin 0"zephyr,gpio-emul"
One main.c, three builds. The compatible string in the devicetree switches the matching driver on; the application never names it.

That is the whole reason native_sim feels effortless. To the application it is not a simulator bolted on from outside. It is just another board, whose GPIO controller happens to be emulated, picked by exactly the same chain. Remove any one link (the devicetree, the Kconfig symbols generated from it, the generic API, the build system that reads both) and it stops working.

What stands out, and how each piece leans on the others#

native_sim: You can start developing firmware without hardware. Work moves on without waiting for the boards. As above, it exists because a board is only a set of devicetree nodes and drivers behind generic APIs.

Twister and ztest: The test infrastructure comes built in. There is nothing separate to set up and integrate. A test suite is declared in a testcase.yaml, and the same suite runs on native_sim in CI and on a real board with twister --device-testing. That is only possible because the board is a build argument, not something baked into the code.

Drivers, Kconfig and Device Tree: Together they give you a codebase that is hardware independent, portable and easy to maintain. The table above is that sentence, measured.

BLE and networking stacks: You build on top of them instead of writing them from scratch. Zephyr has its own Bluetooth LE host and an open source link layer, an IPv4 and IPv6 stack with TCP and UDP, and protocol libraries such as MQTT, CoAP and LwM2M on top, with Thread available through the OpenThread module. They are configured with the same Kconfig and run on the same driver model as everything else, so enabling Bluetooth starts with CONFIG_BT=y on a board whose devicetree already describes the radio.

Security: Memory protection, secure boot and mbedTLS integration come ready to use. Threads can run in user mode behind the MPU (CONFIG_USERSPACE), MCUboot provides the verified boot chain, and on Armv8-M parts Trusted Firmware-M is available as a module. The pieces fit here too: MCUboot finds its slots in the same devicetree partitions the application is linked into. On the nRF52840 DK they are called mcuboot, image-0 and image-1, and the build places the application in image-0 because the devicetree says so.

Power management: Integrated at the kernel level, so you do not have to write it yourself. A tickless kernel knows exactly when the next thread is due, the system power manager uses that to pick a sleep state, and drivers that support device power management switch their peripheral off while nobody is using it.

Support for more than 1,000 boards: In Zephyr 4.4.2, west boards lists 1,014 of them. When the hardware changes, you do not rewrite the application; the blinky above is the small version of that promise.

Fully open source: No vendor lock-in. Zephyr is Apache-2.0 licensed and governed under the Linux Foundation, so the code you depend on is code you can read, debug and fix.

None of these is decisive on its own. What is decisive is that they all come at once, and they all fit together.

Where it is not the right choice#

“Whenever I could” is doing real work in the first sentence of this post. Zephyr is not free, and there are places where its costs outweigh what it gives back.

The smallest parts. Zephyr goes surprisingly low: on the STM32F401, the minimal sample builds to 6.1 KB of flash single-threaded and 8.7 KB with multithreading. But a realistic application with a console, logging and a few drivers quickly reaches tens of kilobytes; Hello World alone, with its console, is 16.8 KB. On a part with 16 or 32 KB of flash that budget is tight.

Simple products that will not change. A single control loop with no connectivity, on hardware that will be built unchanged for ten years, gets little from portability and abstraction. There, the layers are cost without benefit.

Brand-new silicon. Support for a new chip often lands in the vendor’s own SDK before it reaches upstream Zephyr, sometimes in a Zephyr-based SDK such as Nordic’s nRF Connect SDK. Check the board and SoC support for the exact part before you commit a schedule to it.

A deadline next month. The learning curve is real, as the first post in this series describes. Moving a team to Zephyr in the middle of a crunch is the worst time to pay it.

Outside those cases, the coherence is what keeps me coming back. Each piece on its own exists elsewhere too. All of them together, designed to lean on each other, is the reason I reach for Zephyr first.

Next in · Zephyr fundamentals Why Zephyr, for companies