Skip to main content


Geoffrey Hunter Author
This page is in notes format, and may not be of the same quality as other pages on this site.


Zephyr is the combination of a real-time operating system, peripheral/system API framework, and build system that is designed for resource-constrained devices such as microcontrollers. Is is part of the Linux Foundation.

The Zephyr Project logo.

Zephyr provides a firmware developer with a rich ecosystem of out-of-the-box OS and peripheral functionality that is consistent across MCU manufacturers (e.g. you can use the same UART API on both a STM32 and nRF53 MCU). It also features an integrated build system called west.

Some criticism arises from Zephyr's complexity. Once such example is that Zephyr adds another layer of configurability with the use of Kconfig (a configuration language that was originally designed to configure the Linux kernel1). Firmware behaviour can now be controlled via a combination of Kconfig settings, preprocessor macros and API calls at runtime. The Kconfig is layered in a hierarchical manner and dependencies and overrides can make it difficult to determine how your MCU is being configured.

The main repo can be found on GitHub.

Zephyr provides cooperative and preemptive scheduling.

Uses a CMake build environment.

Zephyr is cross-platform (i.e. you can build projects and flash embedded devices) and supported on Linux, Windows and macOS. However, you will experience the least amount of issues and friction running Zephyr on Linux. Linux is also the only platform currently supported by the Zephyr SDK.

Zephyr is also a platform supported by the PlatformIO build system and IDE.


Below are some basic Zephyr installation guides for various OSes. Another good read is the official Getting Started Guide.


The easiest way to install Zephyr on Windows is to use the chocolatey package manager. Once that is installed, run the following steps from an elevated command prompt:


Make sure to use a command-prompt and not PowerShell, as PowerShell does not play nice with the set method of defining environment variables.

  1. Enable global confirmation so that you don't have to manually confirm the installation of individual programs:

    > choco feature enable -n allowGlobalConfirmation
    Chocolatey v0.10.15
    Enabled allowGlobalConfirmation
  2. Install Zephyr dependencies:

    > choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
    > choco install ninja gperf python git

    If you already have some of these packages installed on your machine via means other than chocolatey, it's advisable to remove them from the above command so you don't install them again and have one program shadow/conflict the other.

  3. Install the Zephyr command-line tool called west (NOTE: I have run into problems using a Python virtual environment, so I install west to the OS version of Python, more on this below):

    > pip install west
  4. Create a new Zephyr project:

    > west init myproject
    === Initializing in C:\Users\gbmhunter\myproject
    --- Cloning manifest repository from, rev. master
    Initialized empty Git repository in C:/Users/gbmhunter/myproject/.west/manifest-tmp/.git/
    remote: Enumerating objects: 55, done.
    remote: Counting objects: 100% (55/55), done.
    remote: Compressing objects: 100% (42/42), done.
    Branch 'master' set up to track remote branch 'master' from 'origin'.
    --- setting manifest.path to zephyr
    === Initialized. Now run "west update" inside C:\Users\gbmhunter\zephyrproject.

    WARNING: I got the following error when trying to use a Python virtual environment rather than the OS Python environment to install west and then run west init:

    > python -m venv .venv
    > .\.venv\Scripts\activate
    > pip install west
    > west init myproject
    Updating files: 100% (14758/14758), done.
    Already on 'master'
    Branch 'master' set up to track remote branch 'master' from 'origin'.
    Traceback (most recent call last):
    File "C:\Users\gbmhunter\AppData\Local\Programs\Python\Python38-32\lib\", line 788, in move
    os.rename(src, real_dst)
    PermissionError: [WinError 5] Access is denied: 'C:\\Users\\gbmhunter\\myproject\\.west\\manifest-tmp' -> 'C:\\Users\\gbmhunter\\myproject\\zephyr'
  5. Change into the project directory and run west update:

    > west update

    This command can take a few minutes to run as it clones a number of repositories into the project directory.

  6. Export a Zephyr CMake package to your local CMake user package registry. This allows CMake to automatically find a Zephyr "base":

    > west zephyr-export
    Zephyr (C:/Users/gbmhunter/zephyrproject/zephyr/share/zephyr-package/cmake)
    has been added to the user package registry in:

    ZephyrUnittest (C:/Users/gbmhunter/zephyrproject/zephyr/share/zephyrunittest-package/cmake)
    has been added to the user package registry in:
  7. Download and install the GNU Arm Embedded Toolchain from By default it will want to be installed on your system at C:\Program Files (x86)\GNU Arm Embedded Toolchain\10 2020-q4-major>, but don't let it! Zephyr does not like spaces in the path, so install it to a path which has none. I chose C:\gnu-arm-embedded-toolchain\10-2020-q4-major\:

  8. Setup environment variables:

    set GNUARMEMB_TOOLCHAIN_PATH=C:\gnu-arm-embedded-toolchain\10-2020-q4-major
  9. Install the Python dependencies that Zephyr

  10. Build the application! For this example I will be using the ST NUCLEO F070RB development board. Note that you can't build this from the project root directory, first you have to change into the zephyr subdirectory:

    > cd .\zephyr
    > west build -b nucleo_f070rb samples/basic/blinky
    C:\Users\gbmhunter\temp\myproject\zephyr>west build -b nucleo_f070rb samples/basic/blinky
    [121/128] Linking C executable zephyr\zephyr_prebuilt.elf

    [128/128] Linking C executable zephyr\zephyr.elf
    Memory region Used Size Region Size %age Used
    FLASH: 13544 B 128 KB 10.33%
    SRAM: 4416 B 16 KB 26.95%
    IDT_LIST: 0 GB 2 KB 0.00%
  11. Install OpenOCD from I extracted the .zip file and then copied the files to C:\Program Files\OpenOCD\bin.

  12. Flash the application:

    west flash

    NOTE: I got an error when running this:

      File "C:\Users\gbmhunter\temp\myproject\zephyr\scripts/west_commands\runners\", line 504, in require
    if shutil.which(program) is None:
    File "C:\Users\gbmhunter\AppData\Local\Programs\Python\Python38-32\lib\", line 1365, in which
    if os.path.dirname(cmd):
    File "C:\Users\gbmhunter\AppData\Local\Programs\Python\Python38-32\lib\", line 223, in dirname
    return split(p)[0]
    File "C:\Users\gbmhunter\AppData\Local\Programs\Python\Python38-32\lib\", line 185, in split
    p = os.fspath(p)
    TypeError: expected str, bytes or os.PathLike object, not NoneType


Follow these instructions to setup/install Zephyr on UNIX like systems, including Ubuntu and WSL. Use of apt assumes you are running a Debian like system.

  1. Firstly, install system dependencies:

    sudo apt install --no-install-recommends git cmake ninja-build gperf \
    ccache dfu-util device-tree-compiler wget \
    python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \
    make gcc gcc-multilib g++-multilib libsdl2-dev
  2. Create a new directory for the Zephyr project and a Python virtual environment in a .venv sub-directory:

    mkdir ~/zephyr-project
    cd ~/zephyr-project
    python3 -m venv .venv
    source .venv/bin/activate
  3. Install west in the new virtual environment (west is a Python package):

    pip install west
  4. Initialize the west workspace:

    west init .

    This creates a directory called .west inside the current working directory. Inside .west another directory called manifest-tmp.

  5. Update:

    west update

    west update downloads a lot of Git submodules that are present in the project under ./modules/.

  6. Export a Zephyr CMake package:

    west zephyr-export

    This adds Zephyr to the user package registry at ~/.cmake/packages/Zephyr (which is outside of the project root directory).

  7. Install additional Python dependencies:

    pip install -r ./zephyr/scripts/requirements.txt
  8. Next, download the Zephyr SDK. The SDK contains toolchains (compilers, linkers, assemblers and other tools such as QEMU and OpenOCD) for all of Zephyr's supported architectures (e.g. ARM, RISCV64, x86_64, Xtensa, aarch64). Find the release you want from (use the latest if you don't otherwise care). The example below uses v0.16.4.

    This is downloaded and installed outside of the Zephyr project directory. You only need one copy of this for many Zephyr projects.

    cd ~

    This file is over 1GB in size, so make sure you have space! There is also a minimal release which doesn't come with all the toolchains, but instead let's you select and download the ones you needed.

  9. Extract the downloaded SDK:

    tar xvf zephyr-sdk-0.16.4_linux-x86_64.tar.xz

    This will extract the SDK to ~/zephyr-sdk-0.16.4. You can choose to move it somewhere else if you want. Other common choices include /opt/ and /usr/local/.

  10. Run the Zephyr SDK setup script:

    cd zephyr-sdk-0.16.4
  11. Install udev rules, which let's you program most boards as a regular user (serial port permissions):

    sudo cp ~/zephyr-sdk-0.16.4/sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d
    sudo udevadm control --reload
  12. cd into the zephyr directory (a sub-directory of the project directory) and build the Blinky sample:

    cd ~/zephyr-project/zephyr
    west build -p auto -b nucleo_f070rb samples/basic/blinky
  13. Make sure the dev. kit is plugged into the computer, and then flash the application onto the dev kit (remember we were using the NUCLEO-F070RB for this example):

    west flash

Both west init and west update can take some time to run (2-5mins each).

If you don't have a dev. board, you could always test out Zephyr by building it for Linux.

west build -p auto -b native_sim samples/basic/blinky

The build executable is at ./zephyr/build/zephyr/zephyr.exe (even though the extension is .exe, it is compiled for Linux not Windows). Of course, there is no basic LED we can blink when running on Linux, but it is mocked for us and instead prints to stdout:

~/zephyr-project/zephyr/build/zephyr$ ./zephyr.exe 
*** Booting Zephyr OS build zephyr-v3.5.0-4014-g0d7d39d44172 ***
LED state: OFF
LED state: ON
LED state: OFF
LED state: ON
LED state: OFF
LED state: ON

Creating An Application

Once you've followed the installation instructions, you should be able to build and flash repository applications that are contained with the Zephyr repo (e.g. blinky). But what if you want to develop your own application? This is were is pays to understand the three different application types:

  • Repository Application: An application contained with the Zephyr repository itself (inside the zephyr/ directory in the workspace you created above). Typically these are example projects under samples/ like "Hello, world!" and "blinky". In the directory structure below, hello_world is a repository application.
    ├─── .west/
    │ └─── config
    └─── zephyr/
    ├── arch/
    ├── boards/
    ├── cmake/
    ├── samples/
    │ ├── hello_world/
    │ └── ...
    ├── tests/
    └── ...
  • Workspace Application: An application that is within the west workspace, but not within the zephyr repository (zephyr/). This is the way I recommended you create an application if you're learning! In the directory structure below, app is a workspace application:
    ├─── zephyr-project/
    │ ├── .west/
    │ ├── app/
    │ │ ├── CMakeLists.txt
    │ │ ├── prj.conf
    │ │ └── src/
    │ │ └── main.c
    │ ├── zephyr/
    │ ├── bootloader/
    │ ├── modules/
    │ └── ...
  • Freestanding Application: An application that is not within the west workspace, i.e. stored somewhere else entirely on your disk. In the directory structure below, app is a freestanding application:
    ├─── zephyr-project/
    │ ├─── .west/
    │ │ └─── config
    │ ├── zephyr/
    │ ├── bootloader/
    │ ├── modules/
    │ └── ...

    └─── app/
    ├── CMakeLists.txt
    ├── prj.conf
    └── src/
    └── main.c

Creating a Workspace Application

If you are learning, I'd recommend you start by creating a bare bones workspace application to learn what the core files are for. Create a directory called app in the workspace:

cd ~/zephyr-project
mkdir app

Now create a file called CMakeLists.txt with the follow context:

cmake_minimum_required(VERSION 3.20.0)


target_sources(app PRIVATE src/main.c)

Create an empty prj.conf. It needs to exist, but you don't need anything in it for now.

The last thing you need to create a directory called src to store all the source code, and then create a file in it called main.c with the following contents.

#include <stdio.h>
#include <zephyr/kernel.h>

int main(void) {
while (1) {
printf("Hello, world!\n");
return 0;

Your basic app is done! Your file structure should look like this:

├─── zephyr-project/
│ ├── .west/
│ ├── app/
│ │ ├── CMakeLists.txt
│ │ ├── prj.conf
│ │ └── src/
│ │ └── main.c
│ ├── zephyr/
│ ├── bootloader/
│ ├── modules/
│ └── ...

To build and run, first cd back into the west workspace at ~/zephyr-project/. The run this at the command-line:

west build -b native_sim ./app
west build -t run

The first command performs a build, telling west to use the native_sim board (runs on Linux) and to build the app located at ./app. The second command tells west to run it, and you should get output similar to this:

(.venv) geoff@my-computer:~/zephyr-project$ west build -t run
-- west build: running target run
[1/2] cd /home/geoff/zephyr-project/build && /home/geoff/zephyr-project/build/zephyr/zephyr.exe
*** Booting Zephyr OS build zephyr-v3.5.0-4014-g0d7d39d44172 ***
Hello, world!
Hello, world!
Hello, world!

Getting VSCode IntelliSense Working

The most effective way of getting VS Code's IntelliSense working well with Zephyr is to use the "compile_commands" method as described below. I've found this to be much more effective than trying to provide include paths.

Add the following to your .west/config file:


This will tell CMake to always generate a compile_commands.json file when building.

Add the following to your .vscode/c_cpp_properties.json file:

"configurations": [
"name": "App Config",
"compileCommands": "${workspaceFolder}/build/compile_commands.json"
"version": 4

This will tell VS Code to use the compile_commands.json file for IntelliSense. The above snippet assumes your build directory is build/. Change this as needed.

Rebuild your project from scratch. A compile_commands.json should be generated in your build directory. Thus will be picked up by VSCode's Intellisense and should fix any include errors you have!

If you are working on multiple apps with one west workspace, you can separate configurations for each. The following example sets up two configurations, one for the application and one for it's ztest unit tests that are in a tests/ sub-directory, and with a build folder set to build-tests:

"configurations": [
"name": "App Config",
"compileCommands": "${workspaceFolder}/build/compile_commands.json"
"name": "Tests Config",
"compileCommands": "${workspaceFolder}/build-tests/compile_commands.json"
"version": 4,
"enableConfigurationSquiggles": true

Once this is added, you can select the active configuration from the right-hand side of the bottom toolbar within VS Code.

Instead of adding to .vscode/c_cpp_properties.json, if you just have one configuration, you can instead add it to .vscode/settings.json:

"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json",

If you want more info on Zephyr unit tests and IntelliSense, see the Tests section of this page.

Moving a West Workspace

I haven't had much luck moving a West workspace on Linux. After moving, the west executable could not be found (making sure the Python virtual environment was activated).

Hardware Abstraction Layers

Examples of some of the HALs (which are installed under <project root dir>/modules/hal/) supported by Zephyr:

  • ST
  • STM32
  • NXP
  • Atmel
  • Microchip

Zephyr SDK

The Zephyr SDK contains toolchains to compile, assemble, link and program/debug Zephyr applications. Currently it is only supported on Linux. On Windows and macOS, you have to manually install the toolchains you require for your Zephyr application.

Supported Boards

See for a comprehensive list of all the development boards supported by the Zephyr platform.

Native Simulator (native_sim)

native_sim allows you to build a Zephyr application to run on POSIX-like OSes, e.g. Linux.


native_sim is a successor to the legacy native_posix Zephyr board. Use native_sim instead wherever possible.

To build for POSIX, provide native_sim as the build target to west with the -b option, e.g.:

west build -b native_sim samples/hello_world

You can then run the built application with:

west build -t run

The native_sim board supports the following APIs:

  • GPIO (mocked)
  • Watchdog
  • Timers

Device Trees

Zephyr borrows the concept of device trees popularized by Linux to describe the hardware that the firmware is running on. Device trees are computer-readable text files that have a hierarchical structure.



/ {
a-node {
a-subnode-node-label: a-sub-node {
a-property = <3>;

a-node is a node. It is freely chosen identifier for a container. a-sub-node is a subnode. It is also a freely chosen identifier.

Basic Example


/ {
soc {
uart0: serial@12340000 {

Getting Nodes

In C, to grab a node from the device tree you can use:

By path

Use DT_PATH() to get the node from a fully quality path from the root node. e.g DT_PATH(soc, serial_12340000). Note that non alphanumeric characters in the devicetree are converted to _, including hyphens and at symbols.

Do not use camel case when naming nodes, as DT_PATH() cannot work with these.

/ {
mySoc { // Bad! DT_PATH() won't like this
serial@12340000 {

mysoc { // Good!
serial@12340000 {

By node label


Compiled Full Device Tree

When you perform a build, a final, merged device tree gets generated at build/zephyr/zephyr.dts. This file is useful for debugging device tree issues.

Full Example

Example device tree (for the STM32F070RB development board):

* Copyright (c) 2018 qianfan Zhao
* SPDX-License-Identifier: Apache-2.0

#include <st/f0/stm32f070Xb.dtsi>
#include "arduino_r3_connector.dtsi"

/ {
model = "STMicroelectronics NUCLEO-F070RB board";
compatible = "st,stm32f070rb-nucleo", "st,stm32f070";

chosen {
zephyr,console = &usart2;
zephyr,shell-uart = &usart2;
zephyr,sram = &sram0;
zephyr,flash = &flash0;

leds {
compatible = "gpio-leds";
green_led_2: led_2 {
gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>;
label = "User LD2";

gpio_keys {
compatible = "gpio-keys";
user_button: button {
label = "User";
gpios = <&gpioc 13 GPIO_ACTIVE_LOW>;

aliases {
led0 = &green_led_2;
sw0 = &user_button;

&usart1 {
current-speed = <115200>;
status = "okay";

&usart2 {
current-speed = <115200>;
status = "okay";

&i2c1 {
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;

&i2c2 {
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;

&spi1 {
status = "okay";

&spi2 {
status = "okay";

&iwdg {
status = "okay";

System/OS Features


System on time can be read with k_uptime_get() which returns a int64_t with the number of milliseconds since system start.

For higher level precision, you can measure time in either ticks or cycles.

Cycles are the fastest clock that you have available. You can use k_cycle_get_32() to get a uint32_t of the system's hardware clock. You can then use functions like k_cyc_to_us_floor64() to convert this into an equivalent number of microseconds:

uint64_t currentTime_us = k_cyc_to_us_floor64(k_cycle_get_32());

Although I could not find it explicitly mentioned anywhere in the Zephyr documentation, it appears that it is safe to use cycles for time measurements even if the microcontroller is sleeping. I suspect Zephyr updates the cycle count when the system wakes back up to account for the duration of the sleep.

Be careful with the 32-bit value from k_cycle_get_32(). With a fast clock, this could overflow pretty quickly. If you are just interested in the duration between two time points, luckily the maths of subtracting a large unsigned number from a smaller one still gives you the right duration, until of course the later time catches up and passes the same uint32_t value the previous time point was at.


Zephyr Timers are an OS primitive that you can start and then have timeout after a certain duration. If you provide a callback, you can to run things after a fixed duration in the future in either a one-off (one-shot) or continuous manner. If you don't provide a callback, you can still inspect the state of the timer from your application.

You do not have to add anything to prj.conf to use timers. First you'll need to include the following header file which defines the timer API:

#include <zephyr/kernel.h>

You create a timer object and initialize it with:

struct k_timer myTimer;

void MyHandlerFn(struct k_timer * timer)

// Initialize it with an expiry function, but no stop function
void k_timer_init(&myTimer, &MyHandlerFn, NULL);

expiryFn and stopFn are both optional and can be NULL if you don't want anything to be called when the timer expires or stops.


Note that the function you pass in as expiryFn gets executed in the system interrupt context. Thus you have to be careful not to block in the expiry function, take too much time processing or call things that are not ISR safe.

stopFn gets called in the context of the thread which stopped the timer. Thus, if you stop the timer in a ISR, you need to make sure the stop function is ISR safe.

You can then start a timer with void k_timer_start(struct k_timer * timer, k_timeout_t duration, k_timeout_t period).

// Start the timer 
k_timer_start(struct k_timer * myTimer, K_MSEC(1000), K_NO_WAIT);

duration is the time before the timer expires for the first time. period is the time between expires after the first one. period can be set to K_NO_WAIT or K_FOREVER to make the timer only expire once (one-shot).

Here is a basic example:

#include <stdio.h>
#include <zephyr/kernel.h>

extern void MyExpiryFn(struct k_timer * timerId) {
printf("Timer expired!\n");

int main(void) {
struct k_timer myTimer;
printf("Creating timer to expire every 1s...\n");
k_timer_init(&myTimer, MyExpiryFn, NULL);
k_timer_start(&myTimer, K_MSEC(1000), K_MSEC(1000));

while (1) {
return 0;
Running the timer example code above and observing the expiry function run every second.

You can read the official Zephyr documentation for Timers here.


A Zephyr thread is a kernel object which can be used to execute code asynchronously to other threads. Threads can operate both cooperatively (a thread continues running until it gives up control) and pre-emptively (the thread is interrupted by the kernel when the kernel decides to run something else).

A basic thread can be created and started with the following code:

#define PRIORITY   (5)
#define STACK_SIZE (500)

// Statically define the thread stack. Would be handy if we could do this dynamically.

void MyThreadFn(void *, void *, void *)
LOG_INF("Thread started!");
while(1) {

int main() {
// Dynamically create thread
struct k_thread myThread;
k_tid_t myTid = k_thread_create(
&myThread, myThreadStack,
NULL, NULL, NULL, // User data you can pass to your thread function if desired

// Wait for thread to finish (which won't happen because we never return from the
// thread function)
k_thread_join(myTid, K_FOREVER);

Remember that a thread stack is a different object to just a general purpose "stack" (which Zephyr also provides to the user for general FILO functionality).

Dynamic Thread Stack Allocation

You'll notice that in the above example, although the thread is created at runtime, the stack is statically defined at compile time with K_THREAD_STACK_DEFINE(). Zephyr is meant to support dynamic thread stack allocation. However I could not get it working.


A Zephyr workqueue is like a thread but a few extra features included, the main one being a "queue" in which you can add work to for the thread to complete.

What you submit to a workqueue is a function pointer. This function will be run when the thread processes the item from the queue. This is very similar to the way you would typically create a thread, except that usually thread functions in embedded systems are designed to never return (i.e. they are designed to be created when the firmware starts-up and run continuously).

The following C code shows a basic work object being statically defined using the K_WORK_DEFINE() macro, and then work submitted in main() using k_work_submit(). The program will log the "Hello" message when the workqueue processes the work in the workqueue thread. Note that k_work_submit() submits work to the special system workqueue (explained below).

static void myWorkQueueHandler(struct k_work * work)
LOG_INF("Hello from the work thread!");

K_WORK_DEFINE(my_work_queue, &myWorkQueueHandler);

int main()
int rc = k_work_submit(&my_work_queue); // Submits work to the system workqueue, myWorkQueueHandler() will get called soon from a different thread...
// Allow any of the positive return codes but don't allow errors
__ASSERT_NO_MSG(rc >= 0);

return 0;

System Workqueue

The Kernel defines a standardized "system workqueue" that you can use. It is recommended that you use this workqueue by default, and only create additional ones only if you need multiple work queue jobs to run in parallel (e.g. in one job may block or otherwise take a long time to complete). The reason for this is that every new workqueue requires a stack, and a typical stack size could be 2kB or more. Having many workqueues will quickly eat into your remaining available RAM2.

Work can be submitted to the system workqueue by using the function k_work_submit(). Use the more generic k_work_submit_to_queue() if you want to submit work to a queue that you created (in this case, you also have to pass in a pointer to the queue).

Creating Your Own Workqueue

If you can't just use the system workqueue and want to create your own workqueues, you can use the functions k_work_queue_init() and k_work_queue_start() to do so. You first have to create a stack object before creating the workqueue, passing the stack object into it.

The following code example shows how to do this:

#define STACK_SIZE 512 // Stack size of work queue
#define PRIORITY 5 // Priority of work queue


struct k_work_q myWorkQueue;




A Zephyr mutex is a kernel primitive that allows multiple threads to safely access a shared resource by ensuring mutually exclusive access. It is provides the same functionality as mutexes in most other operating systems.

First you need to define and initialize the mutex:

struct k_mutex myMutex;

int rc = k_mutex_init(&myMutex);
__ASSERT_NO_MSG(rc == 0);

If you want to declare a mutex statically with file-level scope, rather than the above you can just use K_MUTEX_DEFINE(myMutex);. Presumably it declares the struct and sets up the init function to be run at startup.

You can then lock the mutex with:

int rc = k_mutex_lock(&myMutex, K_FOREVER);
__ASSERT_NO_MSG(rc == 0);

This is a blocking call which will sleep the current thread until the mutex is unlocked and available to be locked by this thread. K_FOREVER states to wait indefinitely for the mutex to be unlocked. Generally, I would not recommend using K_FOREVER, but specifying a timeout with a sensible time limit such that if it expires, something has gone really wrong. Then log an error! This is useful for debugging purposes, as forgetting to unlock mutexes is a common mistake (especially in functions which have many exit points). Without a timeout, your application will hang if you forget to unlock the mutex, and you'll get no helpful debug info. With a timeout, you can get a helpful log message stating which mutex failed to lock and where.

int rc = k_mutex_lock(&myMutex, K_MSEC(1000));
if (rc != 0)
LOG_ERR("Failed to lock myMutex.");
// ... do appropriate action here
// If not being able to lock should be considered a fatal error in your firmware, you
// could replace this if() with an assert: __ASSERT_NO_MSG(rc == 0)

Sometimes you will want to use the timeout to do something useful after a period of time, or give up and try something else. That is entirely application specific!

You then unlock a mutex with:


You must not unlock a mutex that has not been locked, and you must remember to unlock just as many times as you locked before any other thread can lock the mutex again. Be careful with unlocking in functions that have many exit points. A useful feature in C++ (and many other languages) is the ability to turn a mutex into an object, and it's destructor is guaranteed to be called on function exit no matter how it exits (you don't need to unlock yourself!). Unfortunately C does not have this functionality.

Zephyr mutexes support reentrant locking3. This means that a thread is allowed to lock a mutex more than once. This is a useful feature that allows a thread to lock the mutex more than once. The same thread must unlock the mutex just as many times before it can be used by another thread. A common use of this pattern is if you have a shared resource that can be accessed via an API. You can lock the mutex inside the API functions themselves so that they are individually guaranteed to be exclusive, but also allow the caller access to the mutex so they can lock it if they want to chain together multiple API calls in one single "atomic" operation.


Mutex objects should not be used from within ISRs3. Neither locking or unlocking from ISRs is supported. If you want to perform cross-thread communication from an ISR, use a semaphore instead.

Zephyr mutexes also support priority inheritance. The Zephyr kernel will elevate the priority of a thread that has currently locked the mutex if a thread of a higher priority begins waiting on the mutex. This works well if there is only ever one mutex locked at once by a thread. If multiple mutexes are locked, then less-than-ideal behaviour occurs if the mutex is not unlocked in the reverse order to which the owning thread's priority was originally raised. It is recommended that only one mutex is locked at a time when multiple mutexes are used between multiple threads of different priorities.


Zephyr provides support for standard C library assert() function as well as providing more powerful assert macros if you wish you use them.

ASSERT_NO_MSG() can be used if you don't want to have to provide a message. I end up using this a lot as adding a message to every assert becomes tiresome (a typical project ends up with hundreds of assert calls, checking things like passed-in pointers are non-null, integers are within range, e.t.c).



Zephyr provides a software/hardware based watchdog API you can use to monitor your threads and perform actions (usually a system reset) in the case that your threads become unresponsive and do not feed the watchdog in time. The API provides the ability to monitor multiple application threads at once via it's software watchdog. This software watchdog can in turn be monitored by a hardware watchdog. A hardware watchdog (i.e. a physical peripheral provided by the MCU) can be trusted to reliably reset the device, even if the software watchdog locks up (which can be the case with certain errors).


Why not just use the hardware watchdog? The useful thing about the software watchdog thread is that is provides the ability to independently monitor multiple threads, with different timeouts associated with each thread. It provides a more granularly and control about what you watch and how often you expect each thread to "check in".

To use the watchdog, first add the following to your prf.conf:


You'll then need to include the header file that provides the API:

#include <zephyr/task_wdt/task_wdt.h>

You then need to enable the watchdog task with int task_wdt_init (const struct device * hw_wdt):

int wdRc = task_wdt_init(NULL);
__ASSERT_NO_MSG(wdRc == 0);

Passing in NULL for hw_wdt says you don't want to connect the software watchdog task up with a hardware watchdog. In most real life applications you do want to provide a hardware watchdog, as the probability of the software watchdog failing is too high to rely solely on it (whereas a hardware watchdog is very reliable).

You can "install" a new channel with the software watchdog by using int task_wdt_add(uint32_t reload_period, task_wdt_callback_t callback, void *user_data). You then need to regularly feed the channel with int task_wdt_feed (int channel_id), as shown in the below code snippet:

void my_thread_fn() {
// When your thread starts, install a new watchdog timeout for this thread
// Passing NULL as second param means system reset handler will be called
// if watchdog timer expires
int wdtChannelId = task_wdt_add(5000, NULL, NULL);
__ASSERT_NO_MSG(wdtChannelId == 0);

while(1) {
int rc = task_wdt_feed(wdtChannelId); // Regularly feed the watchdog to prevent system reset
__ASSERT_NO_MSG(rc == 0);
// Sleep for a second before feeding again

Make sure you have enough available channels to be able to install timeouts. You can change this in prf.conf with CONFIG_TASK_WDT_CHANNELS. By default is set to 5, but can be changed to anything in the range [2, 100]4.

The following code is a complete example showing watchdog task functionality that can be built for the native_sim board (Linux). It sets the watchdog up with a 3s timeout. It feeds the watchdog once a second for 5 seconds, and then pretends there is a bug which locks up the code. The software watchdog successfully resets the device 3 seconds later.

It does not use multiple threads (as a real world application typically would), but just shows watchdog working in the main thread.

#include <stdio.h>

#include <zephyr/kernel.h>
#include <zephyr/task_wdt/task_wdt.h>

int main(void) {
// Initialize, passing NULL so we are not using a hardware watchdog
// (for real applications you normally want a hardware watchdog backing the software one!)
int wdRc = task_wdt_init(NULL);
__ASSERT_NO_MSG(wdRc == 0);

// Install a new WDT channel
int wdtChannelId = task_wdt_add(3000, NULL, NULL);
__ASSERT_NO_MSG(wdtChannelId == 0);

uint32_t cycleCount = 0;

while(1) {
printf("Feeding watchdog.\n");
int rc = task_wdt_feed(wdtChannelId); // Regularly feed the watchdog to prevent system reset
__ASSERT_NO_MSG(rc == 0);

if (cycleCount == 5) {
printf("Oh oh, bug has got this thread stuck!\n");
while(1) {
// Do nothing, just hang here

cycleCount += 1;
// Sleep for a second before cycling around again
return 0;
Running the watchdog example code and seeing it timeout.


Zephyr provides traditional counting semaphores.

A semaphore can be created with:

struct k_sem mySem;
k_sem_init(&mySem, 0, 1);

// OR
K_SEM_DEFINE(mySem, 0, 1);

Semaphores can be used with the Polling API to wait to multiple conditions simultaneously.

Polling API

Zephyr's Polling API lets you wait (block) on multiple events at the same time, e.g. block waiting one of two or more semaphore to become available, or until either a semaphore is available or FIFO has data in it.

You first have to enable the polling API by adding the following into your prf.conf:


then the API becomes available through the standard #include <zephyr/kernel.h>. Before you can call k_poll() to wait for events, you have to declare an array of k_poll_event and initialize them.

struct k_poll_event myEvents[2];


You can then wait until one or more of these events occurs by calling k_poll():

k_poll(myEvents, 2, K_FOREVER); // This will block until 1 or more events occur

Semaphores are not taken as part of the call to k_poll(). After k_poll() returns, you have to check which event (e.g. semaphore) was available and then take it. But you are not necessarily guaranteed that this semaphore is still available, due to the possibility of another thread pre-empting this one and taking it first. You either have to handle the case where the semaphore can't be taken, or design your application so it's guaranteed to be available (one way is to make sure the thread that calls k_poll() is the only thread which takes the semaphore).

Full Working Example

Here is a full working example:

#include <stdio.h>

#include <zephyr/kernel.h>

struct k_sem mySem1;
struct k_sem mySem2;

void MyThreadFn(void * v1, void * v2, void * v3) {
printf("THREAD: Thread started.\n");

struct k_poll_event events[2];

while(1) {
printk("THREAD: Waiting on k_poll()...\n");
int rc = k_poll(events, 2, K_FOREVER);
if (rc != 0)
// Handle error. If a finite time was provided to k_poll()
// we would also need to check for -EAGAIN returned, which indicates
// a timeout

if (events[0].state == K_POLL_STATE_SEM_AVAILABLE) {
printk("THREAD: Semaphore 1 available.\n");
k_sem_take(events[0].sem, K_NO_WAIT); // Careful! If this thread was preempted, we might not actually be able to take the semaphore
} else if (events[1].state == K_POLL_STATE_SEM_AVAILABLE) {
printk("THREAD: Semaphore 2 available.\n");
k_sem_take(events[1].sem, K_NO_WAIT);

// Because we are going to check again, we need to clear the state!
events[0].state = K_POLL_STATE_NOT_READY;
events[1].state = K_POLL_STATE_NOT_READY;

struct k_thread my_thread_data;

int main(void) {
// printf("Creating timer to expire every 1s...\n");

// Initialize semaphores
k_sem_init(&mySem1, 0, 1);
k_sem_init(&mySem2, 0, 1);

// Create thread
k_thread_create(&my_thread_data, myStack,
5, 0, K_NO_WAIT);

// Give semaphore, this should wake up the thread
printf("MAIN: Giving semaphore 1...\n");

// Give semaphore, this should wake up the thread
printf("MAIN: Giving semaphore 2...\n");

return 0;

and the output is:

The output from the full working Polling API example above, showing how a thread blocks on two separate semaphores.


Zephyr has very powerful logging features (compared to what you typically expect for embedded devices) provided via it's logging API.

First you have to enable logging in your prj.conf with:


You can also set a default compiled log level with:

# The default compile time log level. Recommended to leave this as verbose as
# possible given memory constraints and then set the runtime log level as needed.

If you want to add logs to a .c file, first include #include <zephyr/logging/log.h>. Then you have to register the source file as a "module":

// Uses the compiled log level set in prj.conf with CONFIG_LOG_DEFAULT_LEVEL

// OR:

// Optionally override the compiled log level with a custom level as a second
// parameter to the macro.

Now you can use log statements in your code:

void MyFunction() {
LOG_DBG("Here is a debug level log!"); // Prints: [00:10:04.267,242] <dbg> MyModule: MyFunction: Here is a debug level log!
LOG_INF("Here is a info level log!"); // Prints: [00:00:01.024,291] <inf> MyModule: Here is a info level log!
LOG_WRN("Here is a warning level log!"); // Prints: [00:00:01.024,291] <wrn> MyModule: Here is a warning level log!
LOG_ERR("Here is a error level log!"); // Prints: [00:00:01.024,291] <err> MyModule: Here is a error level log!

Note that the debug level log prints additional information -- it also prints the function name (in the above example this is MyFunction) that the log message was printed from. For outputs that support ANSI escape codes, the warning log is printed in yellow (except for the timestamp), and similarly the error log is printed in red.

Compile Time vs. Runtime Log Levels

It's important to make the distinction between compile-time log levels and runtime levels. Providing CONFIG_LOG_DEFAULT_LEVEL or a second parameter to LOG_MODULE_REGISTER sets a compile-time log level. All log levels higher than this (both in number and verbosity) are not included in the compiled firmware binary, meaning you cannot change the level to high levels at runtime. Runtime log levels are set via log_filter_set() or with the shell command log enable <log_level> (e.g. log enable dbg). Runtime adjustable log levels also depend on CONFIG_LOG_RUNTIME_FILTERING=y, which is set automatically if the shell is enabled. My recommendation is to leave the compile time log level to LOG_LEVEL_DBG if you have enough flash to allow that, and then set the log level at runtime. This will give you the ability to dynamically change the levels as needed without having to re-compile firmware. It would be a pain to have to recompile and re-flash firmware on a buggy device just to get the "debug" logs you need to diagnose the problem. And you may not want to re-flash as you have just caught an intermittent bug that is hard to reproduce!

The code below shows how you can change the logging levels at runtime:

#include <zephyr/logging/log.h>
#include <zephyr/logging/log_ctrl.h>

int main() {
// ...

// Change all log levels at runtime
// NOTE: If you do this for a particular event, you may want to save all
// the previous levels and restore them after the event is finished
uint32_t logLevel = LOG_LEVEL_INF;
uint32_t numLogSources = log_src_cnt_get(0);
for (uint32_t sourceId = 0; sourceId < numLogSources; sourceId++)
char * sourceName = (char *)log_source_name_get(0, sourceId);
__ASSERT_NO_MSG(sourceName); // Should not be null
log_filter_set(NULL, 0, sourceId, level);

// ...

Remember to set CONFIG_LOG_RUNTIME_FILTERING=y in your prj.conf if you don't have the shell enabled and you want runtime log level adjustment.

X Messages Dropped Errors

Zephyr implements a character based circular buffer for storing messages to be processed (remember -- Zephyr logging is typically done asynchronously). If other threads create logs too quickly for the log thread to process, at some point Zephyr will drop logs. You will typically see a log error generated when this happens stating --- x messages dropped --- (as shown below).

A screenshot of the "x messages dropped" error that can occur with Zephyr logging.

If you have extra RAM space, one way to reduce the probability of this error is to bump up the circular buffer Zephyr uses to write messages into in your prj.conf:

# Increased from 1024 to reduce probability of --- x messages dropped --- errors 

CONFIG_LOG_BUFFER_SIZE sets the number of bytes assigned to the circular packet buffer5.

Deferred vs. Immediate Logging

Zephyr supports two different modes for logging, deferred and immediate. In deferred mode, whenever your code calls LOG_DBG(...) or similar, the passed in format string and variables get saved for for further processing in a separate logging thread (further processing entails inserting the arguments into the format string, and sending it to backends such as UART). In immediate mode, when your code calls LOG_DBG(...), the log is processed and emitted to backends in the calling thread.

Deferred logging is a great choice when you don't want to slow down your threads emitting log messages. The biggest downside is that log messages are not synchronous with what your microcontroller is actually doing in the real world at any point of time. Deferred logging can mask errors such as hard faults -- when a serious fault occurs the processor will restart but you won't see the last 1 second or so of log messages, making it hard to track down the problem.

This is what immediate mode is great for -- debugging crashes and other time-sensitive issues when you need the logs to print at the same time the LOG_DBG() calls are being made. If a crash occurs, you can look at the last few log messages and get a good idea were the problem might be in your source code.

C++ Compatibility

In general logging will work just fine in C++. However, there is an issue if trying to log messages from a class which uses templates. The function bodies for templated functions are usually contained with the header file, because the compiler needs to know how to create these. This is a problem for LOG_MODULE_REGISTER().

Peripheral APIs

Not only does Zephyr provide you with an RTOS, but it also gives you a collection of well defined APIs for interacting with microcontroller peripherals.

The latest documentation for peripheral (UART, SPI, PWM, PIN, e.t.c.) APIs can be found at

Hardware Info

The Zephyr Hardware Info API can be used to access the device ID and reset cause of a microcontroller. The reset cause comes from a list of standardised reasons that apply to most microcontrollers such as reset pin, a software reset, a watchdog timer reset, brownout reset, debug event, e.t.c (not all reset causes may be applicable to a particular microcontroller).

hwinfo_get_reset_cause() sets a uint32_t with each bit representing a different reason for reset (a bit field). More than 1 bit can be set. Below is a C function which decodes the reset cause bit field returned from hwinfo_get_reset_cause() and creates a human-readable string of the reset reason(s) (great for logging).

#include <stdint.h>

#include <zephyr/drivers/hwinfo.h>
#include <zephyr/logging/log.h>

typedef struct {
uint32_t flag;
char name[20]; // Make sure this is as large as the largest name
} resetCauseFlag_t;

* @brief This function logs the reset cause in a human-readable manner, obtained via hwinfo_get_reset_cause().
void LogResetCause()
uint32_t resetCause;
int resetCauseRc = hwinfo_get_reset_cause(&resetCause);
__ASSERT_NO_MSG(resetCauseRc == 0);

resetCauseFlag_t resetCauseFlags[] = {

const uint32_t buffSize = 100;
char resetReasonBuffer[buffSize];
resetReasonBuffer[0] = '\0'; // Set the initial byte to 0 so that strncat will work correctly
uint32_t currLength = 0;

for(int i = 0; i < sizeof(resetCauseFlags) / sizeof(resetCauseFlag_t); i++)
if(resetCause & resetCauseFlags[i].flag)
// Append reset cause string to message
strncat(resetReasonBuffer, resetCauseFlags[i].name, buffSize - currLength);
currLength = strlen(resetReasonBuffer);

// Add ", " between reasons
strncat(resetReasonBuffer, ", ", buffSize - currLength);
currLength = strlen(resetReasonBuffer);

LOG_INF("Reset reason as a bit field: 0x%04X. This means the following flags were set: %s", resetCause, resetReasonBuffer);

// Clear the reset cause
resetCauseRc = hwinfo_clear_reset_cause();
__ASSERT_NO_MSG(resetCauseRc == 0);


To setup a single GPIO pin your device tree, you can add the following to your .dts file:

my_gpio {
# Pin 7 in GPIO controller bank 0
gpios = <&gpio0 7 GPIO_ACTIVE_HIGH>;

You can then get access to this GPIO from your C code with:

const struct gpio_dt_spec myGpio = GPIO_DT_SPEC_GET(DT_PATH(my_gpio), gpios);

bool boolRc = gpio_is_ready_dt(&myGpio);
int intRc = gpio_pin_configure_dt(&myGpio, GPIO_OUTPUT_INACTIVE);
__ASSERT_NO_MSG(intRc == 0);

The above code gets the device tree spec based of it's path, checks that the GPIO is ready for use (I'm not sure why it wouldn't me, but good practice!) and then configures it as an output. To then set the pin high or low, you use gpio_pin_set_dt():

int rc = gpio_pin_set_dt(&myGpio, 0); // Set it low
__ASSERT_NO_MSG(rc == 0);

rc = gpio_pin_set_dt(&myGpio, 1); // Set it high
__ASSERT_NO_MSG(rc == 0);

GPIO_OUTPUT_LOW and GPIO_OUTPUT_HIGH both set the physical state of the pin, and do not care if the pin is active high or active low. GPIO_OUTPUT_INACTIVE and GPIO_OUTPUT_ACTIVE take into account whether the pin is active high or active low. In the case the pin is active high, setting the pin to a logical level of INACTIVE results in a physical LOW, and ACTIVE is HIGH. In the case it is active low, INACTIVE

If you are using a Nordic MCU with NFC pins (e.g. NFC1, NFC2) make sure to disable NFC with CONFIG_NFCT_PINS_AS_GPIOS=y in your prj.conf if you want to use these pins for GPIO (or anything else for that matter, including PWM).

To read the value of a GPIO input, use gpio_pin_get_dt():

int rc = gpio_pin_get_dt(&myGpio);
__ASSERT_NO_MSG(rc >= 0); // 0 or 1 for low/high, negative number for error

if (rc == 0) {
LOG_INF("GPIO was low.");
} else if (rc == 1) {
LOG_INF("GPIO was high.");

If you want to configure an interrupt for a GPIO pin, you can use gpio_pin_interrupt_configure_dt():

intRc = gpio_pin_interrupt_configure_dt(&myGpio, GPIO_INT_EDGE_TO_ACTIVE);
__ASSERT_NO_MSG(intRc == 0);

You then need to setup a callback:

static struct gpio_callback gpioCallbackData;

void GpioCallback(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
// GPIO interrupt callback

int main() {
gpio_init_callback(&gpioCallbackData, &GpioCallback, BIT(;
gpio_add_callback(myGpio.port, &gpioCallbackData);

See for an example of using a Zephyr GPIO to handle button presses along with debouncing using delayable work queue.


For example, looking at zephyr/dts/arm/nordic/nrf52832.dtsi and searching for "pwm" we find:

/ {
soc {
pwm0: pwm@4001c000 {
compatible = "nordic,nrf-pwm";
reg = <0x4001c000 0x1000>;
interrupts = <28 NRF_DEFAULT_IRQ_PRIORITY>;
status = "disabled";
#pwm-cells = <3>;
pwm1: pwm@40021000 {
compatible = "nordic,nrf-pwm";
reg = <0x40021000 0x1000>;
interrupts = <33 NRF_DEFAULT_IRQ_PRIORITY>;
status = "disabled";
#pwm-cells = <3>;
pwm2: pwm@40022000 {
compatible = "nordic,nrf-pwm";
reg = <0x40022000 0x1000>;
interrupts = <34 NRF_DEFAULT_IRQ_PRIORITY>;
status = "disabled";
#pwm-cells = <3>;

In your .dts file:

my-pwm-group {
compatible = "pwm-leds";
status = "okay";
my-pwm-pin-1 {
// Use pwm peripheral 0 and channel 0
pwms = <&pwm0 0 PWM_MSEC(5) PWM_POLARITY_NORMAL>;
my-pwm-pin-2 {
// Use pwm peripheral 0 and channel 1
pwms = <&pwm0 1 PWM_MSEC(5) PWM_POLARITY_NORMAL>;

This defines 3 PWM peripherals.

PWM can be set up with:

const struct pwm_dt_spec myPwmSpec = PWM_DT_SPEC_GET(DT_PATH(my_pwm_group, my_pwm_pin_1));

boolRc = pwm_is_ready_dt(&myPwmSpec);

And then enabled with:

// This will enable the PWM with a period of 1000us and a pulse width of 500us (i.e. 50% duty cycle)
int intRc = pwm_set_dt(&myPwmSpec, PWM_USEC(1000), PWM_USEC(500));
__ASSERT_NO_MSG(intRc == 0);


There is a working example in the Zephyr repo at samples/drivers/adc/.

#include <zephyr/drivers/adc.h>

Non-Volatile Storage

Zephyr provides an API for storing things in non-volatile storage (flash memory). The NVS API is provided by #include <zephyr/fs/nvs.h>.


Although different, Zephyr provides a similar service called the Retention System for storing data whilst the device is powered on (i.e. it will persist across resets, but it will NOT persist across power cycles).

Typically a partition called storage_partition is setup in the main flash for the NVS system to use. This can be defined in the board files.

An official code example of the NVS can be found at[^github-zephyr-nvs-code-example].


Nordic has contributed significantly to the Zephyr Bluetooth API ever since they adopted Zephyr as their official platform for the nRF52, nRF53 and nRF91 MCU families.

Update the LE Connection Interval

After you are connected, you can call bt_conn_le_param_update() to update the Bluetooth connection interval. This is useful if you want to save power by increasing the connection interval when you don't need to send/receive data as often.

struct bt_le_conn_param conn_param = { .interval_min = (708), .interval_max = (800), .latency = (0), .timeout = (400), };
int rc = bt_conn_le_param_update(conn, &conn_param);
__ASSERT_NO_MSG(rc == 0);

conn is a pointer to a struct bt_conn which is the connection object. It's assumed you have that handy to pass in! The connection interval is in units of 1.25ms, so the above code sets the connection interval min. to 885ms and the max. to 1000ms. By default the min. and max were set to 15ms and 30ms respectively, so this is a significant slow down and results in good power savings for small battery powered devices. The timeout is in units of 10ms, so the above code sets the timeout to 4s. You can use the helper macro BT_LE_CONN_PARAM() to create the struct bt_le_conn_param object if you want.

If you are a Bluetooth central device, these settings will take effect. If you a peripheral device, these settings are "suggestions". They are sent to the central device and it is up to the central device to accept them. The central device may reject them or choose other values.

What Does A Basic Zephyr Firmware Application Look Like?

The following example shows main.c (the only .c file) for the Blinky sample project:

* Copyright (c) 2016 Intel Corporation
* SPDX-License-Identifier: Apache-2.0

#include <zephyr.h>
#include <device.h>
#include <devicetree.h>
#include <drivers/gpio.h>

/* 1000 msec = 1 sec */
#define SLEEP_TIME_MS 1000

/* The devicetree node identifier for the "led0" alias. */
#define LED0_NODE DT_ALIAS(led0)

#define LED0 DT_GPIO_LABEL(LED0_NODE, gpios)
#define PIN DT_GPIO_PIN(LED0_NODE, gpios)
/* A build error here means your board isn't set up to blink an LED. */
#error "Unsupported board: led0 devicetree alias is not defined"
#define LED0 ""
#define PIN 0
#define FLAGS 0

void main(void)
const struct device *dev;
bool led_is_on = true;
int ret;

dev = device_get_binding(LED0);
if (dev == NULL) {

ret = gpio_pin_configure(dev, PIN, GPIO_OUTPUT_ACTIVE | FLAGS);
if (ret < 0) {

while (1) {
gpio_pin_set(dev, PIN, (int)led_is_on);
led_is_on = !led_is_on;


Zephyr supports the targets qemu_x86 and qemu_cortex_m3 for running Zephyr applications on desktop computers. This is great for development, testing and CICD purposes.

Zephyr and C++

There are a number of C++ features that Zephyr does not support which removes C++ as a 1st tier language for writing Zephyr applications. This includes:

  • No dynamic memory allocation support via new or delete. Dynamic memory allocation in the embedded land is a contentious subject, but it's nice to be able to use it if it's a suitable choice for your application.
  • No RTTI (run-time type information)
  • No support for exceptions. Again, another contentious embedded subject, but nice to have the option of using them if you want.

You can enable support for compiling C++ by adding the following into prj.conf:


You can then change main.c to main.cpp. Remember to update the path in the CMakeLists.txt file also!

# Adds everything!

The Zephyr Shell

Zephyr features a "shell" (one of it's modules) that it can provide over a serial transport such as a UART, USB or Segger RTT. The shell provides to the user things such as Linux command-line style commands, logging, auto-complete, command history and more. In It is great for implementing a debug interface to control your microcontroller from a terminal application such as NinjaTerm (shameless plug, I developed this app).

Zephyr provides an API that the firmware can use to define the commands available to the user over the shell.


You can use the addr2line executable to decode a memory address back into a source code file and line number. addr2line should be provided by the toolchain as part of the compiler's suite of executables. For example, with Nordic toolchains in Zephyr:

c:\ncs\toolchains\31f4403e35\opt\zephyr-sdk\arm-zephyr-eabi\bin\arm-zephyr-eabi-addr2line.exe -e .\app\build\zephyr\zephyr.elf -a 0x0

Would give output something like this:


Reducing Flash and RAM Usage in Zephyr

Zephyr-based applications can get large, in part due to the powerful features it provides out-of-the-box. Just things like using logging throughout your code can increase flash usage significantly, due to every call saving the log message (before substitution takes place at runtime) as a string literal in ROM. This can easily use up many "kB" of space. If you weren't using float printing before hand, this call also bring in float formatting functionality. Similarly, all ASSERT() style macros save the file name and line number of the assert as a string literal in ROM. However these are quite useful, even in production, so think carefully before disabling them.

CONFIG_SIZE_OPTIMIZATIONS=y can be set in prj.conf to reduce the flash size. One thing this does is set the compiler flag -Os which tells the compiler to optimize for size, not speed or debug ability.

On one project I was working on, just setting CONFIG_SIZE_OPTIMIZATIONS=y in prf.conf resulted in a flash size reduction from 421kB to 330kB!

You can execute the west command with -t ram_report to make Zephyr generate and print a table of RAM usage to the terminal.

A screenshot of the output produce by the west build -t ram_report command.

puncover can be used to visualize the memory usage via a web GUI. Install puncover in the projects Python virtual environment:

# Activate python environment if needed, then
pip install puncover

Then perform a clean build of west (it will detect puncover and add a target for it):

west build -c

Then run puncover by invoking west and giving it a specific target:

west build -t puncover

If you get the error ninja: error: unknown target 'puncover' (as shown below) when trying to run puncover, it might be because you have not done a clean rebuild. You must do a clean rebuild after installing puncover. This is needed because CMake looks for the puncover executable during the build process. If it can't find it, no target is made for it.

A screenshot of the build error you can get if you don't clean build after installing puncover.

You can also look at reducing the stack size of some of the default stacks you will likely have in your project:

# Stack size of the main thread.
# Defaults to 2048

# Stack used for Zephyr initialization and interrupts
# If you have a stack overflow before your code gets to main() you will
# likely need to make this bigger
# Defaults to 2048

# Stack size of the shell thread (if you are using a shell)
# Defaults to 2048


There is a template test project located at zephyr/samples/subsys/testsuite/integration.

Twister is Zephyrs test runner tool. It is a command-line tool that collects tests, builds the test application and runs it. The best way to access it is via a subcommand of west, i.e. west twister.

Twister dumps it's build output into directories called twister-out, twister-out1, twister-out2, e.t.c.

Adding Native Unit Tests To Your Application

Let's assume you have a workspace application at ~/zephyr-project/app/ and want to add unit tests to it using Zephyr and ztest. We'll run the tests on the native_sim board so that we can

Let's create a directory called tests under app, and then copy all of the files from ~/zephyr-project/zephyr/samples/subsys/testsuite/integration into this new tests directory.

tests will be an entire Zephyr application in it's own right. The directory structure for your project should look like this:

├─── zephyr-project/
│ ├── .west/
│ ├── app/
│ │ ├── CMakeLists.txt
│ │ ├── prj.conf
│ │ ├── src/
│ │ | ├── main.c
| | | ├── RgbLed.c
| | | └── RgbLed.h
| | └── tests/
| | ├── src/
| | | ├── main.c
| | | ├── SomeUnitTests.c
| | | └── SomeOtherUnitTests.c
| | ├── CMakeLists.txt
| | ├── prj.conf
| | └── testcase.yaml
│ ├── zephyr/
│ ├── bootloader/
│ ├── modules/
│ └── ...

When including sources from app/src/ into your test application, make sure NOT to include the apps main.c (or wherever your int main() function is defined). The test application defines it's own main() which runs the unit test functions (which makes sense, you don't want to RUN your app normally, you want a new entry point which runs the unit test functions). If you include the apps main() you'll get a multiple definition of main compile error.

Assuming you are currently in the root of the west workspace, you can run your tests with the following command. You do not need to pass in the tests directory, west twister can work that out so you just need to pass in the directory to your app.

west twister -T app/

This should build the example tests included in the template. But what we really want to test is code in the app/src/ directory. To do this, we need to do two things:

  1. Include the source code in app/src when building the test application.
  2. Add the app/src directory as a include path when building the test application.

To do this, we can modify the file tests/CMakeLists.txt to the following:

cmake_minimum_required(VERSION 3.20.0)

# Include all source files in the main application
# except main.c, which we will filter out
FILE(GLOB appSources ../src/*.c)
list(FILTER appSources EXCLUDE REGEX ".*main\\.c$")

# Include all source files in the test application
FILE(GLOB testAppSources src/*.c)
target_sources(app PRIVATE ${testAppSources} ${appSources})

# Add the main application directory to the include path
target_include_directories(app PRIVATE "../src/")

Now in our tests .c files (tests/src/*.c), we can include header files from app/src/ and test the code.

When writing the tests, just using west twister -T app/ is not that useful because:

  1. It takes a long time to build and run the tests, and the output is hidden in a log file rather than being printed to the terminal. This slows down development when you want to write some test code and then run it to see if it works.
  2. Intellisense does not work as well as it does for the Zephyr app. This is because compile_commands.json is not created in the twister build directories, and the build directory keeps changing.

A better way is to reserve ``west twister -T app/only for running your tests once you have finished writing the tests. While writing the tests, use the standard west build instead. You can do this because thetest/` directory is a self-contained west application in it's own right!

First use (assuming you are building the tests to run on native_sim):

west build -b native_sim ./<path_to_zephyr_app>/tests/ --pristine

and then from then on you can use the faster:

west build -t run

Distributing Zephyr Libraries

See for an example.

Common Errors

File not found (on Windows)

If you get an error when running west build similar to:

CMake Error at C:/Users/Geoffrey Hunter/temp/zephyrproject/zephyr/cmake/kconfig.cmake:206 (message):
File not found: C:/Users/Geoffrey

It is due to there being one or more spaces in the path to your Zephyr project directory. This isn't a bug that is going to be fixed anytime soon, Zephyr is very clear on the matter in their documentation:

A warning on Zephyr's documentation about using spaces in paths.

I found this out the hard way and went through all the trouble of renaming my user directory to fix the issue.

No module named 'elftools'

FAILED: zephyr/include/generated/kobj-types-enum.h zephyr/include/generated/otype-to-str.h zephyr/include/generated/otype-to-size.h
cmd.exe /C "cd /D C:\Users\gbmhunter\temp\myproject\zephyr\build\zephyr && C:\Users\gbmhunter\AppData\Local\Programs\Python\Python38-32\python.exe C:/Users/gbmhunter/temp/myproject/zephyr/scripts/ --kobj-types-output C:/Users/gbmhunter/temp/myproject/zephyr/build/zephyr/include/generated/kobj-types-enum.h --kobj-otype-output C:/Users/gbmhunter/temp/myproject/zephyr/build/zephyr/include/generated/otype-to-str.h --kobj-size-output C:/Users/gbmhunter/temp/myproject/zephyr/build/zephyr/include/generated/otype-to-size.h --include C:/Users/gbmhunter/temp/myproject/zephyr/build/zephyr/misc/generated/struct_tags.json "
Traceback (most recent call last):
File "C:/Users/gbmhunter/temp/myproject/zephyr/scripts/", line 62, in <module>
import elftools
ModuleNotFoundError: No module named 'elftools'

You typically get the error No module named 'elftools' if you haven't installed the Python modules that Zephyr requires to build. To install the required modules:

> pip3 install -r scripts/requirements.txt

"__device_dts_ord_DT_N_NODELABEL_xxx_ORD" undeclared

Zephyr can produce some really obscure error messages when there are errors relating to the device tree, for example:

C:/project/zephyr/include/zephyr/device.h:83:41: error: '__device_dts_ord_DT_N_NODELABEL_hs_0_ORD' 
undeclared (first use in this function)
83 | #define DEVICE_NAME_GET(dev_id) _CONCAT(__device_, dev_id)
| ^~~~~~~~~

If you are using the VS Code and the nRF Connect extension, sometimes this can be fixed by making when you setup the build configuration you set the Configuration to "Use build system default" as shown below:

Selecting "Use build system default" can sometimes fix device tree errors.

"ERROR: Build directory xxx is for application yyy, but source directory zzz was specified"

The error:

ERROR: Build directory xxx is for application yyy, but source directory zzz was specified;
please clean it, use --pristine, or use --build-dir to set another build directory

typically occurs when you try to build a second project for the first time. By default, west creates build directories outside of the application you are currently building, in a directory called build directly under the west workspace directory (e.g. zephyr-project/build/).

When you tell west to build a different project (say, you tested out a sample like samples/hello_world but now want to build your own workspace application), west will try and re-use build. Except that it notices that the remnants from the last build do not belong to the same project, and gives you this error. Because build artifacts can be reproduced by rebuilding, it is generally save to provide the --pristine option and override the contents (this would be equivalent to you deleting the build directory and re-running west). If you want to have multiple builds on-the-go at the same time (perhaps because builds can take a long time to rebuild from scratch!), you can specify a different build directory with the --build-dir option.

"By not providing "FindZephyr.cmake" in CMAKE_MODULE_PATH ..."

If you get the following warning (which then results in an error further down in the build process):

(.venv) geoff@geoffs-laptop:~/zephyr-project$ west build -b native_sim ./apps/hello-world/
-- west build: generating a build system
CMake Warning at CMakeLists.txt:3 (find_package):
By not providing "FindZephyr.cmake" in CMAKE_MODULE_PATH this project has
asked CMake to find a package configuration file provided by "Zephyr", but
CMake did not find one.

Could not find a package configuration file provided by "Zephyr" with any
of the following names:


Add the installation prefix of "Zephyr" to CMAKE_PREFIX_PATH or set
"Zephyr_DIR" to a directory containing one of the above files. If "Zephyr"
provides a separate development package or SDK, be sure it has been
Screenshot of the error "By not providing "FindZephyr.cmake" in CMAKE_MODULE_PATH ...".

It usually can be due to forgetting to export Zephyr to the CMake user package registry. Run the following command from the west workspace directory:

west zephyr-export

ModuleNotFoundError: No module named 'elftools'

The error:

Traceback (most recent call last):
File "/home/geoff/zephyr-project/zephyr/scripts/build/", line 62, in <module>
import elftools
ModuleNotFoundError: No module named 'elftools'
Screenshot of the error "No module named 'elftools'".

can occur if you have forgotten to install the additional Zephyr dependencies into your Python environment (which can happen if you delete the existing virtual environment and recreate it). This can be fixed by running the following command, assuming you have activated the Python virtual environment if relevant:

pip install -r ./zephyr/scripts/requirements.txt

Printing Floats Results in float

If tring to print a float using %f in any printf style functions (or log macros) results in the output *float*, it's likely you need to enable floating-point print support with CONFIG_FPU=y in prj.conf:

CONFIG_FPU=y # Required for printing floating point numbers


In February 2016, Wind River (the same company the makes VxWorks, one of the leading commercial RTOSes for safety critical systems) donated the Rocket OS kernel to the Linux Foundation and Zephyr was born6 7. Rocket still existed in parallel as a commercial version of Zephyr.

Nordic chose to move from their nRF5 platform to Zephyr as the officially supported development environment for their nRF52, nRF53 and nRF92 MCU/SoC families. Zephyr support for the nRF52 family was added around April 20208.

Other Resources

Check out the Zephyr Discord channel.


  1. Kconfig Language [documentation]. Retrieved 2024-10-12, from

  2. Zephyr. Docs / Latest -> Kernel -> Kernel Services -> Workqueue Threads [documentation]. Zephyr Docs. Retrieved 2024-01-10, from

  3. Zephyr (2023, Nov 7). Mutexes [documentation]. Retrieved 2024-02-14, from 2

  4. Zephyr (2024, Jan 16). Kconfig Search - CONFIG_TASK_WDT_CHANNELS [documentation]. Zephyr Docs. Retrieved 2024-01-17, from

  5. Zephyr (2024, Feb 19). Logging [documentation]. Retrieved 2024-02-19, from

  6. Wikipedia (2023, Oct 20). Zephyr (operating system). Retrieved 2024-02-21, from

  7. Scaler. Scaler Topics - How does the Zephyr Operating System Work?. Retrieved 2024-02-1, from

  8. Nordic Semiconductor (2020, Apr 2). Nordic Semiconductor now offering broad product line support for its short-range and cellular IoT devices on nRF Connect platform including a suite of development tools and open source nRF Connect SDK [blog post]. Retrieved 2024-02-21, from