Embedded HandbookApps
All sections › IV. FreeRTOS
STM32F407, STM32G474 · level intermediate · 15 min read
Open in the interactive handbook

34а. Zephyr: a different operating system — an overview for those who know FreeRTOS

Sections 23–34 covered FreeRTOS: a kernel of a few .c files, configured with one header FreeRTOSConfig.h, with drivers of your own or from HAL. Zephyr is built differently. It is not a kernel but a whole tree: kernel, drivers, board descriptions, configuration and build — all together and all connected. This is an overview for those who already write for FreeRTOS: what Zephyr consists of, how it describes a board and chooses what goes into the firmware, what the familiar functions are called, and what it costs in bytes on the STM32F407 and STM32G474.

Based on: the Zephyr 4.4.2 tree with modules on disk — the headers include/zephyr/kernel.h, drivers/gpio.h, device.h, the board descriptions in boards/st/ and the documentation in doc/kernel/; the compiler from Zephyr SDK 1.0.1 (gcc 14.3.0). The samples samples/basic/blinky and samples/synchronization are built with west build for the boards stm32f4_disco and nucleo_g474re, plus two variants of blinky and a file of our own that calls every name from table 34а.5. All numbers come from these builds. The firmware was not run on boards.

Terms used in the documentation

Term Meaning
devicetree, .dts, .dtsi a text description of the board and the chip
overlay a change to the tree from the project, on top of the board files
node, property, label, alias the elements of a devicetree
compatible, binding the "compatible with which driver" string and the schema of a node's properties
status = "okay" the node is enabled; "disabled" — described but not used
Kconfig, prj.conf, defconfig the system of CONFIG_… symbols: what to build and with which numbers
board, SoC the board and the chip
thread; cooperative, preemptible a thread that is never preempted / one that can be
message queue (k_msgq) a queue of fixed-size messages
work queue (k_work) functions run by a kernel thread
west repository and build manager (section 115)

34а.1 What it consists of

FreeRTOS is a kernel (24.1): a scheduler and objects, and you write everything else. Zephyr brings everything with it at once:

The price of this completeness is disk space: the working tree with modules takes 6.0 GB, the SDK with the compiler another 1.5 GB, the Python environment for west 0.6 GB. In FreeRTOS the largest kernel file is tasks.c at 218 KB (24.1).

Building blinky in Zephyr for STM32F4DISCOVERY: devicetree from the board file, eight .dtsi files and the project overlay gives a header with 168 nodes, 93 of them enabled; Kconfig from the board defconfig, all subsystems and prj.conf gives a .config with 369 symbols, 291 of them set to y; together with main.c, the kernel and the drivers west builds in 153 steps a zephyr.elf image of 17108 bytes of Flash and 4480 bytes of RAM with 13 devices; without the console — 15224 bytes and 11 devices, for NUCLEO-G474RE — 17864 bytes


34а.2 Devicetree: the board is described in text

In FreeRTOS only your code knows the LED pin: GPIOD, pin 12. In Zephyr the board is described in a separate file, and the code asks the description. The STM32F4DISCOVERY LEDs in boards/st/stm32f4_disco/stm32f4_disco.dts:

    leds {
        compatible = "gpio-leds";

        orange_led_3: led_3 {
            gpios = <&gpiod 13 GPIO_ACTIVE_HIGH>;
            label = "User LD3";
        };

        green_led_4: led_4 {
            gpios = <&gpiod 12 GPIO_ACTIVE_HIGH>;
            label = "User LD4";
        };

        red_led_5: led_5 {
            gpios = <&gpiod 14 GPIO_ACTIVE_HIGH>;
            label = "User LD5";
        };

        blue_led_6: led_6 {
            gpios = <&gpiod 15 GPIO_ACTIVE_HIGH>;
            label = "User LD6";
        };
    };

Further down in the same file is the alias led0 = &green_led_4; — the blinky sample finds the LED by it, not by the pin name. The board file includes the chip description, and that one includes the series and core descriptions: for the F407 9 .dts and .dtsi files are combined, plus 26 headers with constants. The result is 168 nodes, 93 of them with status = "okay"; for NUCLEO-G474RE it is 170 and 94.

The layers modify each other. In the series description stm32f4.dtsi USART2 exists but is disabled:

        usart2: serial@40004400 {
            compatible = "st,stm32-usart", "st,stm32-uart";
            reg = <0x40004400 0x400>;
            clocks = <&rcc STM32_CLOCK(APB1, 17)>;
            resets = <&rctl STM32_RESET(APB1, 17)>;
            interrupts = <38 0>;
            status = "disabled";
        };

The board file enables it and assigns the pins:

&usart2 {
    pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>;
    pinctrl-names = "default";
    current-speed = <115200>;
    status = "okay";
};

In the same way a project modifies the board without touching the Zephyr tree — with an overlay. The file blue_led.overlay moves led0 to the blue LED:

/* Раздел 34а: светодиод примера blinky на STM32F4DISCOVERY — синий LD6 (PD15) вместо зелёного LD4 */

/ {
    aliases {
        led0 = &blue_led_6;
    };
};

The build with -DEXTRA_DTC_OVERLAY_FILE=blue_led.overlay took 10 input files instead of 9, and led0 points to /leds/led_6 — port gpiod, pin 15. The image size did not change by a single byte: 17108 bytes — only the pin number in the constants changed. The blinky code is the same.

The tree is turned into a generated header devicetree_generated.h with macros — not into structures in memory. DT_ALIAS(led0) and GPIO_DT_SPEC_GET become constants at compile time; an error in the description is a build error.


34а.3 Kconfig: what goes into the image

The handbook's check FreeRTOSConfig.h has 45 lines of #define config… and INCLUDE_… (section 33). In Zephyr Kconfig does the same, and it has more than eight times as many symbols. The configuration is assembled from three layers: the defaults of all subsystems, the board defconfig and the project prj.conf. The defconfig of STM32F4DISCOVERY:

# SPDX-License-Identifier: Apache-2.0

# Enable MPU
CONFIG_ARM_MPU=y

# Enable HW stack protection
CONFIG_HW_STACK_PROTECTION=y

CONFIG_SERIAL=y

# console
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y

# enable GPIO
CONFIG_GPIO=y

The prj.conf of the blinky sample is a single line CONFIG_GPIO=y. After the build .config has 369 symbols with a value, 291 of them enabled (=y), and another 453 explicitly not selected. The numbers that you set yourself in FreeRTOS come here as defaults:

Symbol Value In FreeRTOS
CONFIG_SYS_CLOCK_TICKS_PER_SEC 10000 configTICK_RATE_HZ, 1000 in the handbook (33.1)
CONFIG_TICKLESS_KERNEL y tickless mode, off by default (33.5)
CONFIG_TIMESLICE_SIZE 20 ms the time slice is one tick (26.2)
CONFIG_NUM_PREEMPT_PRIORITIES 15 configMAX_PRIORITIES
CONFIG_NUM_COOP_PRIORITIES 16 no cooperative priorities
CONFIG_MAIN_STACK_SIZE 1024 bytes a task stack is in words (34.1)
CONFIG_ISR_STACK_SIZE 2048 bytes handlers run on the shared MSP stack (23.3)
CONFIG_HEAP_MEM_POOL_SIZE 0 the heap_4 heap from configTOTAL_HEAP_SIZE

Turning off the console in the project takes three lines in no_console.conf:

# Раздел 34а: blinky без консоли — Kconfig выбрасывает драйвер UART
CONFIG_SERIAL=n
CONFIG_CONSOLE=n
CONFIG_UART_CONSOLE=n

The number of symbols with a value became 350, the devices in the image 11 instead of 13 (USART2 and the reset controller are gone), Flash 15224 bytes instead of 17108: minus 1884. The drivers in the image now take 2952 bytes instead of 4606. Not a single line of code changed.


34а.4 The driver model: blinky line by line

The sample samples/basic/blinky/src/main.c, unchanged:

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

/*
 * A build error on this line means your board is unsupported.
 * See the sample documentation for information on how to fix this.
 */
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

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

    if (!gpio_is_ready_dt(&led)) {
        return 0;
    }

    ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
    if (ret < 0) {
        return 0;
    }

    while (1) {
        ret = gpio_pin_toggle_dt(&led);
        if (ret < 0) {
            return 0;
        }

        led_state = !led_state;
        printf("LED state: %s\n", led_state ? "ON" : "OFF");
        k_msleep(SLEEP_TIME_MS);
    }
    return 0;
}

What differs from FreeRTOS with HAL:


34а.5 FreeRTOS → Zephyr mapping table

Every Zephyr name in the table is called in api34a/src/main.c and built for stm32f4_disco without warnings: a typo would be a build error, as in the check file of section 34. The FreeRTOS names come from the tables of section 34.

FreeRTOS Zephyr Difference
xTaskCreateStatic K_THREAD_DEFINE the thread and its stack are created at compile time, the thread starts by itself
xTaskCreate k_thread_create the stack is your own K_THREAD_STACK_DEFINE array; the size is in bytes
vTaskDelete k_thread_abort
vTaskSuspend, vTaskResume k_thread_suspend, k_thread_resume
vTaskPrioritySet k_thread_priority_set a smaller number is more important; a negative one means a cooperative thread
uxTaskGetStackHighWaterMark k_thread_stack_space_get in bytes; needs CONFIG_INIT_STACKS and CONFIG_THREAD_STACK_INFO
vTaskDelay k_msleep, k_sleep time in milliseconds, K_MSEC rounds up
taskYIELD k_yield
xTaskGetTickCount k_uptime_get milliseconds, 64 bits
xQueueSend, xQueueReceive, xQueuePeek k_msgq_put, k_msgq_get, k_msgq_peek the queue is created by K_MSGQ_DEFINE; it also copies
uxQueueMessagesWaiting k_msgq_num_used_get
xSemaphoreTake, xSemaphoreGive k_sem_take, k_sem_give K_SEM_DEFINE(name, initial, limit) — binary and counting in one
uxSemaphoreGetCount k_sem_count_get
mutex: xSemaphoreTake, xSemaphoreGive k_mutex_lock, k_mutex_unlock K_MUTEX_DEFINE; the same thread may lock it again right away — there is no separate recursive mutex
xTimerStart, xTimerStop k_timer_start, k_timer_stop K_TIMER_DEFINE; the callback runs in the interrupt of the system timer, not in a daemon task
xTimerPendFunctionCall k_work_submit K_WORK_DEFINE; the function is run by the thread of the system work queue
xEventGroupSetBits, xEventGroupClearBits k_event_post, k_event_clear K_EVENT_DEFINE, CONFIG_EVENTS; 32 bits, not 24 (31.1)
xEventGroupWaitBits k_event_wait, k_event_wait_all "any" and "all" are two functions instead of an argument
xTaskNotify k_event_post, k_event_set a thread has no notification word of its own: you use an event (set replaces the bits, post adds them) or a semaphore
— k_poll, k_poll_signal_raise wait for several objects at once; in FreeRTOS the sources are merged into one queue (27.4)
xStreamBufferSend, xStreamBufferReceive k_pipe_write, k_pipe_read K_PIPE_DEFINE
vTaskSuspendAll, xTaskResumeAll k_sched_lock, k_sched_unlock
taskENTER_CRITICAL, taskEXIT_CRITICAL irq_lock, irq_unlock k_spin_lock, k_spin_unlock — the same, also on several cores
pvPortMalloc, vPortFree k_malloc, k_free the heap is CONFIG_HEAP_MEM_POOL_SIZE, 0 by default
vTaskStartScheduler — the kernel is started before main

Three differences people trip over most often:


34а.6 Cost in bytes and when to use which

Sizes from the linker output of west build:

Build Board Flash RAM
blinky STM32F4DISCOVERY 17108 4480
blinky NUCLEO-G474RE 17864 4480
blinky + overlay blue_led.overlay STM32F4DISCOVERY 17108 4480
blinky + no_console.conf STM32F4DISCOVERY 15224 4480
synchronization: two threads, two semaphores STM32F4DISCOVERY 18204 7104
synchronization: two threads, two semaphores NUCLEO-G474RE 18960 7104
api34a: all names of 34а.5 STM32F4DISCOVERY 24804 10108

Flash of blinky for the F407 by tree directory — the standard target rom_report:

Part blinky without console
drivers drivers/ 4606 2952
kernel kernel/ 3308 3308
architecture arch/ 2576 2576
chip soc/ 1036 896
module hal_stm32 334 316
the program itself samples/ 116 116
lib/, include/, subsys/ 490 454
symbols without a source path 4634 4598
total 17100 15216

The sum is 8 bytes less than the linker's: rom_report adds up symbols, not the whole region.

The kernel/ part in blinky is 3308 bytes: scheduler, tick and startup; the program does not call queues or timers, and they are not in the image. When all the services of table 34а.5 are used, kernel/ grows to 9428 bytes, and the whole image to 24804 bytes of Flash and 10108 bytes of RAM. For comparison, the FreeRTOS kernel in the handbook is about 6.4 KB (23.4), but that is a different compiler and Cortex-M3.

Kernel objects in the same build (sizeof, Cortex-M4) next to the costs from 34.9 (Cortex-M3):

Object Zephyr, bytes FreeRTOS, bytes
thread / task control block 136 92
queue k_msgq 48 80
semaphore k_sem 24 80
mutex k_mutex 20 80
timer k_timer 56 44
events k_event 12 32
pipe k_pipe / stream buffer 52 36
work item k_work 16 —

The size of k_thread depends on the configuration: here CONFIG_THREAD_STACK_INFO and CONFIG_INIT_STACKS are enabled.

When Zephyr is justified. The device needs networking, Bluetooth, a file system, USB — in the tree these are ready subsystems in subsys/ and drivers, not your own work. One firmware runs on several boards or chips: the board description changes, the code does not (blinky built for F407 and G474 without changes). The team needs a single build and configuration for all products.

When FreeRTOS is simpler. One board, your own drivers on LL or registers, a few tasks and queues — a kernel of a few files, configuration in one header, everything visible in the debugger and nothing generated. The project is already on CubeMX and HAL. You need to understand every byte of the firmware and build it without gigabytes of tools.


34а.7 Pitfalls

Priority "upside down". A thread with priority 10 was ported from FreeRTOS as the most important — in Zephyr it is lower than a thread with priority 5. A smaller number is more important; a negative one means a cooperative thread that is not preempted.

Stack in words. xTaskCreate measures the stack in words, K_THREAD_DEFINE and k_thread_create in bytes. You carried over "512" — and the stack became four times smaller.

A timer callback waits on a semaphore. k_timer calls it from the system timer interrupt, not from a daemon task as FreeRTOS does: you cannot wait there; move long work to k_work_submit.

k_malloc returns NULL. The heap CONFIG_HEAP_MEM_POOL_SIZE is 0 by default — set it in prj.conf.

k_event_post does not build. Events and k_poll are disabled by default: you need CONFIG_EVENTS=y and CONFIG_POLL=y.

The "wrong" LED on your own board. The code takes led0 from the devicetree: fix the alias with an overlay (34а.2), not the pin number in the code.

west build cannot find the compiler of SDK 1.0.1. The cmake files of this SDK look for the compiler in <sdk>/gnu/, but it lives in <sdk>/arm-zephyr-eabi. What helped here was the variant ZEPHYR_TOOLCHAIN_VARIANT=cross-compile with CROSS_COMPILE pointing to the same gcc 14.3.0 — the tools directory was not touched.

west build on Windows refuses to build a project on another drive. The Zephyr tree on D:, the project on C: — west fails in os.path.relpath. Our own project was built with the same steps directly: cmake -G Ninja -DBOARD=… and ninja.


34а.8 Summary

Next: section 35 "Debugging".

Open in the interactive handbook