Sitemap
Press enter or click to view image in full size

Getting Started with Zephyr OS on Arduino đŸȘ

9 min readMar 21, 2025

--

Zephyr OS is a lightweight, open-source real-time operating system (RTOS) maintained by the Linux Foundation. Designed for resource-constrained devices, it offers scalability, modularity, and real-time capabilities, making it an attractive choice for embedded developers.

Press enter or click to view image in full size
Zephyr Logo. Source: Zephyr Project.

Compared to FreeRTOS (currently one of the most widely used open-source RTOS, developed by Amazon Web Services Open Source), Zephyr uses a microkernel architecture, offering more modularity and flexibility. FreeRTOS, with its monolithic kernel, is more straightforward but less flexible, focusing on ease of integration.

Press enter or click to view image in full size
Monolithic vs Microkernel OS. Source: Wikipedia

Why Zephyr on Arduino?

Arduino boards are widely used in prototyping and embedded applications, and Zephyr brings an additional layer of robustness and flexibility.

But how does Zephyr OS fit into the Arduino ecosystem?

In this getting started guide, we’ll explore Zephyr’s direct support for Arduino boards and how to integrate it into the traditional Arduino ecosystem development flow.

Press enter or click to view image in full size
Arduino ❀ Zephyr. Image by author.

Zephyr OS and Arduino: Compatibility Overview

There are two main approaches to use Zephyr OS with Arduino:

  1. Direct Zephyr OS Support for Arduino Boards: Some Arduino boards, like the Portenta H7 and Nano 33 BLE, have direct Zephyr support. In this approach, Zephyr fully controls the board, bypassing the traditional Arduino ecosystem, and developers work directly with Zephyr’s features.
  2. ArduinoCore Integration with Zephyr: In this approach, Zephyr is integrated transparently into the classic Arduino experience. Zephyr runs in the background, providing real-time capabilities, power management, and other advanced features, while still allowing developers to use the familiar Arduino IDE and libraries.

In the following sections, we will explore how to program an Arduino board using both approaches.

Press enter or click to view image in full size
Direct Support vs Arduino Integration. Image by author.

Direct Support: Setting Up Zephyr for Arduino Boards

To set up Zephyr for Arduino boards, you need to install the Zephyr development environment, which includes prerequisites like Python, CMake, and West (Zephyr Meta Tool). You’ll also need to install the Zephyr SDK and toolchain, and clone the Zephyr repository.

For detailed steps on setting up the Zephyr development environment, refer to the Zephyr Getting Started Guide.

For configuring Zephyr with an Arduino board, we will be working on the Arduino UNO R4 WiFi board. You can select it, configure it using the west tool, and build a sample application. Flashing and debugging can then be done directly using the west tool.

For more details on configuring Zephyr for the Arduino UNO R4 WiFi, check the Arduino Uno R4 WiFi Zephyr Documentation.

Press enter or click to view image in full size
Arduino UNO R4 WiFi. Source: Arduino Store.

First Zephyr Application on Arduino

Zephyr comes equipped with a variety of samples and demos that you can use as starting points for your projects. These examples cover different aspects of Zephyr’s functionality and can help you get up and running quickly. You can explore all available samples in the Zephyr Samples Documentation.

For simplicity, we will start with the Blinky sample, which is a basic example where the board’s built-in LED blinks forever using the GPIO peripheral. In addition to controlling the LED, this example also prints the LED’s state to the serial console, making it a great way to familiarize yourself with the basic functionalities of GPIO and UART.

You can build and flash an application using the west tool. Here’s an example for building and flashing the Blinky application on the Arduino UNO R4 WiFi:

# From the root of the Zephyr repository 
west build -b arduino_uno_r4_wifi samples/basic/blinky
west flash

Once the application is flashed, the built-in LED will start blinking with a frequency of 1 second, and the LED’s state will be shown on the serial interface. However, since Zephyr does not use the typical USB serial port for output (like the Arduino ecosystem does), you will need to use an FTDI USB-to-UART device connected to pins D0 and D1 of the board. The serial output will display the LED state as LED state: ON and LED state: OFF.

Press enter or click to view image in full size
Arduino UNO R4 WiFI — UART & Built-in LED Pinout. Image by author.

To read the serial print, connect the FTDI device to your computer and open a serial logger with a baud rate of 115200. For example, you can use Minicom. Use the following command to open the serial connection on device /dev/ttyACM0 (adjust the device name based on your system):

minicom -D /dev/ttyACM0 -b 115200

The Blinky program

Here is the Blinky program with comments on the main parts. The application uses key Zephyr APIs to interact with the GPIO and Console.

#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/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)

/* Retrieve the GPIO device specification for the "led0" alias from the device tree */
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

int main(void)
{
int ret;
bool led_state = true;

/* Check if the GPIO device is ready for use */
if (!gpio_is_ready_dt(&led)) {
return 0;
}

/* Configure the GPIO pin as an output and set it to active (LED ON) */
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
return 0;
}

/* Toggle the LED state in an infinite loop */
while (1) {
/* Toggle the LED (ON -> OFF or OFF -> ON) */
ret = gpio_pin_toggle_dt(&led);
if (ret < 0) {
return 0;
}

/* Update the LED state and print it to the serial console */
led_state = !led_state;
printf("LED state: %s\n", led_state ? "ON" : "OFF");

/* Sleep for 1 second before toggling the LED again */
k_msleep(SLEEP_TIME_MS);
}
return 0;
}

It’s easy to see that the program is very generic and works on all compatible boards without modifications.
But how do I know the hardware implementation for led0 and the UART used by printf? This is discovered through the Device Tree.

The Device Tree

The Device Tree is a data structure that describes the hardware configuration of a system, including details about GPIO pins, UART interfaces, and other peripherals. It is defined in a set of files that specify how hardware resources are mapped, allowing Zephyr to correctly configure and interact with them.

The Device Tree files for the Arduino UNO R4 WiFi board can be found here.

In the device tree file arduino_uno_r4_wifi.dts, the led0 is defined in the led node with the following configuration:

leds {
compatible = "gpio-leds";
led: led {
gpios = <&ioport1 2 GPIO_ACTIVE_HIGH>;
};
};

// ...

arduino_header: connector {
// ...
gpio-map = <0 0 &ioport0 14 0>, /* A0 */
<1 0 &ioport0 0 0>, /* A1 */
<2 0 &ioport0 1 0>, /* A2 */
<3 0 &ioport0 2 0>, /* A3 */
// ...
};

The LED is associated with a GPIO pin located on ioport1, pin number 2, (Connector Pin A3) and it is set to be active high.
In the aliases section, led0 is defined as a reference to this LED:

aliases {
led0 = &led;
};

As for the UART used for the console, the configuration can be found in the arduino_uno_r4_common.dtsi file, where the serial console is mapped to uart2 with a baud rate of 115200:

/ {
model = "Arduino Uno R4 Board";
compatible = "renesas,ra4m1", "renesas,ra";

chosen {
zephyr,console = &uart2; // <-- UART used for Console
zephyr,shell-uart = &uart2;
zephyr,sram = &sram0;
zephyr,flash = &flash0;
zephyr,code-partition = &code_partition;
};
};

&sci2 {
status = "okay";
pinctrl-0 = <&sci2_default>;
pinctrl-names = "default";
interrupts = <4 1>, <5 1>, <6 1>, <7 1>;
interrupt-names = "rxi", "txi", "tei", "eri";

uart2: uart {
current-speed = <115200>; // <-- UART2 Baudate
status = "okay";
};
};

Using Zephyr with the Arduino Ecosystem

The ArduinoCore for ZephyrOS is an implementation of the Arduino core that leverages the full potential of ZephyrOS. This integration allows developers to take advantage of Zephyr’s advanced features while maintaining the simplicity and ease of use of the Arduino development process.

Installation and Board Setup

⚙ To get started, install the core and toolchain via the Board Manager in the Arduino IDE or CLI. For detailed installation instructions, refer to the official README.

đŸ—ïž After installation, before loading your first sketch, use the Tools → Burn Bootloader option in the IDE or CLI. However, it’s important to note that this process doesn’t install a traditional Arduino bootloader. Instead, what you’re installing is the Zephyr loader, a special component that manages the interaction between your sketch and the ZephyrOS.

How Zephyr Sketches Work

Unlike traditional Arduino sketches, Zephyr sketches are compiled into ELF files and dynamically loaded by the precompiled Zephyr loader firmware. This approach leads to faster compilation times and smaller binary files, as only the user-specific code and libraries are compiled, while ZephyrOS itself remains precompiled.

The Blink program

Now, let’s test our first sketch using the classic Blink program found in the File → Examples → Basics, without any modifications.

/*
Blink
Turns an LED on for one second, then off for one second, repeatedly.

https://www.arduino.cc/en/Tutorial/BuiltInExamples/Blink
*/

// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}

As we can see, the process doesn’t change from the typical Arduino flow. You simply verify and upload the sketch to the board using the usual procedure.

However, what we can observe is the efficiency of the sketch when compiled with Zephyr compared to the traditional board core.
For instance, compiling the Blink sketch for an Arduino GIGA R1 board, we get the following results:

Compiled with Zephyr

Sketch uses 2860 bytes (0%) of program storage space. Maximum is 786432 bytes.
Global variables use 3312 bytes (0%) of dynamic memory, leaving 520312 bytes for local variables. Maximum is 523624 bytes.

Compiled with the traditional board core (ArduinoCore-mbed)

Sketch uses 110736 bytes (5%) of program storage space. Maximum is 1966080 bytes.
Global variables use 47520 bytes (9%) of dynamic memory, leaving 476104 bytes for local variables. Maximum is 523624 bytes.

As seen from the comparison, compiling with Zephyr results in much smaller binary files (2860 bytes vs 110736 bytes), reflecting its efficient handling of the code. This is one of the key advantages of using Zephyr for Arduino, offering faster compilation times and smaller program sizes while retaining the simplicity of Arduino development.

Press enter or click to view image in full size
Arduino Zephyr Board Core. Image by author.

Expanding Your Zephyr Knowledge

Zephyr OS brings real-time capabilities and scalability to the Arduino world, enabling developers to create more complex and efficient embedded applications. Whether you’re transitioning from the traditional Arduino workflow or diving straight into RTOS-based development, Zephyr offers a powerful tool for your IoT projects.

For more in-depth information, check out these resources:

--

--

Leonardo Cavagnis
Leonardo Cavagnis

Written by Leonardo Cavagnis

Passionate Embedded Software Engineer, IOT Enthusiast and Open source addicted. Proudly FW & DevRel @ Arduino