Embedded HandbookПриложения
Все разделы › IV. FreeRTOS
STM32F407, STM32G474 · уровень средний · 15 мин чтения
Открыть в интерактивном справочнике

34а. Zephyr: другая операционная система — обзор для того, кто знает FreeRTOS

Разделы 23–34 разобрали FreeRTOS: ядро из нескольких файлов .c, настройка одним заголовком FreeRTOSConfig.h, драйверы — свои или HAL. Zephyr устроен иначе. Это не ядро, а целое дерево: ядро, драйверы, описание плат, конфигурация и сборка — всё вместе и всё связано. Здесь — обзор для того, кто уже пишет под FreeRTOS: из чего состоит Zephyr, как в нём описывают плату и выбирают, что попадёт в прошивку, как называются знакомые функции и сколько это стоит в байтах на STM32F407 и STM32G474.

Опоры: дерево Zephyr 4.4.2 с модулями на диске — заголовки include/zephyr/kernel.h, drivers/gpio.h, device.h, описания плат boards/st/ и документация doc/kernel/; компилятор из Zephyr SDK 1.0.1 (gcc 14.3.0). Примеры samples/basic/blinky и samples/synchronization собраны west build для плат stm32f4_disco и nucleo_g474re, к ним — два варианта blinky и свой файл, в котором вызвано каждое имя таблицы 34а.5. Все числа — из этих сборок. На платах прошивки не запускались.

Как это называется в документации

Оригинал Перевод
devicetree, .dts, .dtsi дерево устройств: описание платы и кристалла текстом
overlay наложение: правка дерева из проекта, поверх файлов платы
node, property, label, alias узел, свойство, метка, псевдоним
compatible, binding строка «с каким драйвером совместим» и схема свойств узла
status = "okay" узел включён; "disabled" — описан, но не используется
Kconfig, prj.conf, defconfig система символов CONFIG_…: что собрать и с какими числами
board, SoC плата, кристалл
thread; cooperative, preemptible поток; кооперативный, вытесняемый
message queue (k_msgq) очередь сообщений
work queue (k_work) очередь работ: функции, выполняемые потоком ядра
west менеджер репозиториев и сборки (раздел 115)

34а.1 Из чего состоит

FreeRTOS — это ядро (24.1): планировщик и объекты, а всё остальное пишете вы. Zephyr приносит с собой сразу всё:

Цена этой полноты — объём на диске: рабочее дерево с модулями занимает 6,0 ГБ, SDK с компилятором — ещё 1,5 ГБ, окружение Python для west — 0,6 ГБ. У FreeRTOS самый большой файл ядра — tasks.c на 218 КБ (24.1).

Сборка blinky в Zephyr для STM32F4DISCOVERY: devicetree из файла платы, восьми файлов .dtsi и наложения проекта даёт заголовок с 168 узлами, из них 93 включены; Kconfig из defconfig платы, всех подсистем и prj.conf даёт .config с 369 символами, из них 291 со значением y; вместе с main.c, ядром и драйверами west собирает за 153 шага образ zephyr.elf на 17 108 байт флеш-памяти и 4480 байт ОЗУ с 13 устройствами; без консоли — 15 224 байта и 11 устройств, для NUCLEO-G474RE — 17 864 байта


34а.2 Devicetree: плата описана текстом

Во FreeRTOS о выводе светодиода знает только ваш код: GPIOD, вывод 12. В Zephyr плата описана отдельным файлом, и код спрашивает у описания. Светодиоды STM32F4DISCOVERY в 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";
        };
    };

Ниже в том же файле псевдоним led0 = &green_led_4; — пример blinky ищет светодиод по нему, не по имени вывода. Файл платы подключает описание кристалла, а то — описание серии и ядра: для F407 собрано 9 файлов .dts и .dtsi, плюс 26 заголовков с константами. Итог — 168 узлов, из них 93 со status = "okay"; у NUCLEO-G474RE — 170 и 94.

Слои правят друг друга. В описании серии stm32f4.dtsi USART2 есть, но выключен:

        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";
        };

Файл платы его включает и назначает выводы:

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

Так же проект правит плату, не трогая дерево Zephyr, — наложением. Файл blue_led.overlay перевешивает led0 на синий светодиод:

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

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

Сборка с -DEXTRA_DTC_OVERLAY_FILE=blue_led.overlay взяла 10 входных файлов вместо 9, и led0 указывает на /leds/led_6 — порт gpiod, вывод 15. Размер образа не изменился ни на байт: 17 108 байт — поменялся только номер вывода в константах. Код blinky тот же.

Из дерева генерируется заголовок devicetree_generated.h с макросами — не структуры в памяти. DT_ALIAS(led0) и GPIO_DT_SPEC_GET превращаются в константы при компиляции; ошибка в описании — ошибка сборки.


34а.3 Kconfig: что попадёт в образ

Проверочный FreeRTOSConfig.h справочника — 45 строк #define config… и INCLUDE_… (раздел 33). В Zephyr то же делает Kconfig, и символов в нём в восемь с лишним раз больше. Настройка собирается из трёх слоёв: умолчания всех подсистем, defconfig платы и prj.conf проекта. defconfig 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

prj.conf примера blinky — одна строка CONFIG_GPIO=y. После сборки в .config 369 символов со значением, из них 291 включены (=y), и ещё 453 явно не выбраны. Числа, которые во FreeRTOS вы задаёте сами, здесь пришли по умолчанию:

Символ Значение Во FreeRTOS
CONFIG_SYS_CLOCK_TICKS_PER_SEC 10 000 configTICK_RATE_HZ, в справочнике 1000 (33.1)
CONFIG_TICKLESS_KERNEL y режим без тика, по умолчанию выключен (33.5)
CONFIG_TIMESLICE_SIZE 20 мс квант — один тик (26.2)
CONFIG_NUM_PREEMPT_PRIORITIES 15 configMAX_PRIORITIES
CONFIG_NUM_COOP_PRIORITIES 16 кооперативных приоритетов нет
CONFIG_MAIN_STACK_SIZE 1024 байта стек задачи — в словах (34.1)
CONFIG_ISR_STACK_SIZE 2048 байт обработчики — на общем стеке MSP (23.3)
CONFIG_HEAP_MEM_POOL_SIZE 0 куча heap_4 из configTOTAL_HEAP_SIZE

Выключить в проекте консоль — три строки no_console.conf:

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

Символов со значением стало 350, устройств в образе — 11 вместо 13 (ушли USART2 и контроллер сброса), флеш-памяти — 15 224 байта вместо 17 108: минус 1884. Драйверов в образе стало 2952 байта вместо 4606. Ни одной строки кода не поменялось.


34а.4 Драйверная модель: blinky построчно

Пример samples/basic/blinky/src/main.c — без изменений:

#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;
}

Что здесь не так, как во FreeRTOS с HAL:


34а.5 Таблица соответствий FreeRTOS → Zephyr

Каждое имя Zephyr из таблицы вызвано в api34a/src/main.c и собрано для stm32f4_disco без предупреждений: опечатка была бы ошибкой сборки, как в проверочном файле раздела 34. Имена FreeRTOS — из таблиц раздела 34.

FreeRTOS Zephyr Чем отличается
xTaskCreateStatic K_THREAD_DEFINE поток и стек заводятся при компиляции, поток стартует сам
xTaskCreate k_thread_create стек — ваш массив K_THREAD_STACK_DEFINE; размер в байтах
vTaskDelete k_thread_abort
vTaskSuspend, vTaskResume k_thread_suspend, k_thread_resume
vTaskPrioritySet k_thread_priority_set меньше число — важнее; отрицательный — кооперативный поток
uxTaskGetStackHighWaterMark k_thread_stack_space_get в байтах; нужны CONFIG_INIT_STACKS и CONFIG_THREAD_STACK_INFO
vTaskDelay k_msleep, k_sleep время в миллисекундах, K_MSEC округляет вверх
taskYIELD k_yield
xTaskGetTickCount k_uptime_get миллисекунды, 64 бита
xQueueSend, xQueueReceive, xQueuePeek k_msgq_put, k_msgq_get, k_msgq_peek очередь заводит K_MSGQ_DEFINE; тоже копирует
uxQueueMessagesWaiting k_msgq_num_used_get
xSemaphoreTake, xSemaphoreGive k_sem_take, k_sem_give K_SEM_DEFINE(имя, начальное, предел) — двоичный и считающий разом
uxSemaphoreGetCount k_sem_count_get
мьютекс: xSemaphoreTake, xSemaphoreGive k_mutex_lock, k_mutex_unlock K_MUTEX_DEFINE; повторный захват тем же потоком разрешён сразу — отдельного рекурсивного нет
xTimerStart, xTimerStop k_timer_start, k_timer_stop K_TIMER_DEFINE; обратный вызов — в прерывании системного таймера, не в задаче-демоне
xTimerPendFunctionCall k_work_submit K_WORK_DEFINE; функцию выполнит поток системной очереди работ
xEventGroupSetBits, xEventGroupClearBits k_event_post, k_event_clear K_EVENT_DEFINE, CONFIG_EVENTS; 32 бита, а не 24 (31.1)
xEventGroupWaitBits k_event_wait, k_event_wait_all «любой» и «все» — две функции вместо аргумента
xTaskNotify k_event_post, k_event_set своего слова уведомлений у потока нет: берут событие (set заменяет биты, post добавляет) или семафор
— k_poll, k_poll_signal_raise ждать сразу несколько объектов; во FreeRTOS источники сводят в одну очередь (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 — то же и на нескольких ядрах
pvPortMalloc, vPortFree k_malloc, k_free куча CONFIG_HEAP_MEM_POOL_SIZE, по умолчанию 0
vTaskStartScheduler — ядро запущено до main

Три отличия, на которых спотыкаются чаще всего:


34а.6 Цена в байтах и когда что брать

Размеры из вывода компоновщика west build:

Сборка Плата Флеш-память ОЗУ
blinky STM32F4DISCOVERY 17 108 4480
blinky NUCLEO-G474RE 17 864 4480
blinky + наложение blue_led.overlay STM32F4DISCOVERY 17 108 4480
blinky + no_console.conf STM32F4DISCOVERY 15 224 4480
synchronization: два потока, два семафора STM32F4DISCOVERY 18 204 7104
synchronization: два потока, два семафора NUCLEO-G474RE 18 960 7104
api34a: все имена 34а.5 STM32F4DISCOVERY 24 804 10 108

Флеш-память blinky для F407 по каталогам дерева — штатная цель rom_report:

Часть blinky без консоли
драйверы drivers/ 4606 2952
ядро kernel/ 3308 3308
архитектура arch/ 2576 2576
кристалл soc/ 1036 896
модуль hal_stm32 334 316
сама программа samples/ 116 116
lib/, include/, subsys/ 490 454
символы без пути к исходнику 4634 4598
всего 17 100 15 216

Сумма на 8 байт меньше, чем у компоновщика: rom_report складывает символы, а не всю область.

Ядро kernel/ в blinky — 3308 байт: планировщик, тик и старт; очередей и таймеров программа не зовёт, и их нет в образе. Когда в дело идут все службы таблицы 34а.5, kernel/ вырастает до 9428 байт, а весь образ — до 24 804 байта флеш-памяти и 10 108 байт ОЗУ. Для сравнения, ядро FreeRTOS в справочнике — около 6,4 КБ (23.4), но это другой компилятор и Cortex-M3.

Объекты ядра в той же сборке (sizeof, Cortex-M4) рядом с ценой из 34.9 (Cortex-M3):

Объект Zephyr, байт FreeRTOS, байт
поток / блок управления задачей 136 92
очередь k_msgq 48 80
семафор k_sem 24 80
мьютекс k_mutex 20 80
таймер k_timer 56 44
события k_event 12 32
канал k_pipe / потоковый буфер 52 36
работа k_work 16 —

Размер k_thread зависит от конфигурации: здесь включены CONFIG_THREAD_STACK_INFO и CONFIG_INIT_STACKS.

Когда Zephyr оправдан. Прибору нужна сеть, Bluetooth, файловая система, USB — в дереве это готовые подсистемы subsys/ и драйверы, а не своя работа. Одна прошивка идёт на несколько плат или кристаллов: меняется описание платы, код — нет (blinky собрался для F407 и G474 без правок). Команде нужна единая сборка и конфигурация для всех изделий.

Когда проще FreeRTOS. Одна плата, свои драйверы на LL или регистрах, несколько задач и очередей — ядро из нескольких файлов, конфигурация в одном заголовке, всё видно в отладчике и ничего не генерируется. Проект уже на CubeMX и HAL. Нужно понимать каждый байт прошивки и собирать её без гигабайтов инструментов.


34а.7 Грабли

Приоритет «наоборот». Поток с приоритетом 10 перенесли из FreeRTOS как самый важный — в Zephyr он младше потока с приоритетом 5. Меньше число — важнее; отрицательное — кооперативный поток, который не вытесняют.

Стек в словах. xTaskCreate меряет стек в словах, K_THREAD_DEFINE и k_thread_create — в байтах. Перенесли «512» — стека стало вчетверо меньше.

Обратный вызов таймера ждёт семафор. k_timer зовёт его из прерывания системного таймера, а не из задачи-демона, как FreeRTOS: ждать там нельзя, долгую работу — в k_work_submit.

k_malloc возвращает NULL. Куча CONFIG_HEAP_MEM_POOL_SIZE по умолчанию 0 — её заводят в prj.conf.

k_event_post не собирается. События и k_poll выключены по умолчанию: нужны CONFIG_EVENTS=y и CONFIG_POLL=y.

Светодиод «не тот» на своей плате. Код берёт led0 из devicetree: правят псевдоним наложением (34а.2), а не номер вывода в коде.

west build не находит компилятор SDK 1.0.1. Файлы cmake этого SDK ищут компилятор в <sdk>/gnu/, а он лежит в <sdk>/arm-zephyr-eabi. Здесь помог вариант ZEPHYR_TOOLCHAIN_VARIANT=cross-compile с CROSS_COMPILE на тот же gcc 14.3.0 — каталог инструментов не трогали.

west build на Windows отказывается собирать проект с другого диска. Дерево Zephyr на D:, проект на C: — west падает на os.path.relpath. Свой проект собран теми же шагами напрямую: cmake -G Ninja -DBOARD=… и ninja.


34а.8 Итоги

Дальше: раздел 35 «Отладка».

Открыть в интерактивном справочнике