Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

12.5 Zephyr RTOS

Throughout this book you have built up two complementary skill sets for the edge: the C and C++ tradition that gives you direct, efficient control of hardware, and the Rust ecosystem that adds memory safety and data-race-free concurrency. You have applied both on the ESP32, comparing bare-metal no_std, the std path on top of ESP-IDF (which is really FreeRTOS), and the modern async synthesis offered by Embassy. This section introduces a third option that sits alongside those, and increasingly underpins commercial edge products: the Zephyr RTOS.

Zephyr matters here for a simple reason. The book has deliberately taught principles (ownership, concurrency models, networking, resource budgets, design patterns) rather than a single vendor framework, because the edge is heterogeneous and you will move between platforms during your career. Zephyr is the clearest example of that portability in practice. The same ESP32 boards you have used in this book are first-class Zephyr targets, and almost every concept you have learned reappears, sometimes under a different name.

Zephyr is a small, scalable, open-source real-time operating system (RTOS) designed for resource-constrained devices, from tiny sensor nodes with a few kilobytes of RAM up to richer microcontroller-class systems. It is hosted by the Linux Foundation and developed as a vendor-neutral project, with founding and member organisations including Intel, Nordic Semiconductor, NXP, STMicroelectronics, and Espressif (the makers of the ESP32). It is released under the permissive Apache 2.0 licence, which makes it attractive for commercial products.

Two characteristics make Zephyr stand out for the kind of work in this book.

  • It is genuinely cross-architecture. A single codebase and application can be built for Arm Cortex-M, RISC-V, ARC, x86, and Xtensa cores, among others. The Xtensa and RISC-V support is exactly why the ESP32 family (Xtensa on the classic and S series, RISC-V on the C and H series, as discussed in Chapter 12) runs Zephyr.
  • It borrows its development model from the Linux kernel. Hardware is described in devicetree, software features are switched on and off through Kconfig, and a meta-tool called west manages the multi-repository source tree and the CMake-based build. If you have used the Linux kernel, this will feel familiar; if you have not, it is a clean, well-documented model worth learning.

In Chapter 12 you saw the spectrum from a naive no_std blocking loop, through FreeRTOS preemptive tasks, to Embassy’s cooperative async tasks. An RTOS such as Zephyr occupies the same problem space: it gives you a scheduler so that reading a sensor, servicing the network, and updating a display can each be expressed as an independent unit of work, rather than tangled together in one loop.

Zephyr’s kernel provides the building blocks you would expect from the concurrency chapter, implemented for tiny devices:

  • Threads with both cooperative and preemptive scheduling, and priorities.
  • Synchronisation: mutexes, semaphores, and condition variables.
  • Communication: message queues, FIFOs, LIFOs, mailboxes, and pipes (the direct equivalent of the channels you met in Chapter 10).
  • Timing: kernel timers, delayed work queues, and high-resolution time.
  • Memory management: a kernel heap, plus deterministic memory slabs for fixed-size allocation, which avoid the fragmentation that makes a general heap risky on a long-running device.

How Zephyr compares to the ESP32 paths you already know

Section titled “How Zephyr compares to the ESP32 paths you already know”

Chapter 12 compared three Rust paths on the ESP32. Zephyr is best understood as a fourth column in that same table: a portable, C-first RTOS that you can also drive from C++ and, increasingly, from Rust.

Characteristicstd + ESP-IDFno_std + esp-halno_std + EmbassyZephyr RTOS
Primary languageRust on C (FreeRTOS)RustRustC (with C++ and Rust support)
Portable across vendorsNo (Espressif only)Partly (via embedded-hal)PartlyYes (Arm, RISC-V, Xtensa, x86)
Concurrency modelOS threads (FreeRTOS)Interrupts / blockingasync/await tasksRTOS threads plus work queues
Hardware described byESP-IDF configRust HAL crateRust HAL crateDevicetree
Configurationmenuconfig / sdkconfigCargo featuresCargo featuresKconfig
NetworkinglwIPexternal cratesembassy-netNative IP stack, BSD sockets
Typical useQuick connected appsTight size budgetsI/O-heavy IoTPortable, certifiable products

A board support package is the collection of files that tells Zephyr how to run on one specific board: which CPU and SoC it uses, how much memory it has, which pins connect to which peripherals, and which features should be enabled by default. Zephyr does not keep this information in scattered #define macros. It separates the two questions that the HAL discussion in Chapter 12 hinted at:

  • What hardware exists, and how is it wired? This is answered by devicetree.
  • What software features do I want compiled in? This is answered by Kconfig.

Keeping these separate is what lets one application build for many boards. You change the board, not your code.

Devicetree is a text format (inherited from the Linux kernel) that describes the hardware as a tree of nodes. A board’s .dts file declares its SoC, memory, and peripherals; your application can then refer to them symbolically. For example, a board defines an led0 alias, and your code asks for “whatever led0 is on this board” without caring which physical pin that is.

/* Simplified board devicetree fragment */
/ {
aliases {
led0 = &green_led;
};
leds {
compatible = "gpio-leds";
green_led: led_0 {
gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
label = "Green LED";
};
};
};

This is the same role the esp-hal crate played in Chapter 12, where you wrote Output::new(peripherals.GPIO8, Level::High) instead of poking registers. Devicetree is Zephyr’s vendor-neutral way of providing that abstraction, and it is resolved at build time so there is no runtime cost.

Kconfig (again borrowed from the Linux kernel) selects which subsystems and drivers are compiled into your image. You set options in a prj.conf file in your project. Because Zephyr only links what you ask for, an image stays small, which directly serves the memory-constraint concerns from Chapter 2.

# prj.conf : enable logging and GPIO for this application
CONFIG_GPIO=y
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3

west is Zephyr’s meta-tool. It manages the many Git repositories that make up a Zephyr installation (the core, plus vendor HALs and libraries as modules), and it wraps the CMake and Ninja build. The multi-repo, Git-driven workflow is exactly the kind of branching and dependency management you practised in Chapter 4, applied at the scale of an operating system.

A first build and flash for an ESP32-S3 board looks like this:

Terminal window
# Build the standard blinky sample for an ESP32-S3 dev board
west build -b esp32s3_devkitc samples/basic/blinky
# Flash it over USB and open a serial monitor
west flash
west espressif monitor

The canonical “blinky” shows devicetree and the kernel API working together. Note how the code never mentions a specific pin: it asks devicetree for led0.

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
/* Pull the led0 alias from the board's devicetree */
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
int main(void)
{
if (!gpio_is_ready_dt(&led)) {
return 0;
}
gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
while (1) {
gpio_pin_toggle_dt(&led);
k_msleep(500); /* kernel sleep: yields to the scheduler */
}
return 0;
}

The key difference from a bare Arduino sketch is k_msleep. It does not busy-wait; it hands the CPU back to the scheduler so other threads can run, and on many boards the core can drop into a low-power idle state, which connects directly to the power-budget discussion in Chapter 2.

Zephyr is, at heart, a C project, but it supports all three languages this book has taught. Understanding where each fits tells you how your existing skills transfer.

The kernel, drivers, and subsystems are written in C, and C is the path of least resistance for a Zephyr application. Everything from Chapter 3 (types, pointers, control flow, and especially bit manipulation for registers) applies directly. One detail worth highlighting for object-oriented thinkers: Zephyr’s driver model is object orientation expressed in C. Each driver class defines a struct of function pointers (an API table), and each device instance is bound to one. This is precisely the manual “vtable” mechanism that Chapter 1 and Chapter 5 described as the machinery underneath C++ virtual functions. If you understand polymorphism, you already understand Zephyr drivers.

Zephyr supports application code in C++ (commonly up to C++20), enabled with a single Kconfig option. This lets you reuse the class design, inheritance, RAII, and templates from Chapters 5 and 6 to model peripherals and protocols as clean objects.

# prj.conf : turn on C++ support
CONFIG_CPP=y
CONFIG_STD_CPP20=y
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
// RAII-style wrapper around an LED, in the spirit of Chapter 5
class Led {
public:
explicit Led(const gpio_dt_spec &pin) : pin_(pin)
{
gpio_pin_configure_dt(&pin_, GPIO_OUTPUT_ACTIVE);
}
void toggle() const { gpio_pin_toggle_dt(&pin_); }
private:
const gpio_dt_spec &pin_;
};

There are important constraints, and they reinforce a recurring theme of this book. On a constrained device you typically avoid the parts of C++ that hide cost: exceptions and run-time type information are off by default, and the full Standard Template Library is not generally available because most containers assume a heap. The smart-pointer and modern-C++ habits from Chapter 6 still apply, but with the same discipline Chapter 12 urged for no_std Rust: prefer stack and static storage, and treat the heap as a scarce resource.

Rust support in Zephyr is emerging and experimental but advancing quickly, through an official module (zephyr-lang-rust) that lets you write application code in Rust against safe bindings to the Zephyr kernel. This is a different model from the ESP32 Rust paths in Chapter 12: rather than esp-hal or Embassy providing the runtime, Zephyr provides the RTOS and Rust sits on top, much as the std + ESP-IDF path sits on FreeRTOS.

// Representative zephyr-lang-rust application (experimental API)
#![no_std]
use zephyr::printkln;
#[no_mangle]
extern "C" fn rust_main() {
loop {
printkln!("Hello from Rust on Zephyr");
zephyr::time::sleep(zephyr::time::Duration::millis(500));
}
}
LanguageStatus on ZephyrWhat transfers from this bookWatch out for
CNative, first-classChapter 3 fundamentals; driver model as OO-in-CManual safety; the burden Rust removes
C++Supported, widely usedChapters 5 and 6: classes, RAII, templatesNo exceptions/RTTI by default; limited STL
RustExperimental, growingChapters 7 and 8: ownership, Result, heaplessUnstable bindings; smaller ecosystem so far

Practical Examples: Concurrency and Networking

Section titled “Practical Examples: Concurrency and Networking”

Two short examples show how the concurrency and networking chapters reappear almost verbatim in Zephyr.

Threads and a message queue (Chapter 10). A producer thread reads a sensor and posts readings to a queue; the main thread consumes them. This is the channel pattern from Chapter 10, and the Embassy DATA_CHANNEL example from Chapter 12, expressed with Zephyr primitives.

#include <zephyr/kernel.h>
/* A queue of up to 10 four-byte readings */
K_MSGQ_DEFINE(readings_q, sizeof(int32_t), 10, 4);
#define STACK_SIZE 1024
#define PRIORITY 5
K_THREAD_STACK_DEFINE(producer_stack, STACK_SIZE);
static struct k_thread producer;
static void producer_entry(void *a, void *b, void *c)
{
int32_t reading = 0;
while (1) {
reading = read_sensor(); /* your sensor read */
k_msgq_put(&readings_q, &reading, K_FOREVER);
k_msleep(100);
}
}
int main(void)
{
k_thread_create(&producer, producer_stack, STACK_SIZE,
producer_entry, NULL, NULL, NULL,
PRIORITY, 0, K_NO_WAIT);
int32_t value;
while (1) {
k_msgq_get(&readings_q, &value, K_FOREVER); /* blocks until data */
process(value);
}
return 0;
}

A network client with BSD sockets (Chapter 11). Zephyr provides a BSD-style sockets API, so the client and server code from Chapter 11 maps over with minimal change. The same socket, connect, send, and recv calls you learned apply, and TLS is available through a socket option backed by mbedTLS, which connects to the encryption material in Section 11.4.

#include <zephyr/net/socket.h>
int sock = zsock_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
zsock_connect(sock, (struct sockaddr *)&addr, sizeof(addr));
zsock_send(sock, payload, len, 0);

Almost every chapter has a direct counterpart in Zephyr. The table below is a study aid: it shows that the principles you learned are transferable, and names the Zephyr feature where each idea reappears.

Book chapter and topicWhere it reappears in Zephyr
Ch 1, 5: OOP, classes, inheritance, polymorphismThe driver model (API tables of function pointers) is OO-in-C; C++ classes model peripherals and protocols
Ch 2: Edge architectures, resource constraints, powerZephyr targets the endpoint tier; Kconfig trims the image; the PM subsystem manages sleep states
Ch 3: C fundamentals, pointers, bit manipulationThe native language of the kernel, drivers, and register-level work
Ch 4: Git and GitLab, branchingwest manages a multi-repo Git tree; module versions are pinned and branched
Ch 6: Templates, STL, smart pointers, modern C++C++ application code, with the heap-avoidance discipline a constrained device demands
Ch 7, 8: Rust ownership, borrowing, Result/Option, collectionsThe experimental Rust binding; Result/Option for fallible hardware calls; heapless collections
Ch 9: GUIs, immediate-mode eguiZephyr’s display subsystem and the LVGL library for on-device user interfaces
Ch 10: Threads, channels, atomics, data racesKernel threads, message queues and FIFOs, atomics, mutexes, and semaphores
Ch 11: Network clients and servers, async I/O, encryptionNative IP stack, BSD sockets, MQTT and CoAP, TLS via mbedTLS
Ch 12: ESP32, resource management, design patternsZephyr runs on the same ESP32 boards; memory slabs, work queues, and split-peripheral patterns

A real strength of Zephyr for teaching is that you do not need hardware to start. It can build for host and simulated targets, which removes the cost and logistics barrier for a class, and then move unchanged to a physical board.

  • native_sim: builds your Zephyr application as a native executable for your development machine. You run it like any other program, which is ideal for learning the API, the threading model, and networking before touching hardware.
  • QEMU: Zephyr ships QEMU targets (for example Arm Cortex-M and RISC-V) so you can run real cross-compiled firmware in an emulator.
  • Renode: an open-source functional simulator that can model whole boards and even multi-node networks, which is useful for the multi-device edge scenarios from Chapter 2. It is covered in depth, with worked GPIO and I2C examples, on the dedicated Renode page.
  • Wokwi: the browser-based simulator referenced in Chapter 12 also supports Zephyr, which is convenient for quick experiments and sharing.
Terminal window
# Build and run entirely on your PC, no board required
west build -b native_sim samples/hello_world
./build/zephyr/zephyr.exe

When you do move to hardware, these boards are well supported and widely used for teaching.

BoardCoreWhy it suits this book
ESP32-S3 / ESP32-C3 DevKitXtensa LX7 / RISC-VThe book’s own target hardware; Wi-Fi and BLE for the networking and AIoT chapters
Nordic nRF52840 DKArm Cortex-M4A flagship Zephyr board; excellent BLE, Thread, and low-power support
Raspberry Pi Pico / Pico 2RP2040 / RP2350Very low cost, good for class sets, strong Zephyr support
ST Nucleo (various)Arm Cortex-MInexpensive, huge peripheral range, well documented

The book frames the edge through Edge AI and AIoT: putting machine-learning intelligence on resource-constrained devices, as set out in Chapter 2. Zephyr is a natural operating system for the endpoint tier of that three-layer architecture (the leaf nodes performing real-time inference at the data source), and it provides the surrounding infrastructure that a TinyML application needs to be useful in the field.

Zephyr integrates the inference runtimes used for on-device machine learning. TensorFlow Lite for Microcontrollers is available as a Zephyr module, and platforms such as Edge Impulse can export a model packaged for a Zephyr build. The optimisation techniques from Chapter 2 are what make this fit: a model that has been quantised to 8-bit integers and pruned can run within the kilobytes of RAM a Zephyr endpoint has available, and benefits directly from the AI vector instructions on the ESP32-S3 mentioned in Chapter 12.

A model is only as good as its inputs. Zephyr’s sensor API gives a uniform, devicetree-described interface to accelerometers, microphones, temperature sensors, and the like, and the newer sensing subsystem adds higher-level sensor fusion. This means the sensor read in your TinyML pipeline looks the same across vendors, the same portability benefit you saw with devicetree for GPIO.

The resource challenges from Chapter 2 are addressed by named Zephyr subsystems, which is a useful way to revise that material.

  • Power. The power management (PM) subsystem puts the device into low-power idle and sleep states automatically when threads are blocked, which is the operating-system-level realisation of the sleep-mode and DVFS strategies described in Section 2.1.
  • Connectivity. For the “offline-first”, intermittently connected reality of the edge, Zephyr supports BLE, Thread and Matter, Wi-Fi, and 802.15.4, with lightweight application protocols (MQTT, MQTT-SN, CoAP, LwM2M) for telemetry. These are the on-device counterparts to the networking you built in Chapter 11.
  • Security and updates. Edge security from Section 2.1 maps onto concrete components: MCUboot provides secure boot and signed images, Trusted Firmware-M uses hardware security features on Arm, and the MCUmgr/SMP protocol supports over-the-air firmware updates. That update path is also how you address model drift: when a deployed model’s accuracy declines, you push a retrained, re-quantised model to the fleet as a signed firmware update.

Zephyr is not a replacement for what you have learned through this text; it is a demonstration that what you learned was portable all along.

  • It is a vendor-neutral, Linux-Foundation RTOS that runs across Arm, RISC-V, and Xtensa, including the very ESP32 boards used in this book.
  • Its BSP model separates hardware description (devicetree) from software configuration (Kconfig), with west managing a Git multi-repo build, so one application targets many boards.
  • It is C-first, supports C++ for object-oriented design, and has experimental Rust support, so all three languages in this book apply.
  • Nearly every chapter, from OOP and concurrency to networking and resource management, has a direct counterpart in a named Zephyr feature.
  • It can be taught without hardware using native_sim, QEMU, Renode, or Wokwi, then moved unchanged to physical boards.
  • For AIoT, it provides the full endpoint-tier stack: sensors, a TinyML runtime, power management, standard connectivity, and secure over-the-air updates.
Concept Match

Match the Zephyr Concept to its Role

Quiz
Select 0/1

Why is the ESP32 family able to run the Zephyr RTOS?

Quiz
Select 0/2

In Zephyr, what is the relationship between devicetree and Kconfig? (Select all that apply.)

Quiz
Select 0/1

How does Zephyr support the three programming languages taught in this book?