Skip to content

Hussam Talks Tech

Welcome to my Electronics Blog!

Menu
  • Home
  • STM32
  • KiCad Tutorial
  • ESP32
  • Raspberry Pi
  • About
Menu

Is the Zephyr framework the right choice for your embedded project?

Posted on 11.10.202611.10.2026 by halherta

I recently decided to experiment with using Zephyr to build applications for my STM32 based EmbLynx board. I found the process of developing custom board files and getting developing USB CDC functionality very cumbersome; despite having some familiarity with device trees. I worked on a simple USB CDC project for 3-4 days straight. I did manage to get it to work eventually, but it kept failing. I was able to get a similar application built in 15 minutes using STM32CubeMX. This made be reconsider using Zephyr in my personal projects.

I also realized if I wanted to build serious applications; say a waveform generator that took full advantage of the integrated DAC on the STM32G474RE, I would have to step down to the vendor HAL anyways, which would nullify the advantage of the Zephyr API portability that’s often advertised as a killer feature.

This article highlights both the advantages and disadvantages of using Zephyr vs vendor provided HAL + FreeRTOS. It’s specifically critical of Zephyr’s promise of portability across different microcontrollers and makes the case that the use of LLMs to translate hardware access layers from one vendor to the other makes Zephyr portability an even less attractive proposition.

I do think the Zephyr Framework / RTOS is an excellent ecosystem for the right kind of project and development team. If a project requires well supported USB, networking, LoraWan, Bluetooth stacks ; or MCUBoot, LVGL, FatFS or other libraries without worrying about integration, it becomes a very compelling option. Especially for more beefy crossover MCUs (think STM32V8 or NXP i.MX RT1170) that need a lot of hardware initialization and require running many stacks and libraries at the same time.

Where Zephyr came from

Zephyr’s roots go back to Virtuoso, a small commercial RTOS that Wind River picked up when it acquired Eonic Systems. Wind River later turned that code into a lightweight kernel for tiny devices, and in 2016 it was released as open source under the Linux Foundation. Since then the project has collected a long list of backers, including silicon vendors such as Nordic, NXP, Intel, TI and STMicroelectronics, who contribute board support and drivers.

That origin story has a familiar shape. Arm launched mbed in 2009 with a similar pitch: a friendly, consistent API that hides the ugly details of each vendor’s silicon. For a while it worked beautifully. Hobbyists and professionals alike could blink an LED, read a sensor and open a socket on dozens of boards with nearly identical code. Then the platform lost momentum, its online tooling was retired, the ecosystem fragmented, and Arm eventually wound the whole thing down. Mbed is now defunct.

The comparison has limits and it is not meant to predict Zephyr’s death. Mbed leaned heavily on a single sponsor, while Zephyr sits under a foundation with many members who have real money riding on it. Zephyr is in far better shape. Still, the two projects share the same core bet: that a common abstraction over many microcontrollers is worth the cost of maintaining that abstraction. Mbed’s story is a reminder that this bet depends on a lot of people continuing to do a lot of unglamorous work for a long time.

What Zephyr gets right

It would be unfair to dismiss Zephyr, because the benefits are real.

A capable RTOS comes built in. The scheduler, threads, semaphores, message queues, work queues and timers are mature and well tested. You do not have to choose, configure and integrate a kernel yourself.

Logging is included. A proper logging subsystem with levels, backends and deferred processing is available from day one. Anyone who has hand-rolled a UART printf wrapper for the fifth time will appreciate this.

Third party libraries are first class. LVGL for graphics and FatFS for storage are examples of components that are already integrated and maintained against the kernel. Setting them up is mostly a matter of turning them on.

You choose what to pull in. The west manifest lets you select which modules and HALs get fetched into your workspace, so in principle you only take what you need.

Device trees describe the hardware. Instead of scattering pin assignments and peripheral settings through C files, you describe the board declaratively. Displays, sensors, USB and networking hardware can be attached to the right drivers through that description, and swapping a sensor can be as simple as editing a node.

Peripheral APIs are easy to use. GPIO, I2C, SPI, UART, ADC and friends have common and well designed interfaces that sit on top of the vendor libraries. They do not lock you out of the vendor layer. When the generic API is not granular enough, you can reach down and call the vendor function directly.

The tooling is bundled. The SDK ships a compiler toolchain and debugging support, so a new team member can get to a working build without a day of installing things.

It is more than a portability layer. This deserves emphasis, because it is easy to reduce Zephyr to “write once, run anywhere” and miss what else it offers. Mature Bluetooth Low Energy, networking, USB and file system stacks, a bootloader in MCUboot, a serious testing and CI culture, security processes and a large community that reviews and fixes code are all valuable and expensive to replace. Many teams adopt Zephyr for these reasons and barely care about changing chips.

Taken together, that is a strong package. For a team that wants a Bluetooth stack, a file system and a display all working together quickly, Zephyr can save serious time.

What Zephyr costs

The problems are just as real, and they tend to show up late in a project, after the easy demos are done.

It is heavy. Even with a carefully trimmed manifest, a minimal workspace can balloon to gigabytes on disk. Toolchains, modules, HAL repositories and tooling add up quickly. This is not a flash or RAM complaint, it is a workflow complaint. Cloning, updating, caching in CI and reproducing builds all become slower and more fragile than a small project with a vendor SDK and a Makefile or CMakeLists.txt.

It costs more flash and RAM. For a similarly written application, a Zephyr build will generally be larger than one built on a vendor HAL and FreeRTOS, and it will usually use more RAM too. FreeRTOS is a very small kernel, and a vendor HAL used selectively adds only the drivers you call. Zephyr brings more with it: the device model with its statically allocated device structures, driver instances for every enabled node in the device tree, a main thread and an idle thread each with their own stacks, a system work queue, dedicated interrupt stacks, and kernel objects that are all created at build time. Turn on logging, a shell, or a networking stack and the numbers climb further, since each of these carries buffers, thread stacks and configuration overhead of its own.

The gap is not fixed. A stripped down Zephyr image can be quite small, and you can claw back a lot of space by tuning Kconfig options, trimming log levels, shrinking stacks and disabling features you do not use. But that tuning is itself work, and it is work you do not have to do when the baseline is already lean. On a part with 16 KB of RAM or 128 KB of flash the difference can decide whether a product is feasible at all. On a chip with a megabyte of flash it may not matter. Because the overhead depends heavily on the board, the enabled subsystems and the vendor driver quality, the only trustworthy answer comes from building the same small application both ways and comparing the map files.

Custom boards are a lot of work. Eventually most real products leave the evaluation kit behind and need their own board definition. In Zephyr that is not just a device tree. A board typically needs a board metadata file, Kconfig files, a default configuration file, the device tree itself, a yaml file describing its capabilities, and CMake files for flashing and debugging, with more again if you define a new SoC or want to upstream the work. Each of these has its own syntax and conventions, and they have to agree with each other.

Verifying all of that is a challenge in its own right. A wrong pin assignment, a missing clock setting or a subtly incorrect node property will often build without complaint and then misbehave at runtime in ways that are hard to trace back to a text file. With a vendor SDK you can write a small init function, step through it in a debugger and see exactly what happened. With a custom Zephyr board you are often checking generated output and resolved configuration to find out what the build system actually decided.

The vendor library often gives better control. If you want to push a chip to its limits, such as unusual DMA chaining, precise clock tree tuning, low power modes with exact wake sources or peripheral features that are only present on one family, the generic Zephyr API will frequently not expose what you need. You then call the vendor API alongside the Zephyr one. At that point you carry both layers in your head, and you have given up part of the portability that justified the framework in the first place.

Bugs have an extra place to hide. With a vendor HAL and FreeRTOS, a misbehaving peripheral points at your code, the HAL or the silicon. With Zephyr there is another layer in between: the driver model, the device tree bindings, the Kconfig options and the abstraction itself. To be fair, vendor HALs have plenty of bugs of their own, and some are notorious. But a problem in Zephyr’s layer can be harder to recognize, because everything looks correct at the API level, and fixing it may mean learning how a driver was shaped to serve many chips before you can safely change it for yours.

Device trees are hard to grasp. They are a powerful idea, borrowed from Linux, but the learning curve is steep. Overlays, bindings, node labels, aliases, pin control and the interaction with Kconfig all have to line up. When something fails, the error often shows up far from the cause, and the fix involves reading generated files rather than your own source.

The macro usage is heavy. Much of Zephyr’s configuration and instantiation happens through layers of C preprocessor macros. This is clever and it works, but it runs against the advice most of us have been given for years about keeping code readable, debuggable and type checked. Stepping through a macro expansion in a debugger is nobody’s idea of a good afternoon.

The stack is opinionated. Zephyr is built around its own kernel, logging, driver model and way of organizing a project. Much of it can be switched off or configured through Kconfig, but customizing these services deeply, or loading only the small slice you need, is harder than it would be in a project you assembled yourself. If your product has its own logging scheme or a kernel arrangement that does not fit Zephyr’s assumptions, expect some friction.

Support is uneven. Not every microcontroller is fully supported, and it can be difficult to tell how much of the Zephyr API a particular part actually implements. A board can be listed as supported while a specific peripheral mode you depend on is missing or marked experimental. You often only find out by trying.

The portability promise is weaker than it looks

Portability is the headline feature, so it is worth being honest about how far it actually goes. The common peripheral APIs are great for the basics. But real products rarely stay in the basics. The moment you need a particular DMA behavior, a specific low power state, a timer feature or a radio setting that the generic API does not model, you reach for the vendor library. In practice this happens in many projects, perhaps most of them.

Once it does, those parts of your code are tied to one vendor again, only now they sit inside a framework that adds its own layer of configuration. You get the maintenance burden of both worlds and only a partial return on the portability you paid for. This is true even before we bring language models into the picture. An abstraction that covers the easy 80 percent of hardware access is useful, but it is not the same as making the hardware completely interchangeable.

The real trade

Put these points together and a pattern emerges. Embedded development with vendor libraries is complicated because hardware is complicated. Zephyr does not remove that complexity. It moves it. Instead of wrestling with register-level details and HAL quirks, you wrestle with project layout, west manifests, Kconfig fragments, overlays, board definitions and device tree semantics. For some teams that is a good exchange, because the new complexity is shared across projects and written down in a standard way. For others it is simply a different kind of difficulty, and one that is harder to debug.

Enter the Large Language Model (LLM)

Let us be clear about what large language models are and are not. They are not magic. They will invent functions that do not exist, mix up two chips in the same family, miss errata and produce code that compiles and still does the wrong thing. Nobody should expect to paste in a project and receive a finished, trustworthy port.

What they can do is cut the typing and lookup work substantially, and they are best at the hardware controlling layers. Translating an STM32 HAL SPI transaction into the equivalent call sequence for an NXP, Nordic or Microchip library is mostly pattern matching. The structure of the program stays the same. The names, handles, initialization order and error codes change. That is exactly the kind of task where a model can produce a useful first draft in minutes that would otherwise take hours of reading examples and reference manuals.

The application layer is a different story, and ideally you do not need the model there at all. If your application logic is cleanly separated from hardware access, behind a thin set of functions for each peripheral, then it should carry over untouched. FreeRTOS helps here too, since the kernel API is the same everywhere and tasks, queues, mutexes and timers move without changes. The part that changes is the layer that talks to the silicon, and that is the part to hand to the model.

A practical workflow looks something like this:

  1. Keep application logic separate from hardware access, with a thin driver layer for each peripheral your product uses.
  2. Give the model the existing implementation of that layer (hardware access), the target vendor’s headers or reference manual excerpts, and a description of the new board.
  3. Ask for a new implementation with the same function signatures.
  4. Review it against the datasheet, compile it, and test it on the bench.

The review step is the whole game. The developer remains responsible for correctness, and the savings come from starting with a draft instead of a blank file, not from skipping verification. Reviewing and fixing a draft is cheaper than writing from scratch, and the result is plain C that you can read, step through and own. The gain is meaningful and worth having. It is a speedup, not a revolution.

Why this keeps more control

This is where the approach compares well with relying on Zephyr’s portability layer.

When you port with an LLM, the result is written directly against the vendor’s own library. If the new chip has a feature the old one lacked, you simply use it. If you need to tune a timing parameter, switch a clock source or set up a peripheral in a way that a common API would never expose, you can do it in the same code, with no translation layer deciding what is allowed. You never face the choice of stepping outside the abstraction and losing its benefits, because there is no abstraction to step outside of.

You also keep a smaller, simpler toolchain and a smaller binary. A vendor SDK, a compiler and FreeRTOS fit in a project that builds quickly and can be understood by one person reading the repository, and the resulting firmware typically needs less flash and RAM than the equivalent Zephyr image, which lets you choose cheaper parts or leave more headroom for features. Your board bring-up is also a short init function you can debug directly, rather than a set of definition files that must all agree.

And the portability is not lost, it is just produced differently. Instead of paying for it continuously, through every build and every bug, you pay for it when you actually need it, at the moment of a port. The model absorbs part of that one-time cost.

So does Zephyr still make sense?

It does, for some teams, where the case is stronger than portability alone. If you need a mature Bluetooth, networking or USB stack that you would rather not integrate yourself, if you value the testing, security processes and community of a large open source project, or if your silicon vendor’s own SDK is already built on Zephyr, then adopting it is a sensible and often easy decision. Teams shipping across several chip families will also find real value in a shared framework, as long as they accept that some code will still go through vendor libraries.

But the case is narrower than it used to be. A large part of Zephyr’s original appeal was avoiding the pain of rewriting hardware code for each new chip, and that appeal was always weaker than advertised because serious projects end up calling vendor APIs anyway. LLMs make the rewrite cheaper still, and they let developers keep direct access to everything the hardware can do. For a small team, a product built around one or two chips, or any project where squeezing the most out of the silicon matters, a vendor HAL with FreeRTOS and an LLM-assisted porting workflow is a compelling alternative, and perhaps the better default.

The lesson from mbed is not that abstraction layers are doomed. It is that they carry an ongoing cost, and that cost has to keep being worth paying. In the era of LLMs, it is worth checking the math again before you commit.

Category: C and C++, STM32

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

October 2026
M T W T F S S
 1234
567891011
12131415161718
19202122232425
262728293031  
« Sep    
  • C and C++
  • Electronics
  • ESP32
  • KiCad
  • KiCad Tutorial
  • Linux
  • Micropython
  • Python
  • Raspberry Pi
  • STM32

Archives

  • October 2026
  • September 2026
  • August 2026
  • January 2026
  • November 2025
  • October 2025
  • September 2025
  • August 2025
  • July 2025
© 2026 Hussam Talks Tech | Powered by Minimalist Blog WordPress Theme