Basics of Design CC Blog Research & Design Hub

Zephyr RTOS

Written by Pedro Bertoleti

A Small, Real-Time Operating System for Embedded Devices

Zephyr RTOS, the product of the of the Zephyr Project collaborative effort, is a robust, scalable, and versatile real-time operating system (OS) for embedded systems that require high-quality software. In this article, Pedro explains what it is, how it’s organized, its main subsystems, and how to use Device Tree Overlays. He presents as an example project a pulse counter with LoRaWAN connectivity.


  • What is Zephyr RTOS?
  • What are the primary advantages of Zephyr RTOS?
  • How do I use device tree overlays?
  • Zephyr RTOS
  • The Zephyr Project | www.zephyrproject.org

Embedded systems are now integral parts of our lives. It doesn’t matter what we’re doing or where we’re going; everyone is surrounded by embedded systems solutions. Smart watches, car subsystems, household appliances, and urban smart lighting systems are just a tiny part of where embedded systems are applied. Despite the huge variety of embedded systems solutions available, all of them have something in common—they use embedded software.

Embedded software is like the brain and soul of embedded systems, governing how they work, and making them effective and efficient. To achieve these goals, embedded software must be designed with excellence of quality as a requirement, since software bugs can ruin an embedded system solution. One way to ensure the highest quality in high-performance embedded software running on resource-restricted MCUs and SoCs is to use a reliable real-time operating system (RTOS), which safely takes care of most of the critical tasks (memory management, I/O access management, CPU usage, and so on), thereby allowing developers to write their applications to run on safe environments.

Nowadays, developers have several RTOS options from which to choose, including free and open-source RTOS. Each has its own highlights. One of the best options for RTOS today is the Zephyr Project (also called Zephyr RTOS), a Linux Foundation hosted Collaboration Project. It inherits a lot of features of Linux embedded systems, making it familiar to those who are used to developing solutions for Linux-based systems.

This article is about Zephyr RTOS. I explain its key points and major features, and show how to prepare a development environment for those who want to get deeper into Zephyr RTOS and develop their own solutions with it. I also present a practical application using it—a pulse counter project with LoRaWAN connectivity.

ZEPHYR PROJECT: OVERVIEW

The Zephyr Project, created in 2016, is a Collaboration Project, hosted by the Linux Foundation. It is an open source collaborative effort. Like Kernel Linux, it unites corporate and independent developers from all over the world, helping to build Zephyr as a small, scalable, RTOS. Zephyr has been ported to many microcontroller (MCU) and System-on-Chip (SoC) families, making it available for use in a huge variety of embedded systems solutions. Source code for Zephyr RTOS can be obtained at its official GitHub repository [1].

Zephyr RTOS is a suitable choice for solutions that consist of connected sensors, wearables, and wireless gateways. It can therefore be classified as a state-of-the-art RTOS for developing end devices (and some gateways, too) in Internet of Things (IoT) solutions. Its footprint is a great highlight of this operating system. Since it requires less than 8KB flash memory, and less than 5KB RAM as minimal requirements for MCUs or SoC (system-on-a-chip) resources, it is suitable even for less powerful MCUs and SoCs.

The software quality of solutions that use Zephyr RTOS tends to be very high, due to its modularity—inherited from Linux based systems features, such as device tree support and IPC frameworks.

Zephyr RTOS is licensed under Apache 2.0, a permissive software license that allows it to be used in proprietary solutions. The key characteristics of Zephyr’s architecture are summarized below. Full specifications are available on the Zephyr Project’s official website [2].

  • Highly configurable and modular
  • Supports cooperative and preemptive threading
  • Memory and resources are typically statically allocated, benefitting long-running systems
  • Uses an integrated device driver interface
  • Offers memory protection via stack overflow protection, kernel object and device driver permission tracking, and thread isolation
  • Has Bluetooth Low Energy (BLE 4.2 and 5.0)
  • Is 802.15.4 OpenThread compliant
  • Has a native, fully-featured, and optimized networking stack
  • Supports huge range of subsystems largely used in embedded systems, including USB, filesystem, logging, and DFU
  • Has multi-processor support
  • Has periodic releases every 4 months, leading to long-term support releases about every 2 years, making it suitable for use in commercial products
ZEPHYR RTOS BLOCK DIAGRAM

The Zephyr RTOS block diagram is shown in Figure 1. Zephyr RTOS comprises all the layers in an embedded software application, from HAL (Hardware Abstraction Layer—a hardware-specific layer for low-level interaction with MCUs and SoCs)—to third-party libraries support. The following items mentioned in the block diagram deserve special attention:

  • SDK (Software Development Kit): Refers to toolchains for all supported target architectures (MCUs and SoCs), as well as the host tools, for testing and debugging the Zephyr RTOS.
  • West: Zephyr’s meta tool— the “Swiss army knife” command line tool for all developers who use Zephyr RTOS in their embedded software solutions. West tool is used for almost everything, from preparing development environments to compiling, to debugging, and flashing Zephyr RTOS on all supported target architectures. By far, west is the most important tool when developing solutions with Zephyr RTOS.
  • Bootloader: Zephyr RTOS can support some bootloaders, and by default, it uses MCUBoot. However, in some cases it can make use of target-specific bootloaders (in pre-compiled binary form). This is the case of ESP32 SoC bootloader.

The Zephyr RTOS buildsystem, uses CMake.

FIGURE 1
Zephyr RTOS block diagram.
FIGURE 1
Zephyr RTOS block diagram.
MAIN ZEPHYR RTOS SUBSYSTEMS

In embedded systems solutions, Zephyr RTOS counts on several subsystems to maximize source-code reuse and make the development process easier, safe, reliable (once the subsystems are extensively tested and improved) and as modular as possible. From the many subsystems available in Zephyr RTOS, those listed below are some of the most important ones.

Debug: This subsystem allows developers to properly debug Zephyr RTOS-based solutions. The resources available for debug are powerful and include thread analyzer, core dump (a special feature for debugging software crashes, making it possible to find out in which line and which task software has crashed, all variables values, and so on), and GDB (Gnu Project DeBugger) support;

DFU (Device Firmware Upgrade): DFU subsystem provides all the necessary frameworks to upgrade the image of a Zephyr RTOS-based application at run time. It’s important to mention that the DFU subsystem deals only with Zephyr RTOS image management (flash images and bootloader handling, for example). This means it doesn’t deal with the transport or management protocols required to send the image to the target device (MCUs or SoCs).

IPC: This subsystem allows Inter-Processes Communication in Zephyr RTOS, providing everything needed to communicate processes in a safe and reliable way. The RPMsg, a popular IPC framework used in Linux based systems, is also supported.

Logging: This subsystem provides tools for developers to generate log messages from applications, containing identification from modules that generated the log messages and log message timestamp (time spent since system has booted). One important feature of the Logging subsystem is that all log messages are automatically queued; therefore, logging subsystem is thread-safe.

Shell: This subsystem makes it possible to run a shell over a UART communication interface, thus allowing users and developers to write commands and get responses, just as it’s commonly done in Linux terminal sessions.

USB: These subsystems make it possible for Zephyr RTOS based systems to interact with other devices using USB modes, such as USB CDC-ACM, USB HID, USB Audio headset support.

Display: This subsystem allows Zephyr RTOS systems to communicate to displays for user interface purposes. It also offers support to LVGL, a popular, free, and open-source embedded graphics library, used largely in embedded systems solutions.

These are just a small part of all subsystems supported in Zephyr RTOS. All subsystems supported in Zephyr RTOS are given in the Zephyr RTOS repository [3]. Examples for most of the subsystems also are available [4].

DEVICE TREE OVERLAYS: ADDING PORTABILITY AND SCALABILITY TO EMBEDDED SOFTWARE PROJECTS

Of all Zephyr RTOS features, the Device Tree and Device Tree Overlay support are among the best.

Device Tree is highly used in embedded, Linux-based systems, and stands for a resource that allows developers to describe the hardware wired to a MCU/SoC It tells the operating system what pieces of hardware it must handle, and how to access them (defining addresses, compatible drivers and other hardware-specific parametrizations from these peripherals). This hardware description seen in Device Tree looks a lot like a programming language or a script, but it’s written in a readable form for us humans.

The Device Tree Overlay has a similar concept. It adds, removes, and edits Device Tree nodes. Nodes are pieces of Device Tree code that describe specific peripherals. These changes are made to the original Device Tree file via external files, called Device Tree Overlay files (named as target name plus .conf extension—for example: esp32_devkitc_wroom.overlay). Device Tree Overlays consist of a Device Tree’s modular and flexible resource. Figure 2 shows an example of a Device Tree Overlay node used to describe a Bosch’s BME280 sensor node in a Zephyr RTOS solution.

FIGURE 2
Example of Bosch’s BME280 sensor Device Tree Overlay.
FIGURE 2
Example of Bosch’s BME280 sensor Device Tree Overlay.

Zephyr RTOS makes use of Device Tree and Device Tree Overlays to describe which peripherals, such as communication interfaces, compatible drivers, sensors, and GPIOs, must be handled by the operating system and its applications. Then, taking a look at Device Tree Overlays of a project is usually enough to know which hardware peripherals it uses and their hardware-specific parametrizations.

It’s important to mention that any Zephyr RTOS-based project has exactly the same application code, and it doesn’t depend on which target it’s being compiled for. Therefore, the application code is the same for any MCU or SoC that should run this project. The hardware-specific parameterizations are set and configured in each target’s Device Tree Overlay. This results in great productivity when developing Zephyr RTOS based projects, since the application code doesn’t depend on the target (MCU or SoC) that will run it, allowing Device Tree Overlays to set all hardware-specific parametrizations.

Another interesting feature related to Device Tree Overlays is how and when they’re used in Zephyr RTOS. In embedded, Linux-based solutions, Device Tree Overlays are interpreted in boot time, in a dynamic way. This means that if a bug in it makes it impossible to use some piece of hardware described in Device Tree Overlay, you’ll only find it out after booting the embedded Linux-based solution.

In Zephyr RTOS, however, things work a little differently. Considering the limitations such as RAM memory and processing, that some MCUs and SoCs may present, Device Tree Overlays aren’t interpreted in boot time. Instead, they are interpreted during image compilation, meaning you’ll be able to find errors even before booting the system.

This allows one project to be compiled for a great variety of targets, enhancing the portability of embedded systems projects to different targets. This feature becomes especially useful for solutions that should have the lowest production cost possible (such as devices with aggressive prices due to many similar devices in the market). This makes it possible to rapidly and almost effortlessly port a project to another less expensive target and optimize development and manufacturing costs.

MODULARIZATION OF PROJECT-SPECIFIC TARGET CONFIGURATIONS

As explained in the previous topic, it’s possible to leave hardware-specific parametrizations to Device Tree Overlays, so the same application code supports several targets. In Zephyr RTOS-based projects, it’s also possible to modularize the hardware-specific project configurations, in external files named as: target name plus .conf extension (for example: esp32_devkitc_wroom.conf).

Every target that should run a project can thereby have its own specific configurations set in a file exclusively for this target, enhancing both productivity and scalability.

ZEPHYR RTOS PROJECT STRUCTURE

Zephyr RTOS-based projects follow a files/folder standard structure, shown in Figure 3. In this structure:

  • boards: folder contains all project Device Tree Overlay (.overlay) files and specific target configurations (.conf).
  • src: folder contains all application source-code (.c, .h and .cpp files).
  • CMakeLists.txt: CMake build system required file. It contains a set of directives and instructions describing the project’s source files and targets (executable, library, or both).
  • prj.conf: file contains configurations that should be applied to the project, regardless of the chosen target.
  • sample.yaml: file describes the project features, project description, and Zephyr RTOS subsystems project makes use. This file is optional, and is required only when there’s an intention to submit this project as a Zephyr RTOS contribution.

Alternatively, Kconfig files can also be used to set project-specific configurations.

FIGURE 3
Zephyr RTOS standard based project files/folder structure.
FIGURE 3
Zephyr RTOS standard based project files/folder structure.
PREPARING THE ZEPHYR RTOS DEVELOPMENT ENVIRONMENT

Now it’s time to prepare your computer as a Zephyr RTOS development environment. It’s possible to develop a Zephyr RTOS based solution using Windows, macOS or Linux (especially Ubuntu distro) as your computer operating system. But I’d like to recommend that you use Ubuntu distro, because it makes it easy to prepare a Zephyr RTOS development environment for those who aren’t familiar with Zephyr RTOS. The following steps assume you’re using Ubuntu and also that you’re running the most recent LTS Ubuntu version. (At the time this article was written, it was version 22.04.).

Before starting the process, open a terminal session and go to your home folder using the cd ~ command. Then follow the steps below.

  1. Upgrade all installed packages by using sudo apt-get update and sudo apt-get upgrade commands.
  2. Install all Zephyr RTOS development environment dependencies. To do this, use the commands in Listing 1.
  3. Since Zephyr RTOS uses a Python environment, this should be installed as well. To install and create a Python environment for Zephyr RTOS, use the commands in Listing 2.
  4. Add an alias to your bashrc file in Ubuntu, to make it easier to activate the Zephyr RTOS Python environment using a single command (in this case, the command activate_zephyr). To do this, add the source ~/zephyrproject/.venv/bin/activate line at the end of your ~./bashrc file.
  5. Once the Python environment for Zephyr is installed, it’s time to install west, the most important tool for developing solutions with Zephyr RTOS. To do this, use the pip install west command.
  6. After installing west, it’s time to get Zephyr RTOS code and install Python requirements for Zephyr RTOS. To do this, use the commands in Listing 3.
  7. After west, Zephyr RTOS Python environment, Zephyr RTOS source-code, and all Python requirements have been installed successfully, it’s time to install Zephyr SDK. This consists of a set of toolchains for all supported target architectures (MCUs and SoCs), as well as the host tools, for testing and debugging the Zephyr RTOS. To install Zephyr SDK, use the commands in Listing 4.
LISTING 1
Commands to install all Zephyr RTOS development environment dependencies in an Ubuntu machine.

wget https://apt.kitware.com/kitware-archive.shsudo bash kitware-archive.shsudo 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 libmagic1
LISTING 2
Commands to install and create Python environment for Zephyr RTOS.

sudo apt install python3-venvpython3 -m venv ~/zephyrproject/.venvsource ~/zephyrproject/.venv/bin/activate
LISTING 3
Commands to get Zephyr RTOS code and install Python requirements for Zephyr RTOS.

west init ~/zephyrprojectcd ~/zephyrprojectwest updatewest zephyr-exportpip install -r ~/zephyrproject/zephyr/scripts/requirements.txt
LISTING 4
Commands to install Zephyr SDK.

cd ~wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.4/zephyr-sdk-0.16.4_linux-x86_64.tar.xzwget -O - https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.4/sha256.sum | shasum --check --ignore-missingtar xvf zephyr-sdk-0.16.4_linux-x86_64.tar.xzcd zephyr-sdk-0.16.4./setup.shsudo cp ~/zephyr-sdk-0.16.4/sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.dsudo udevadm control --reload

Now you’re ready to develop solutions using Zephyr RTOS in an Ubuntu machine. Note that if you open a new terminal session to develop a Zephyr RTOS project, remember to execute the activate_zephyr command (to activate Zephyr RTOS Python environment), and go to the Zephyr RTOS project folder using the cd zephyrproject/zephyr command.

If you prefer to prepare the Zephyr RTOS development environment in another operating system (Windows or macOS), instructions are available from the Zephyr Project [5].

EXAMPLE PROJECT OVERVIEW: PULSE COUNTER WITH LoRaWAN CONNECTIVITY

Once you have prepared your computer as a Zephyr RTOS development environment, it’s time for an example project. This article will take as an example the pulse counter project with LoRaWAN (long range wide area network) connectivity, which can be obtained in the project’s GitHub repository [6].

This project counts pulses received in a digital input of a target (a MCU or SoC), and periodically sends the counter value to the Cloud, using a LoRaWAN module attached to a target’s UART communication interface. The LoRaWAN module used here, Smart Modular (Zilia Technologies), behaves like a modem, controlled via AT commands. The LoRaWAN communication in this project uses ABP (Authentication by Personalization) as activation mode, without the package receiving confirmation. It’s important to mention that each pulse generates an external interrupt, to have maximum efficiency when reading pulses (no polling required to read pulses).

Taking advantage of the Device Tree Overlay resource supported by Zephyr RTOS, this project is already prepared to be compiled for three different targets, each one having a different architecture:

  1. ESP32-C3, which uses RISC-V architecture;
  2. ESP32 WROOM-32, which uses Xtensa architecture;
  3. RP2040 (Raspberry Pi Pico board, for example), which uses ARM architecture.
UNDERSTANDING THE PULSE COUNTER PROJECT

To understand how the pulse counter project with LoRaWAN connectivity works, let’s explore its core points, especially those related to hardware (peripherals) description and control. It’s strongly recommended that you read explanations and check the project’s full source code at the same time. The Project’s full source code is available in the Project’s repository on GitHub [6].

The hardware components used in this project are listed in Table 1. In addition to those shown in the table, you will need general-purpose tools and prototype materials, including jumpers, solder iron, and beadboard.

Organization of the Example Project’s Source Code: Now is a good time to know how this project’s source code is organized. The project is divided into three different modules, with each one developed for a specific purpose:

  1. The Breathing Light Module is responsible for periodically blinking an LED, which indicates that the embedded software is running. This LED blinking feature is done using a thread;
  2. The LoRaWAN UART Smart Modular Module is responsible for communicating to the LoRaWAN module used in this project (Smart Modular/Ziliatech’s LoRaWAN module) via AT commands. This module’s features range from LoRaWAN module initialization to sending messages to the Cloud using LoRaWAN connectivity;
  3. The Pulse Counter Module reads pulses (using interrupts), counts the pulses read, and using a thread, periodically sends this pulse counter to the LoRaWAN UART Smart Modular Module, which redirects it to the Cloud using LoRaWAN connectivity.

Also from the software point of view, one important resource is the set of Device Tree Overlays, with one Device Tree Overlay for each supported MCU and/or SoC supported. As previously discussed, a Device Tree Overlay adds, removes, and edits Device Tree nodes to the original device tree file.

In Zephyr RTOS, it’s possible to have one Device Tree Overlay per MCU or SoC that must run the project. In the current pulse counter project, it means there’s one Device Tree Overlay for ESP-32 WROOM-32, ESP32-C3 and RP2040, describing to the Zephyr Kernel which hardware pieces this project should interact with. Listing 5Listing 6, and Listing 7 contain project Device Tree Overlays for the ESP-32 WROOM-32, ESP32-C3 and RP2040, respectively.

LISTING 5
ESP-32 WROOM-32 Device Tree Overlay.

        pulseinput = &button1;        led0 = &led0;    };};/ {    buttons {        compatible = “gpio-keys”;        button1: button_1 {            gpios = <&gpio0 21 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;			label = “Pulse Input”;			zephyr,code = <INPUT_KEY_1>;        };     leds {        compatible = “gpio-leds”;        led0: led {            gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;            label = “Breathing light”;        };      };       };};&pinctrl {uart1_default: uart1_default {		group1 {			pinmux = <UART1_TX_GPIO22>;		};		group2 {			pinmux = <UART1_RX_GPIO23>;			bias-pull-up;		};	};};&uart1 {	current-speed = <115200>;	pinctrl-0 = <&uart1_default>;	pinctrl-names = “default”;    status = “okay”;};
LISTING 6
ESP32-C3 Device Tree Overlay.

/ {    aliases {        pulseinput = &button1;        led0 = &led0;    };};/ {    buttons {        compatible = “gpio-keys”;        button1: button_1 {            gpios = <&gpio0 3 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;			label = “Pulse Input”;			zephyr,code = <INPUT_KEY_1>;        };     leds {        compatible = “gpio-leds”;        led0: led {            gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;            label = “Breathing light”;        };      };       };};&pinctrl {uart1_default: uart1_default {		group1 {			pinmux = <UART1_TX_GPIO6>;		};		group2 {			pinmux = <UART1_RX_GPIO7>;			bias-pull-up;		};	};};&uart1 {	current-speed = <115200>;	pinctrl-0 = <&uart1_default>;	pinctrl-names = “default”;    status = “okay”;};
LISTING 7
RP2040 Device Tree Overlay.

#include <dt-bindings/input/input-event-codes.h>/ {    aliases {        pulseinput = &button1;        led0 = &led0;    };};/ {    buttons {        compatible = “gpio-keys”;        status = “okay”;                button1: button_1 {            gpios = <&gpio0 2 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;			label = “Pulse Input”;			zephyr,code = <INPUT_KEY_1>;        };      };    leds {        status = “okay”;    };   };&pinctrl {        uart1_default: uart1_default {		group1 {			pinmux = <UART1_TX_P4>;		};		group2 {			pinmux = <UART1_RX_P5>;			input-enable;		};	};};&uart1 {        current-speed = <115200>;	    status = “okay”;	    pinctrl-0 = <&uart1_default>;	    pinctrl-names = “default”;};

Note that Listings 5 to 7 all describe the same set of peripherals: an LED (used for breathing light routines); a button (used as pulse input); and a UART communication interface (used for communicating to LoRaWAN Module)—according to the specific target (MCU or SoC) they represent, considering target-hardware-specific configurations and parametrizations. This brings considerable productivity and scalability to the project, since it can run exactly in the same way no matter which target has been chosen. It also makes it easier to understand how to wire the hardware components to MCUs and SoCs, because all GPIOs used for all peripherals are explicitly defined in Device Tree Overlay.

— ADVERTISMENT—

Advertise Here

Controlling the Hardware: Now that you know how Device Tree Overlays work in this project, it’s time to learn how to control the hardware components described in these Device Tree Overlays. To understand how this project’s source code controls the LED described in target’s specific Device Tree Overlays, let’s take the Breathing Light Module code as an example.

The LED node (from Device Tree Overlay) is loaded from target’s Device Tree Overlay using GPIO_DT_SPEC_GET and DT_ALIAS macro, as shown in Listing 8. DT_ALIAS macro makes reference to led0 node described in target Device Tree Overlay, while GPIO_DT_SPEC_GET macro associates it to the variable called led, which will allow the breathing light’s LED to be initialized, turned on, or turned off.

Once the LED is loaded from target’s Device Tree Overlay, it can be initialized using the gpio_is_ready_dt() and gpio_pin_configure_dt() functions, and if everything works fine, a breathing light thread is created. All these operations can be seen in Listing 9.

The gpio_pin_toggle_dt() function is used to toggle the LED on and off:

gpio_pin_toggle_dt(&led);

Another piece of hardware this project controls is the pulse input. In Zephyr RTOS, this pulse input behaves as a button. The code shown in Listing 10 is used to load this pulse input from target’s Device Tree Overlay. This code first checks if the pulse input node (button node) exists in Device Tree Overlay, and then loads it in the pulseinput variable using the GPIO_DT_SPEC_GET_OR macro.

Once pulse input is loaded from target’s Device Tree Overlay, it can be initialized using the gpio_is_ready_dt() and gpio_pin_configure_dt() functions. Each pulse read uses GPIO_INT_EDGE_TO_ACTIVE mode, meaning an interrupt will be generated whatever edge this pulse presents—low to high or high to low. Because each pulse read will generate an external interrupt, this external interrupt is configured using the gpio_pin_interrupt_configure_dt() function. When an interruption is generated by an acquired pulse, the callback that is called is configured using gpio_init_callback() and gpio_add_callback() functions. All this is shown in Listing 11.

LISTING 8
Loading LED parametrization and configurations from target’s Device Tree Overlay.

/* Specify which node is used for LED in device tree*/#define LED0_NODE DT_ALIAS(led0)/* Load breathing light LED device tree data */static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
LISTING 9
Breathing light initializing.

if (!gpio_is_ready_dt(&led)) {    init_breathing_light_status = 0;    goto END_BREATHING_LIGHT_INIT;}ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);if (ret < 0) {    init_breathing_light_status = 0;    goto END_BREATHING_LIGHT_INIT;}/* Create pulse counter thread */k_thread_create(&breathing_light_thread_data, breathing_light_thread_stack,          K_THREAD_STACK_SIZEOF(breathing_light_thread_stack),		      breathing_light_task, NULL, NULL, NULL,		      CONFIG_BREATHING_LIGHT_TASK_PRIO, 0, K_NO_WAIT);LOG_INF(“Breathing light module has been started”); init_breathing_light_status = 1;	END_BREATHING_LIGHT_INIT:return init_breathing_light_status;
LISTING 10
Loading pulse input as a button.

#if !DT_NODE_HAS_STATUS(PULSEINPUT_NODE, okay)#error “Unsupported board: pulseinput devicetree alias is not defined”#endifstatic const struct gpio_dt_spec pulseinput = GPIO_DT_SPEC_GET_OR(PULSEINPUT_NODE, gpios,							      {0});
LISTING 11
Pulse input configuration, including interrupt resource.

if (!gpio_is_ready_dt(&pulseinput)) {    ...    return 0;}ret = gpio_pin_configure_dt(&pulseinput, GPIO_INPUT);if (ret != 0) {    ...    return 0;}/* Configure pulse interrupt edge as rising edge */ret = gpio_pin_interrupt_configure_dt(&pulseinput, GPIO_INT_EDGE_TO_ACTIVE);if (ret != 0) {    ...    return 0;}gpio_init_callback(&pulseinput_cb_data, pulse_detected, BIT(pulseinput.pin));gpio_add_callback(pulseinput.port, &pulseinput_cb_data);...

There’s another important measure to apply to pulse input reading and ensure accuracy in pulse counting—”debounce routine.” As pulse input is often wired to mechanical or electromechanical components (such as a switch, push button, or even a relay output), the signal acquired by pulse input will always present undesired bouncing that needs to be ignored or filtered. This is accomplished through use of software timers, which work as described below.

First, each pulse input detection (with low-to-high or high-to-low edge) callback call will restart a software timer, whose time overflow value (amount of time that generated a timer callback) was set using a debounce time value. This value typically ranges from hundreds of milliseconds to a full second.

Second, while the pulse input signal bounces, the software timer restarts. When pulse input stops bouncing, it means the signal input bouncing effect is over. From this point forward, the software timer won’t be restarted and will reach its overflow.

Third, when the software timer overflow callback is called, it means pulse input bounce has been fully filtered; then the pulse counter can be safely incremented by one.

The software timer configuration, pulse input callback function, and software timer callback function are shown in Listing 12.

The last piece of hardware (peripheral) that this project controls is the LoRaWAN module. As mentioned before, this LoRaWAN module is controlled via AT commands, using UART as the communication interface. The UART communication interface, with its hardware-specific configurations and parametrizations, is loaded from target’s Device Tree Overlay using DEVICE_DT_GET and DT_NODELABEL macros, shown in the following code:

/* Load device tree defs for lorawan uart */

const struct device *uart_lorawan = DEVICE_DT_GET(DT_NODELABEL(uart1));

After loading the UART communication interface from Device Tree Overlay, this is configured by uart_cfg structure and initialized as shown in Listing 13. Once the UART communication has been successfully initialized and configured, data can be sent and received from it, as shown in Listing 14.

LISTING 12
Software timer configuration, pulse input callback

/* Pulse debounce timer */static struct k_timer pulse_debounce_timer;.../* Init pulse debounce timer */k_timer_init(&pulse_debounce_timer, pulse_debounce_timer_callback, NULL);.../* Pulse input callback, called when pulse input is detected (whatever edge it presents)*/void pulse_detected(const struct device *dev, struct gpio_callback *cb, uint32_t pins){    ...    k_timer_start(&pulse_debounce_timer, K_MSEC(PULSE_DEBOUNCE_TIME), K_NO_WAIT);}.../* Software timer callback */static void pulse_debounce_timer_callback(struct k_timer *timer){    pulse_counter++;}
LISTING 13
Configuring and initializing UART communication interface.

/* Struct for lorawan uart configuration */struct uart_config uart_cfg = {	.baudrate = 9600,	.parity = PARITY_LORAWAN_UART,	.stop_bits = STOP_BITS_LORAWAN_UART,	.flow_ctrl = FLOW_CTRL_LORAWAN_UART,	.data_bits = DATA_BITS_LORAWAN_UART,};.../*  Configuing UART communication interface */rc = uart_configure(uart_lorawan, &uart_cfg);    if (rc){    LOG_INF(“Failed to configure lorawan uart”);    ...}
LISTING 14
Sending and receiving data from UART.

/* Send data to UART communication interface */void send_custom_at_command_lorawan_smartmodular_uart(char *at_command){    int msg_len = strlen(at_command);    int i = 0;    for (i=0; i<msg_len; i++)     {        uart_poll_out(uart_lorawan, at_command[i]);     }}/* Receive data from UART communication interface */void rcv_response_lorawan_smartmodular_uart(void){    char c;		    LOG_INF(“Bytes received from LoRaWAN module:”);    while (!uart_poll_in(uart_lorawan, &c))     {         LOG_INF(“%c”, c);     }}

PREPARING PROJECT SOFTWARE

Before compiling and flashing this software to target, some preparation is required.

  • First, remember to replace your LoRaWAN ABP credentials (Device Address, Application Session Key, Network Session Key, and Application EUI) into DEVADDR, APPSKEY, NWKSKEY and APPEUI constants in the project source code, otherwise the data project sent over LoRaWAN connectivity won’t reach your LoRaWAN instance in the Cloud;
  • Second, debounce time is configured as 1,000ms by default. It can be changed in PULSE_COUNTER_TIME_MS defined in project source code;
  • Third, the breathing light’s blink period is configured as 1,000ms by default. It can be changed in BREATHING_LIGHT_TIME_MS defined in project source code;
  • Fourth, to change the GPIOs to which the peripherals are wired, edit the Device Tree Overlay file corresponding to the target used.
COMPILING, FLASHING AND TESTING THIS PROJECT

Now it’s time to compile this software for the target you want to run it. To do this, place project source code (pulse_counter_project folder) into the zephyrproject/zephyr folder, and run the commands shown below. Note that different commands are used for the ESP-32 WROOM-32, ESP32-C3, and RP2040.

To compile this project for ESP32 WROOM-32 (Xtensa architecture), use the command:

west build -p always -b esp32_devkitc_wroom pulse_counter_project/

To compile this project for ESP32-C3 (Risc-V architecture), use the command:

— ADVERTISMENT—

Advertise Here

west build -p always -b esp32c3_devkitm pulse_counter_project/

And to compile this project for RP2040 (ARM architecture), use the command:

west build -p always -b rpi_pico pulse_counter_project/

The compilation process may take several minutes, depending on your computer CPU cores amount and clock, and also depending on your computer’s amount of RAM. As discussed earlier, Zephyr’s meta tool, west, includes a flash command. After compilation, it’s time to flash this project into target. To do this, ensure you have the target connected to your computer via USB cable and run the command:

west flash

Flashing the project to target will take only seconds. After flashing, you can test the project and check all its verbose debug messages by using any serial terminal connected to the target’s COM port / ttyUSB* port. After receiving verbose debug messages in the serial terminal, simulate pulses in pulse input. (Check target Device Tree Overlay to discover which GPIO this is.)

FIGURE 4
Verbose debug messages (testing software).
FIGURE 4
Verbose debug messages (testing software).

Figure 4 is an example of verbose debug messages of this project. You can see the debouncing working, by the messages highlighted in light blue. I have attached a push button in the pulse input GPIO, and pressed it twice. In messages, it’s possible to check for multiple pulse detections, which is the consequence of the bounce effect commonly seen when using mechanical and electromechanical components like the push-button in this test; it’s also possible to check that debounce routines have successfully filtered the noise (generated by mechanical “bounces” in the push-button), and correctly counted the total number of pulses as 2. Also, check your LoRaWAN distributor console to get LoRaWAN messages sent from this project to the Cloud, as shown in Figure 5.

FIGURE 5
LoRaWAN distributor console, showing LoRaWAN messages sent by this project.
FIGURE 5
LoRaWAN distributor console, showing LoRaWAN messages sent by this project.
CONCLUSION

In this article, the Zephyr Project’s real-time operating system for embedded devices, especially MCUs (Zephyr RTOS), was introduced and discussed. I showed what this RTOS consists of and how it can be a valuable RTOS for a variety of embedded software products, especially when versatility, scalability, code reuse, and high embedded software quality are requirements.

The example project I presented—a pulse counter with LoRaWAN connectivity—is a fully open-source project that can be taken as basis for other Zephyr RTOS projects. It exemplifies the usage of Zephyr’s subsystems and makes smart use of Device Tree Overlays for multiple MCUs and SoCs. 

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • APRIL 2024 #405 – Get a PDF of the issue

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2026 KCK Media Corp.

Zephyr RTOS

by Pedro Bertoleti time to read: 22 min