12.5 Zephyr RTOS
Introduction
Section titled “Introduction”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.
What is Zephyr?
Section titled “What is Zephyr?”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.
Why an RTOS at the edge?
Section titled “Why an RTOS at the edge?”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.
| Characteristic | std + ESP-IDF | no_std + esp-hal | no_std + Embassy | Zephyr RTOS |
|---|---|---|---|---|
| Primary language | Rust on C (FreeRTOS) | Rust | Rust | C (with C++ and Rust support) |
| Portable across vendors | No (Espressif only) | Partly (via embedded-hal) | Partly | Yes (Arm, RISC-V, Xtensa, x86) |
| Concurrency model | OS threads (FreeRTOS) | Interrupts / blocking | async/await tasks | RTOS threads plus work queues |
| Hardware described by | ESP-IDF config | Rust HAL crate | Rust HAL crate | Devicetree |
| Configuration | menuconfig / sdkconfig | Cargo features | Cargo features | Kconfig |
| Networking | lwIP | external crates | embassy-net | Native IP stack, BSD sockets |
| Typical use | Quick connected apps | Tight size budgets | I/O-heavy IoT | Portable, certifiable products |
Zephyr Board Support Packages (BSPs)
Section titled “Zephyr Board Support Packages (BSPs)”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: describing the hardware
Section titled “Devicetree: describing the hardware”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: configuring the software
Section titled “Kconfig: configuring the software”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 applicationCONFIG_GPIO=yCONFIG_LOG=yCONFIG_LOG_DEFAULT_LEVEL=3west and the build system
Section titled “west and the build system”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:
# Build the standard blinky sample for an ESP32-S3 dev boardwest build -b esp32s3_devkitc samples/basic/blinky
# Flash it over USB and open a serial monitorwest flashwest espressif monitorA first Zephyr program in C
Section titled “A first Zephyr program in C”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.
The Role of C, C++, and Rust in Zephyr
Section titled “The Role of C, C++, and Rust in Zephyr”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.
C: the native language
Section titled “C: the native language”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.
C++ on Zephyr
Section titled “C++ on Zephyr”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++ supportCONFIG_CPP=yCONFIG_STD_CPP20=y#include <zephyr/kernel.h>#include <zephyr/drivers/gpio.h>
// RAII-style wrapper around an LED, in the spirit of Chapter 5class 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 on Zephyr
Section titled “Rust on Zephyr”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)); }}| Language | Status on Zephyr | What transfers from this book | Watch out for |
|---|---|---|---|
| C | Native, first-class | Chapter 3 fundamentals; driver model as OO-in-C | Manual safety; the burden Rust removes |
| C++ | Supported, widely used | Chapters 5 and 6: classes, RAII, templates | No exceptions/RTTI by default; limited STL |
| Rust | Experimental, growing | Chapters 7 and 8: ownership, Result, heapless | Unstable 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 5K_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);How This Book Maps onto Zephyr
Section titled “How This Book Maps onto Zephyr”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 topic | Where it reappears in Zephyr |
|---|---|
| Ch 1, 5: OOP, classes, inheritance, polymorphism | The driver model (API tables of function pointers) is OO-in-C; C++ classes model peripherals and protocols |
| Ch 2: Edge architectures, resource constraints, power | Zephyr targets the endpoint tier; Kconfig trims the image; the PM subsystem manages sleep states |
| Ch 3: C fundamentals, pointers, bit manipulation | The native language of the kernel, drivers, and register-level work |
| Ch 4: Git and GitLab, branching | west 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, collections | The experimental Rust binding; Result/Option for fallible hardware calls; heapless collections |
Ch 9: GUIs, immediate-mode egui | Zephyr’s display subsystem and the LVGL library for on-device user interfaces |
| Ch 10: Threads, channels, atomics, data races | Kernel threads, message queues and FIFOs, atomics, mutexes, and semaphores |
| Ch 11: Network clients and servers, async I/O, encryption | Native IP stack, BSD sockets, MQTT and CoAP, TLS via mbedTLS |
| Ch 12: ESP32, resource management, design patterns | Zephyr runs on the same ESP32 boards; memory slabs, work queues, and split-peripheral patterns |
Test and Teaching Platforms for Zephyr
Section titled “Test and Teaching Platforms for Zephyr”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.
Simulate first
Section titled “Simulate first”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.
# Build and run entirely on your PC, no board requiredwest build -b native_sim samples/hello_world./build/zephyr/zephyr.exeRecommended hardware
Section titled “Recommended hardware”When you do move to hardware, these boards are well supported and widely used for teaching.
| Board | Core | Why it suits this book |
|---|---|---|
| ESP32-S3 / ESP32-C3 DevKit | Xtensa LX7 / RISC-V | The book’s own target hardware; Wi-Fi and BLE for the networking and AIoT chapters |
| Nordic nRF52840 DK | Arm Cortex-M4 | A flagship Zephyr board; excellent BLE, Thread, and low-power support |
| Raspberry Pi Pico / Pico 2 | RP2040 / RP2350 | Very low cost, good for class sets, strong Zephyr support |
| ST Nucleo (various) | Arm Cortex-M | Inexpensive, huge peripheral range, well documented |
Zephyr and AIoT
Section titled “Zephyr and AIoT”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.
Running models on-device (TinyML)
Section titled “Running models on-device (TinyML)”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.
Getting data into the model
Section titled “Getting data into the model”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.
Power, connectivity, and trust
Section titled “Power, connectivity, and trust”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.
Summary
Section titled “Summary”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
westmanaging 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.
🧩 Knowledge Check
Section titled “🧩 Knowledge Check”Match the Zephyr Concept to its Role
Why is the ESP32 family able to run the Zephyr RTOS?
In Zephyr, what is the relationship between devicetree and Kconfig? (Select all that apply.)
How does Zephyr support the three programming languages taught in this book?
© 2026 Derek Molloy, Dublin City University. All rights reserved.