ZEPHYR

Zephyr

Article by:
Date Published:
Last Modified:
WARNING
This page is in notes format, and may not be of the same quality as other pages on this site.

Overview

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.

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.

Installation

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

Windows

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:

WARNING

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:

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

    1
    2
    
    > 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):

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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    > west init myproject
    === Initializing in C:\Users\gbmhunter\myproject
    --- Cloning manifest repository from https://github.com/zephyrproject-rtos/zephyr, 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:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    > 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\shutil.py", 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:

    1
    
    > 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”:

    1
    2
    3
    4
    5
    6
    7
    8
    
    > west zephyr-export
    Zephyr (C:/Users/gbmhunter/zephyrproject/zephyr/share/zephyr-package/cmake)
    has been added to the user package registry in:
    HKEY_CURRENT_USER\Software\Kitware\CMake\Packages\Zephyr
    
    ZephyrUnittest (C:/Users/gbmhunter/zephyrproject/zephyr/share/zephyrunittest-package/cmake)
    has been added to the user package registry in:
    HKEY_CURRENT_USER\Software\Kitware\CMake\Packages\ZephyrUnittest
    
  7. Download and install the GNU Arm Embedded Toolchain from https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads. 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:

    1
    2
    
    set ZEPHYR_TOOLCHAIN_VARIANT=gnuarmemb
    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:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    > 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 https://github.com/xpack-dev-tools/openocd-xpack/releases. I extracted the .zip file and then copied the files to C:\Program Files\OpenOCD\bin.

  12. Flash the application:

    1
    
    west flash
    

    NOTE: I got an error when running this:

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

Ubuntu/WSL

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:

    1
    2
    3
    4
    
    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:

    1
    2
    3
    4
    
    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):

    1
    
    pip install west
    
  4. Initialize the west workspace:

    1
    
    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:

    1
    
    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:

    1
    
    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 https://github.com/zephyrproject-rtos/sdk-ng/tags (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.

    1
    2
    
    cd ~
    wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.4/zephyr-sdk-0.16.4_linux-x86_64.tar.xz
    

    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:

    1
    
    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:

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

    1
    2
    
    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:

    1
    2
    
    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):

    1
    
    west flash
    
NOTE

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.

1
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:

1
2
3
4
5
6
7
8
9
~/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.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    zephyr-project/
    ├─── .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:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    <home>/
    ├─── 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:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    <home>/
    ├─── 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:

1
2
cd ~/zephyr-project
mkdir app

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

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.20.0)

find_package(Zephyr)
project(my_zephyr_app)

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>
#include <zephyr/kernel.h>

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<home>/
├─── 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:

1
2
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:

1
2
3
4
5
6
7
8
(.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!
...

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 https://docs.zephyrproject.org/latest/boards/index.html#boards 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.

NOTE

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.:

1
west build -b native_sim samples/hello_world

You can then run the built application with:

1
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.

Terminology

/dts-v1/;

/ {
    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

/dts-v1/;

/ {
    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

Use DT_NODELABEL(), e.g. DT_NODELABEL(uart0).

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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/*
 * Copyright (c) 2018 qianfan Zhao
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/dts-v1/;
#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

Timers

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:

1
#include <zephyr/kernel.h>

You create a timer object and initialize it with:

1
2
3
4
5
6
7
8
9
struct k_timer myTimer;

void MyHandlerFn(struct k_timer * timer)
{
    LOG_DBG("Hello!");
}

// 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.

WARNING

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).

1
2
// 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#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) {
        k_msleep(1000);
    }
    return 0;
}

Running the timer example code above and observing the expiry function run every second.

Running the timer example code above and observing the expiry function run every second.

You can read the official Zephyr documentation for Timers here.

Workqueues

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).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#define STACK_SIZE 512 // Stack size of work queue
#define PRIORITY 5 // Priority of work queue

K_THREAD_STACK_DEFINE(myStackArea, STACK_SIZE);

struct k_work_q myWorkQueue;

k_work_queue_init(&myWorkQueue);

k_work_queue_start(&myWorkQueue,
                   myStackArea,
                   K_THREAD_STACK_SIZEOF(myStackArea),
                   PRIORITY,
                   NULL);

Mutexes

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:

1
2
3
4
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:

1
2
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.

1
2
3
4
5
6
7
8
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:

1
k_mutex_unlock(&myMutex);
WARNING

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.

WARNING

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.

Asserts

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).

1
ASSERT_NO_MSG();

Watchdog

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).

NOTE

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:

1
CONFIG_TASK_WDT=y

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

1
#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):

1
2
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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
        k_sleep(K_MSEC(1000));
    }
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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
                k_msleep(1000);
            }
        }

        cycleCount += 1;
        // Sleep for a second before cycling around again
        k_msleep(1000);
    }
    return 0;
}

Running the watchdog example code and seeing it timeout.

Running the watchdog example code and seeing it timeout.

Semaphores

Zephyr provides traditional counting semaphores.

A semaphore can be created with:

1
2
3
4
5
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:

1
CONFIG_POLL=y

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct k_poll_event myEvents[2];
k_poll_event_init(&myEvents[0],
                  K_POLL_TYPE_SEM_AVAILABLE,
                  K_POLL_MODE_NOTIFY_ONLY,
                  &mySem);

k_poll_event_init(&myEvents[1],
                  K_POLL_TYPE_FIFO_DATA_AVAILABLE,
                  K_POLL_MODE_NOTIFY_ONLY,
                  &myFifo);

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

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

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#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];
    k_poll_event_init(&events[0],
                      K_POLL_TYPE_SEM_AVAILABLE,
                      K_POLL_MODE_NOTIFY_ONLY,
                      &mySem1);
    k_poll_event_init(&events[1],
                      K_POLL_TYPE_SEM_AVAILABLE,
                      K_POLL_MODE_NOTIFY_ONLY,
                      &mySem2);
    
    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;
    }
}

K_THREAD_STACK_DEFINE(myStack, 500);
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,
                    K_THREAD_STACK_SIZEOF(myStack),
                    MyThreadFn,
                    NULL, NULL, NULL,
                    5, 0, K_NO_WAIT);

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

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

    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.

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

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 https://docs.zephyrproject.org/latest/reference/peripherals/index.html.

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).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#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[] = {
        { RESET_PIN, "RESET_PIN" },
        { RESET_SOFTWARE, "RESET_SOFTWARE" },
        { RESET_BROWNOUT, "RESET_BROWNOUT" },
        { RESET_POR, "RESET_POR" },
        { RESET_WATCHDOG, "RESET_WATCHDOG" },
        { RESET_DEBUG, "RESET_DEBUG" },
        { RESET_SECURITY, "RESET_SECURITY" },
        { RESET_LOW_POWER_WAKE, "RESET_LOW_POWER_WAKE" },
        { RESET_CPU_LOCKUP, "RESET_CPU_LOCKUP" },
        { RESET_PARITY, "RESET_PARITY" },
        { RESET_PLL, "RESET_PLL" },
        { RESET_CLOCK, "RESET_CLOCK" },
        { RESET_HARDWARE, "RESET_HARDWARE" },
        { RESET_USER, "RESET_USER" },
        { RESET_TEMPERATURE, "RESET_TEMPERATURE" },
    };

    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);
}

GPIO

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

1
2
3
4
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:

1
2
3
4
5
6
const struct gpio_dt_spec myGpio = GPIO_DT_SPEC_GET(DT_PATH(my_gpio), gpios);

bool boolRc = gpio_is_ready_dt(&myGpio);
__ASSERT_NO_MSG(boolRc);
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():

1
2
3
4
5
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).

PWM

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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:

1
2
3
4
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);
__ASSERT_NO_MSG(boolRc);

And then enabled with:

1
2
3
// 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);

ADCs

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

1
#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>.

NOTE

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 https://github.com/zephyrproject-rtos/zephyr/blob/main/samples/subsys/nvs/src/main.c[^github-zephyr-nvs-code-example].

What Does A Basic Zephyr Firmware Application Look Like?

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
 * 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)

#if DT_NODE_HAS_STATUS(LED0_NODE, okay)
#define LED0	DT_GPIO_LABEL(LED0_NODE, gpios)
#define PIN	DT_GPIO_PIN(LED0_NODE, gpios)
#define FLAGS	DT_GPIO_FLAGS(LED0_NODE, gpios)
#else
/* 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
#endif

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

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

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

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

Logging

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:

1
CONFIG_LOG=y

You can also set a default compiled log level with:

1
2
3
4
5
6
7
8
# 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.
# 0 = LOG_LEVEL_OFF
# 1 = LOG_LEVEL_ERR
# 2 = LOG_LEVEL_WRN
# 3 = LOG_LEVEL_INFO
# 4 = LOG_LEVEL_DBG
CONFIG_LOG_DEFAULT_LEVEL=4

If you want to add logs to a .c file, first you have to register the source file as a “module”:

1
2
3
4
5
6
7
8
// Uses the compiled log level set in prj.conf with CONFIG_LOG_DEFAULT_LEVEL
LOG_MODULE_REGISTER(MyModule);

// OR:

// Optionally override the compiled log level with a custom level as a second
// parameter to the macro.
LOG_MODULE_REGISTER(MyModule, LOG_LEVEL_INF);

Now you can use log statements in your code:

1
2
3
4
5
6
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). 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#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);
  }

  // ...
}

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 in ERROR: fig-zephyr-shell-x-messages-dropped-screenshot REF NOT FOUND).

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

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:

1
2
# Increased from 1024 to reduce probability of --- x messages dropped --- errors 
CONFIG_LOG_BUFFER_SIZE=2048

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

Emulation

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.

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.

Debugging

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:

1
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:

0x00000000
C:/my-project/zephyr/subsys/debug/thread_analyzer.c:75

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.

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:

1
2
# 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):

1
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 in ERROR: fig-puncover-build-error REF NOT FOUND) 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.

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Stack size of the main thread.
# Defaults to 2048
CONFIG_MAIN_STACK_SIZE=1024

# 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
CONFIG_ISR_STACK_SIZE=1024

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

Tests

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<home>/
├─── 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/
│     └── ...
WARNING

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.

1
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(integration)

# 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.

Common Errors

File not found (on Windows)

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

1
2
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:

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'

1
2
3
4
5
6
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/gen_kobject_list.py --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/gen_kobject_list.py", 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:

1
> 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:

1
2
3
4
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 in ERROR: selecting-use-build-system-default-in-nrf-connect REF NOT FOUND.

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

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:

1
2
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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(.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:

    ZephyrConfig.cmake
    zephyr-config.cmake

  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
  installed.

Screenshot of the error "By not providing "FindZephyr.cmake" in CMAKE_MODULE_PATH ...".

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:

1
west zephyr-export

ModuleNotFoundError: No module named ’elftools'

The error:

1
2
3
4
Traceback (most recent call last):
  File "/home/geoff/zephyr-project/zephyr/scripts/build/gen_kobject_list.py", line 62, in <module>
    import elftools
ModuleNotFoundError: No module named 'elftools'

Screenshot of the error "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:

1
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:

1
CONFIG_FPU=y # Required for printing floating point numbers

History

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.

References


  1. Kernel.org. Kconfig Language [documentation]. Retrieved 2024-10-12, from https://www.kernel.org/doc/html/next/kbuild/kconfig-language.html↩︎

  2. Zephyr. Docs / Latest -> Kernel -> Kernel Services -> Workqueue Threads [documentation]. Zephyr Docs. Retrieved 2024-01-10, from https://docs.zephyrproject.org/latest/kernel/services/threads/workqueue.html↩︎

  3. Zephyr (2023, Nov 7). Mutexes [documentation]. Retrieved 2024-02-14, from https://docs.zephyrproject.org/latest/kernel/services/synchronization/mutexes.html↩︎ ↩︎

  4. Zephyr (2024, Jan 16). Kconfig Search - CONFIG_TASK_WDT_CHANNELS [documentation]. Zephyr Docs. Retrieved 2024-01-17, from https://docs.zephyrproject.org/latest/kconfig.html#CONFIG_TASK_WDT_CHANNELS↩︎

  5. Zephyr (2024, Feb 19). Logging [documentation]. Retrieved 2024-02-19, from https://docs.zephyrproject.org/latest/services/logging/index.html↩︎

  6. Wikipedia (2023, Oct 20). Zephyr (operating system). Retrieved 2024-02-21, from https://en.wikipedia.org/wiki/Zephyr_(operating_system)↩︎

  7. Scaler. Scaler Topics - How does the Zephyr Operating System Work?. Retrieved 2024-02-1, from https://www.scaler.com/topics/zephyr-operating-system/↩︎

  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 https://www.nordicsemi.com/Nordic-news/2020/04/nordic-now-offering-support-for-its-shortrange-and-cellular-iot-devices-on-nrf-connect-platform↩︎


Authors

Geoffrey Hunter

Dude making stuff.

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License .

Related Content:

Tags

comments powered by Disqus