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 приносит с собой сразу всё:
- ядро
kernel/— планировщик, потоки, очереди, семафоры, таймеры; - архитектура
arch/— переключение контекста и прерывания Cortex-M, то же, чтоport.cу FreeRTOS; - кристаллы
soc/— код запуска и особенности каждой серии; - драйверы
drivers/— 95 каталогов, по одному на класс:gpio,serial,i2c,spi,can…; внутри драйверов STM32 — те же LL и HAL из модуляhal_stm32; - платы
boards/— 973 описания плат (файловboard.yml), из них 135 у ST; - devicetree — описание железа текстом (34а.2);
- Kconfig — выбор того, что попадёт в образ (34а.3);
- west — менеджер репозиториев и сборки (раздел 115): одной командой
west build -b <плата>собирается всё перечисленное.
Цена этой полноты — объём на диске: рабочее дерево с модулями занимает
6,0 ГБ, SDK с компилятором — ещё 1,5 ГБ, окружение Python для west —
0,6 ГБ. У FreeRTOS самый большой файл ядра — tasks.c на 218 КБ (24.1).
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:
struct gpio_dt_spec— три поля: указатель на устройство порта, номер вывода и флаги из описания.GPIO_DT_SPEC_GETзаполняет его из devicetree при компиляции. Для другой платы меняется описание, не код: та же программа для NUCLEO-G474RE собралась без правок, светодиод там —PA5.gpio_is_ready_dt— обёртка надdevice_is_ready(spec->port). Драйверы инициализирует ядро доmain;mainлишь проверяет, что порт поднялся.- Устройство — это узел дерева со
status = "okay"и драйвером. В образе blinky для F407 их 13: девять портов GPIO, RCC, контроллер сброса, EXTI и USART2 консоли. Порты A–I попали все, хотя мигает один вывод порта D: так включены узлы в описании кристалла. - Функции — общие для всех кристаллов.
gpio_pin_toggle_dtчерез таблицу функций драйвера доходит доgpio_stm32_port_toggle_bitsвdrivers/gpio/gpio_stm32.c— один драйвер на все серии STM32 — и та пишет вODRего прежнее значение, исключающее ИЛИ с маской. main— уже поток. Ядро запущено до неё;vTaskStartSchedulerнет. Стекmain—CONFIG_MAIN_STACK_SIZE, 1024 байта.printfуходит в консоль, которую выбрал devicetree:zephyr,console = &usart2в узлеchosenплаты.
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 |
Три отличия, на которых спотыкаются чаще всего:
- Двойников
FromISRнет. Из прерывания зовут ту жеk_sem_give,k_msgq_put, а ждать там нельзя — времяK_NO_WAIT(так сказано вkernel.h).portYIELD_FROM_ISRне нужен: если прерывание разбудило более важный поток, ядро переключится на него, когда прерывание закончится. Мьютекс из прерывания нельзя — как и во FreeRTOS. - Приоритеты перевёрнуты. Во FreeRTOS старше задача с большим числом (26.2), в Zephyr — с меньшим: 4 важнее 7, а −2 важнее обоих. Отрицательный приоритет — кооперативный поток: его не вытесняют, он работает, пока сам не отдаст процессор.
- Время — в миллисекундах, а не в тиках.
K_MSEC(n)пересчитывается в тики с округлением вверх;pdMS_TO_TICKSокругляет вниз (34.8).
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 Итоги
- Zephyr — ядро, драйверы (95 каталогов), 973 описания плат, devicetree, Kconfig и west в одном дереве; на диске 6,0 ГБ плюс SDK 1,5 ГБ.
- Devicetree описывает плату: для F407 — 9 файлов, 168 узлов, из них 93 включены; наложение перевесило светодиод без правки кода и без лишнего байта.
- Kconfig вместо
FreeRTOSConfig.h: 369 символов вместо 45; тик 10 000 Гц, режим без тика включён сразу. - blinky: 17 108 байт флеш-памяти и 4480 байт ОЗУ на F407, 17 864 на G474; без консоли — 15 224.
- Имена:
k_thread_create,k_msgq_*,k_sem_*,k_mutex_*,k_timer_*,k_event_*; двойниковFromISRнет, приоритеты перевёрнуты, стек в байтах. k_thread— 136 байт,k_msgq— 48,k_sem— 24.
Дальше: раздел 35 «Отладка».