Логирование является важной составной частью профессиональной отладки программ. Написав небольшой логгер и изучая, как он работает, можно лучше разобраться, как работает многозадачность в операционной системе.Из всех возможных вариантов самым удобным, с моей точки зрения, является UART, хотя это не самый быстрый вариант.Если у вас не отладочная плата Nucleo или Discovery, необходимо обзавестись UART-USB адаптером или программатором, который содержит в себе USB-UART адаптер. Оригинальные адаптеры содержат в себе этот интерфейс, но из китайских клонов можно использовать или Weact ST-Link или J-Link. Читать далее
Уровень сложностиСредний
Время на прочтение8 мин
Охват и читатели11K
Туториал

Логирование является важной составной частью профессиональной отладки программ. Написав небольшой логгер и изучая, как он работает, можно лучше разобраться, как работает многозадачность в операционной системе.
Из всех возможных вариантов вывода текста самым удобным, с моей точки зрения, является UART, хотя это не самый быстрый вариант.
Если у вас не отладочная плата Nucleo или Discovery, необходимо обзавестись UART-USB адаптером или программатором, который содержит в себе USB-UART адаптер. Оригинальные адаптеры содержат в себе этот интерфейс, но из китайских клонов можно использовать или Weact ST-Link или J-Link.
Исходный код логгера для микроконтроллера STM32H750 приведён в моём репозитории на GitHub.
Скучные примеры из книжек по FreeRTOS можно сделать гораздо интереснее, если найти прикладную задачу, в нашем случае — логгер. Я хочу убить двух зайцев: рассказать о том, как работать с операционной системой FreeRTOS на микроконтроллерах STM32 и как написать простейший логгер.
Подключаем FreeRTOSFreeRTOS — это операционная система реального времени, которая портирована на множество различных аппаратных платформ. В основном это микроконтроллеры с ограниченными ресурсами.
Когда вы пишете программы, для операционных систем Windows или Linux, операционная система является внешней по отношению к программе. В прошивке для микроконтроллера, работающей под операционной системой FreeRTOS, операционная система является частью прошивки.
FreeRTOS с точки зрения программиста — это несколько .c и .h файлов, в исходном коде проекта.
В FreeRTOS вся программа работает как один большой «процесс», а задачи — это потоки выполнения внутри него. У каждой задачи свой стек, свой контекст (регистры), но общая память. Поэтому ошибка в одной задаче (например, выход за границы массива) легко может «уронить» всю систему. В Linux/Windows процессы полностью изолированы друг от друга благодаря MMU (Memory Management Unit). Потоки внутри одного процесса уже разделяют память — и именно они ближе всего по смыслу к задачам FreeRTOS.
Если вы добавляете поддержку FreeRTOS в свой проект STM32 через STM32СubeMX, то в коде генерируются не вызовы функций FreeRTOS, а обёртки, которые являются функциями из CMSIS-RTOS.
Вместо прямых нативных вызовов FreeRTOS (например, xTaskCreate или vTaskDelay) STM32CubeMX генерирует функции-обёртки от ARM (например, osThreadNew или osDelay).
В проекте создаются файлы cmsis_os2.c / cmsis_os2.h (для CMSIS-RTOS V2). Внутри этих функций-обёрток как раз и вызываются нативные команды FreeRTOS.
Зачем STMicroelectronics это делает?
Портативность: вы можете легко заменить FreeRTOS на другую RTOS, поддерживающую стандарт CMSIS-RTOS, не меняя при этом код самого приложения.
Стандартизация: ARM продвигает CMSIS как единый стандарт для всех микроконтроллеров Cortex-M.
CMSIS чем-то напоминает стандарт POSIX для операционных систем.
Главная цель — обеспечить переносимость кода и избавить разработчиков от привязки к конкретной операционной системе.
Эксперимент с printf()В одной из статей, я уже рассказывал, как сделать вывод текста функцией printf(), используя UART на микроконтроллерах STM32. В этой же статье вы можете посмотреть, как создавать проекты для STM32.
Вывод при помощи printf() годится для простейших случаев, когда мы пишем bare metal прошивку без операционной системы. Если, например, в операционной системе FreeRTOS мы будем вызывать printf() из нескольких задач, то результат будет непредсказуемым. Далее мы рассмотрим, почему так происходит.
При помощи Stm32CubeMX создадим проект с FreeRTOS, в котором три задачи с одинаковым приоритетом, цикле выводят сообщения.
Активируем Serial Wire Debug:

Активируем UART:

Добавление UART может сбить настройки клоков. Поэтому разрешаем STM32CubeMX решить это за нас:

Добавляем поддержку FreeRTOS. И выбираем интерфейс CMSIS_V2:

Для корректной работы FreeRTOS нужно не забыть выбрать Timebase Source:

Создаём и редактируем определение задач FreeRTOS:


Подключаем отладочную плату. К пинам, которые используются UART, необходимо подключить или USB-UART адаптер или программатор, который поддерживает виртуальный COM-порт.
Запускаем терминал
sudo picocom --imap lfcrlf -b 115200 /dev/ttyACM0
где ttyACM0 имя виртуального COM-порта
Собираем и запускаем проект.
На выходе мы видим кашу.

Связано это с тем, что функция printf() из библиотеки newlib не является потокобезопасной. Когда она вызывается одновременно из нескольких потоков, рушатся её внутренние структуры.
Многие изучали операционные системы и имеют представление о вытесняющей (preemptive) и кооперативной (cooperative) многозадачности и как работает планировщик.
Также слышали и имеют базовое представление о синхронизирующих объектах в операционной системе, таких как мьютексы, семафоры и др.
Но если по отдельности всё понятно, то как работает планировщик совместно с примитивами — всё гораздо сложнее.
Вы можете написать неправильно программу, использующую многозадачность, и программа будет даже работать. Но при изменении настроек многозадачности или при другой частоте вывода или другом размере сообщений, программа у вас загадочным образом перестанет работать.
Вы знаете, что защитить совместный ресурс можно при помощи синхронизирующих конструкций, например, мьютекса.
Добавляем мьютекс:

Мьютекс гарантирует взаимное исключение: только та задача, которая захватила мьютекс, получает доступ к коду внутри мьютека, в то время как остальные задачи будут переведены в состояние ожидания (блокировки) при попытке взять этот же мьютекс.
Поэтому, используя мьютекс, можно потокобезопасно работать с функцией printf(). Создадим функцию safe_printf(), где перед вызовом printf() мы захватываем мьютекс, а после — освобождаем.
Запускаем. И что мы видим? Один из потоков монополизировал вывод, а остальные просто ничего вывести не могут.

Почему такое происходит? Мы же выбрали вытесняющую многозадачность, потоки имеют одинаковый приоритет, и даже включен Time Slicing!
Дело в том, что во FreeRTOS используется механизм «несправедливых» мьютексов. Когда поток освобождает мьютекс, поток всё ещё продолжает выполняться. И если в цикле он сразу же пытается захватить мьютекс снова, он делает это быстрее, чем планировщик успевает переключить контекст на другую задачу (даже при одинаковых приоритетах).
Захват уже захваченного мьютекса переводит задачу в состояние blocked. Задачи в состоянии blocked не участвуют в планировании. То есть может возникнуть ситуация, когда задача захватила мьютекс и выводит по UART. Вторая задача пытается захватить мьютекс, но он уже захвачен, поэтому задача переходит в состояние blocked.
Или более подробно:
Поток А освобождает мьютекс. В этот момент поток А всё ещё находится в состоянии Running (исполняется) и его квант времени ещё не истёк.
Поток А снова вызывает safe_printf и пытается захватить мьютекс.
Так как планировщик ещё не переключил контекст на потоки Б или В, поток А мгновенно забирает мьютекс обратно.
Добавим в конце тела цикла vTaskDelay(), и всё заработает как надо. Функция заблокирует текущий поток на время, дав возможность остальным задачам захватить освободившийся мьютекс.
Выводить информацию можно и прямо из нескольких задач, однако в многозадачных системах логгеры обычно устроены иначе. Отдельная задача-логгер постоянно читает сообщения из очереди и выводит их по UART, а все остальные задачи только кладут сообщения в эту очередь.
Помещение сообщения в очередь (когда она не заполнена) — очень быстрая операция по сравнению с передачей данных по UART. Благодаря этому логирование почти не влияет на тайминги основного кода приложения.
Рассмотрим создание такого логгера.
Логгер состоит из трёх основных функций:
Logger_Init() — инициализация логгера.
Logger_Log() — помещение сообщения в очередь.
vLoggerTask() — задача, которая читает сообщения из очереди и выводит их.
Чтобы логгер выводил время в микросекундах (момент, когда сообщение было помещено в очередь), я написал функцию Logger_GetMicroseconds().
Для получения времени в микросекундах нужно взять значение счётчика тактов процессора (DWT->CYCCNT), умножить значение на 1 000 000 и разделить на текущую тактовую частоту ядра.
Для работы со счётчиком необходимо выполнить корректную низкоуровневую инициализацию:
// 1. Включаем трассировку в CoreDebug
if (!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
}
// 2. Снимаем защиту с регистров DWT (обязательно для Cortex-M7 / STM32H7)
DWT->LAR = 0xC5ACCE55;
// 3. Сбрасываем и включаем счётчик тактов (CYCCNT)
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
Для простоты я не использовал функции CMSIS-RTOS в логгере.
Функция Logger_Init()Назначение функции Logger_Init() создать задачу, в которой будет выполняться логгер и создать очередь сообщений для логгера.
void Logger_Init(void) {
// 0. На всякий случай обновляем значение частоты ядра
SystemCoreClockUpdate();
// 1. Включаем трассировку в CoreDebug
if (!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
}
// 2. Снимаем защиту с регистров DWT (ОБЯЗАТЕЛЬНО для Cortex-M7 / STM32H7)
DWT->LAR = 0xC5ACCE55;
// 3. Сбрасываем и включаем счётчик тактов (CYCCNT)
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
// 4. Создаём очередь для хранения строк
xLogQueue = xQueueCreate(LOG_QUEUE_LEN, sizeof(log_item_t));
if (xLogQueue != NULL) {
// 5. Создаём задачу логгера с низким приоритетом
xTaskCreate(vLoggerTask, "LogTask", configMINIMAL_STACK_SIZE + 1024, NULL, osPriorityHigh, NULL);
}
}
Функция Logger_Log()Задача функции Logger_Log — получить сообщение и параметры от вызывающего потока, отформатировать сообщение и поместить в очередь сообщений.
Чтобы логгер оказывал меньшее влияние на тайминги остальных задач операционной системы, более правильно разместить код по форматированию сообщений в задачу логгера, но я этого делать не стал, чтобы не усложнять код.
void Logger_Log(log_level_t level, const char *tag, const char *format, ...) {
if (xLogQueue == NULL) return;
log_item_t item;
int offset = 0;
// Получаем время в микросекундах
uint32_t timestamp = Logger_GetMicroseconds();
// 1. Форматируем префикс: Время (мкс), Уровень, Тэг (ширина поля увеличена до 10 символов)
offset = snprintf(item.msg, LOG_MAX_STR_LEN, "[%10lu us] %s [%s]: ", timestamp, level_strings[level], tag);
if (offset < 0 || offset >= LOG_MAX_STR_LEN) return;
// 2. Добавляем пользовательское сообщение
va_list args;
va_start(args, format);
int body_len = vsnprintf(item.msg + offset, LOG_MAX_STR_LEN - offset - 2, format, args);
va_end(args);
if (body_len > 0) {
offset += body_len;
}
// 3. Добавляем перенос строки
strcpy(item.msg + offset, "\r\n");
// 4. Отправляем в очередь
xQueueSend(xLogQueue, &item, 5);
}
Функция vLoggerTask()Реализация функции vLoggerTask() простая, нужно в бесконечном цикле пытаться получить из очереди сообщений (xLogQueue) сообщение и вывести последнее на UART.
static void vLoggerTask(void *pvParameters) {
log_item_t item;
for (;;) {
if (xQueueReceive(xLogQueue, &item, portMAX_DELAY) == pdTRUE) {
Logger_Output(item.msg);
}
}
}
Ограничение созданного прототипа логгераНесмотря на то, что вы можете использовать разработанный логгер в своих проектах, я всё же считаю логгер прототипом, так для лучшего понимания я сделал ряд упрощений.
Я использую блокирующие вызовы UART, пересылкой данных по UART занимается процессор, что не есть хорошо.
Я использую очередь FreeRTOS, а не кольцевой буфер.
Длина сообщения жёстко определена, и не рационально используется оперативная память.
В статье я рассказал лишь малую часть обширной темы многопоточности вообще и многопоточности (многозадачности) во FreeRTOS.
Полученных знаний должно хватить на понимание основ использования FreeRTOS для микроконтроллера STM32.
Небольшую библиотеку логгера можно использовать в своих проектах, где необходим вывод отладочной информации.
И хотя FreeRTOS освещена в статье довольно поверхностно, читать документацию по FreeRTOS будет легче. А вы можете поставить себе галочку, что имели опыт работы с FreeRTOS (хоть и со словарём :)).
© 2026 ООО «МТ ФИНАНС»
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | STM32: Работа с SD-картами через SDIO | 0 | 13.63 | 07-10-2026 |
| 2 | Загружаем bitstream из Linux через FPGA Manager на Zynq-7000 | 0 | 8 | 28-06-2026 |
| 3 | Проект eMMC-emu | 0 | 8.99 | 06-08-2026 |
| 4 | Система для сборки прошивок для встройки | 0 | 6.54 | 05-08-2026 |
| 5 | От boot-кода до shell: несколько месяцев разработки ОС на Rust с ИИ-агентами | 0 | 12.89 | 30-07-2026 |
| 6 | Считаем пакеты на Rust | 0 | 9.87 | 04-07-2026 |
| 7 | 3D‑мелые ручки или как не надо собирать 3D принтер в домашних условиях | 0 | 8.71 | 17-07-2026 |
| 8 | Как сделать игру для ZX Spectrum в 2026 (и зачем) | 0 | 8.57 | 05-08-2026 |
| 9 | Локализация .NET-игры на коленке (devlog игровой разработки) | 0 | 5 | 15-06-2026 |