ESPNNFL announces U2 will play in first Thanksgiving Eve halftime showPunchSowore’s AAC seeks approval to inaugurate campaign council in Aso VillaThe Jerusalem PostRussia releases wrongfully detained US citizen Hubbard following Witkoff-Putin meetingDaily MaverickINTERVIEW: New EU envoy red-flags rigid BEE, SA’s Ukraine ‘double standard’, but hails ‘deeper’ tiesBollywood HungamaSaif Ali Khan begins shooting for Ritesh Batra’s period drama in Mumbai; details of new collaboration remain under wrapsInquirerKapunan: VP Duterte, Manases prenup, if any, will face scrutinyBBC News BrasilPesquisa Quaest, agenda de Flávio e Lula e outras notícias desta quarta-feira; sigaCollider‘Reacher’ Creator Calls Jack Reacher an Updated Version of Sherlock Holmes [Exclusive]South China Morning PostThe EU will ‘pull its punches’ on China despite growing trade war talk: analystsSCMP ChinaChinese mathematician Sun Zhiwei proposes 1,000 problems to challenge AIThe RegisterAI has 'potential' to turn script kiddies into sophisticated infrastructure attackersThe Hollywood ReporterDon’t Stop Believin’: ‘Glee’ Revival a Go at Disney+ and Hulu
The Daily Newsstand · Free, Always
Wednesday, October 21, 2026

Изучаем многозадачность FreeRTOS на примере создания логгера для STM32

Translate

Логирование является важной составной частью профессиональной отладки программ. Написав небольшой логгер и изучая, как он работает, можно лучше разобраться, как работает многозадачность в операционной системе.

Из всех возможных вариантов вывода текста самым удобным, с моей точки зрения, является UART, хотя это не самый быстрый вариант.

Если у вас не отладочная плата Nucleo или Discovery, необходимо обзавестись UART-USB адаптером или программатором, который содержит в себе USB-UART адаптер. Оригинальные адаптеры содержат в себе этот интерфейс, но из китайских клонов можно использовать или Weact ST-Link или J-Link.

Исходный код логгера для микроконтроллера STM32H750 приведён в моём репозитории на GitHub.

Скучные примеры из книжек по FreeRTOS можно сделать гораздо интереснее, если найти прикладную задачу, в нашем случае — логгер. Я хочу убить двух зайцев: рассказать о том, как работать с операционной системой FreeRTOS на микроконтроллерах STM32 и как написать простейший логгер.

Подключаем FreeRTOS

FreeRTOS — это операционная система реального времени, которая портирована на множество различных аппаратных платформ. В основном это микроконтроллеры с ограниченными ресурсами.

Когда вы пишете программы, для операционных систем 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, в котором три задачи с одинаковым приоритетом, цикле выводят сообщения.

  1. Активируем Serial Wire Debug:

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

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

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

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

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

  7. Подключаем отладочную плату. К пинам, которые используются UART, необходимо подключить или USB-UART адаптер или программатор, который поддерживает виртуальный COM-порт.

  8. Запускаем терминал

        sudo picocom --imap lfcrlf -b 115200 /dev/ttyACM0
    

    где ttyACM0 имя виртуального COM-порта

  9. Собираем и запускаем проект.

На выходе мы видим кашу.

Связано это с тем, что функция printf() из библиотеки newlib не является потокобезопасной. Когда она вызывается одновременно из нескольких потоков, рушатся её внутренние структуры.

Реализация корректного вывода при помощи мьютекса

Многие изучали операционные системы и имеют представление о вытесняющей (preemptive) и кооперативной (cooperative) многозадачности и как работает планировщик.

Также слышали и имеют базовое представление о синхронизирующих объектах в операционной системе, таких как мьютексы, семафоры и др.

Но если по отдельности всё понятно, то как работает планировщик совместно с примитивами — всё гораздо сложнее.

Вы можете написать неправильно программу, использующую многозадачность, и программа будет даже работать. Но при изменении настроек многозадачности или при другой частоте вывода или другом размере сообщений, программа у вас загадочным образом перестанет работать.

Вы знаете, что защитить совместный ресурс можно при помощи синхронизирующих конструкций, например, мьютекса.

Добавляем мьютекс:

Мьютекс гарантирует взаимное исключение: только та задача, которая захватила мьютекс, получает доступ к коду внутри мьютека, в то время как остальные задачи будут переведены в состояние ожидания (блокировки) при попытке взять этот же мьютекс.

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

Запускаем. И что мы видим? Один из потоков монополизировал вывод, а остальные просто ничего вывести не могут.

Почему такое происходит? Мы же выбрали вытесняющую многозадачность, потоки имеют одинаковый приоритет, и даже включен Time Slicing!

Дело в том, что во FreeRTOS используется механизм «несправедливых» мьютексов. Когда поток освобождает мьютекс, поток всё ещё продолжает выполняться. И если в цикле он сразу же пытается захватить мьютекс снова, он делает это быстрее, чем планировщик успевает переключить контекст на другую задачу (даже при одинаковых приоритетах).

Захват уже захваченного мьютекса переводит задачу в состояние blocked. Задачи в состоянии blocked не участвуют в планировании. То есть может возникнуть ситуация, когда задача захватила мьютекс и выводит по UART. Вторая задача пытается захватить мьютекс, но он уже захвачен, поэтому задача переходит в состояние blocked.

Или более подробно:

  1. Поток А освобождает мьютекс. В этот момент поток А всё ещё находится в состоянии Running (исполняется) и его квант времени ещё не истёк.

  2. Поток А снова вызывает safe_printf и пытается захватить мьютекс.

  3. Так как планировщик ещё не переключил контекст на потоки Б или В, поток А мгновенно забирает мьютекс обратно.

Добавим в конце тела цикла 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);
        }
    }
}

Ограничение созданного прототипа логгера

Несмотря на то, что вы можете использовать разработанный логгер в своих проектах, я всё же считаю логгер прототипом, так для лучшего понимания я сделал ряд упрощений.

  1. Я использую блокирующие вызовы UART, пересылкой данных по UART занимается процессор, что не есть хорошо.

  2. Я использую очередь FreeRTOS, а не кольцевой буфер.

  3. Длина сообщения жёстко определена, и не рационально используется оперативная память.

Заключение

В статье я рассказал лишь малую часть обширной темы многопоточности вообще и многопоточности (многозадачности) во FreeRTOS.

Полученных знаний должно хватить на понимание основ использования FreeRTOS для микроконтроллера STM32.

Небольшую библиотеку логгера можно использовать в своих проектах, где необходим вывод отладочной информации.

И хотя FreeRTOS освещена в статье довольно поверхностно, читать документацию по FreeRTOS будет легче. А вы можете поставить себе галочку, что имели опыт работы с FreeRTOS (хоть и со словарём :)).

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.