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,.dtsia 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, bindingthe "compatible with which driver" string and the schema of a node's properties status = "okay"the node is enabled; "disabled"— described but not usedKconfig, prj.conf,defconfigthe system of CONFIG_…symbols: what to build and with which numbersboard, 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:
- kernel
kernel/— scheduler, threads, queues, semaphores, timers; - architecture
arch/— context switching and interrupts for Cortex-M, the same asport.cin FreeRTOS; - chips
soc/— startup code and the specifics of each series; - drivers
drivers/— 95 directories, one per class:gpio,serial,i2c,spi,can…; the STM32 drivers use the same LL and HAL inside, from thehal_stm32module; - boards
boards/— 973 board descriptions (board.ymlfiles), 135 of them from ST; - devicetree — a text description of the hardware (34а.2);
- Kconfig — the choice of what goes into the image (34а.3);
- west — repository and build manager (section 115): one command
west build -b <board>builds everything listed.
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).
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:
struct gpio_dt_spechas three fields: a pointer to the port device, the pin number and the flags from the description.GPIO_DT_SPEC_GETfills it from the devicetree at compile time. For another board the description changes, not the code: the same program for NUCLEO-G474RE built without changes, and the LED there isPA5.gpio_is_ready_dtis a wrapper overdevice_is_ready(spec->port). The kernel initializes the drivers beforemain;mainonly checks that the port came up.- A device is a tree node with
status = "okay"and a driver. The blinky image for the F407 has 13 of them: nine GPIO ports, RCC, the reset controller, EXTI and the console USART2. All ports A–I are included, although only one pin of port D blinks: that is how the nodes are enabled in the chip description. - The functions are common to all chips.
gpio_pin_toggle_dtgoes through the driver's function table togpio_stm32_port_toggle_bitsindrivers/gpio/gpio_stm32.c— one driver for all STM32 series — and that function writes toODRits previous value XORed with the mask. mainis already a thread. The kernel is started before it; there is novTaskStartScheduler. The stack ofmainisCONFIG_MAIN_STACK_SIZE, 1024 bytes.printfgoes to the console chosen by the devicetree:zephyr,console = &usart2in the board'schosennode.
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:
- There are no
FromISRtwins. From an interrupt you call the samek_sem_give,k_msgq_put, but you cannot wait there — the time isK_NO_WAIT(as stated inkernel.h).portYIELD_FROM_ISRis not needed: if the interrupt woke a more important thread, the kernel switches to it when the interrupt ends. A mutex cannot be used from an interrupt — just as in FreeRTOS. - Priorities are inverted. In FreeRTOS the task with the larger number has the higher priority (26.2), in Zephyr the one with the smaller number: 4 is more important than 7, and −2 is more important than both. A negative priority means a cooperative thread: it is not preempted and runs until it gives up the CPU by itself.
- Time is in milliseconds, not in ticks.
K_MSEC(n)is converted to ticks rounding up;pdMS_TO_TICKSrounds down (34.8).
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
- Zephyr is a kernel, drivers (95 directories), 973 board descriptions, devicetree, Kconfig and west in one tree; 6.0 GB on disk plus a 1.5 GB SDK.
- The devicetree describes the board: for the F407 — 9 files, 168 nodes, 93 of them enabled; an overlay moved the LED without a code change and without a single extra byte.
- Kconfig instead of
FreeRTOSConfig.h: 369 symbols instead of 45; a 10000 Hz tick, tickless mode enabled from the start. - blinky: 17108 bytes of Flash and 4480 bytes of RAM on the F407, 17864 on the G474; without the console, 15224.
- Names:
k_thread_create,k_msgq_*,k_sem_*,k_mutex_*,k_timer_*,k_event_*; there are noFromISRtwins, priorities are inverted, the stack is in bytes. k_threadis 136 bytes,k_msgq48,k_sem24.
Next: section 35 "Debugging".