Jump to content
View in the app

A better way to browse. Learn more.

T.M.I IThub

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Зачем RTOS на микроконтроллере

Простой проект — один while(1) цикл. Всё хорошо: считали датчик, обновили дисплей, проверили кнопку. Но что если:

  • Нужно принять UART-пакет точно за 10 мс, иначе потеряем байты

  • Одновременно управлять тремя независимыми ПИД-контурами

  • Обрабатывать CAN-сообщения с задержкой не более 5 мс

  • И параллельно вести логирование на SD-карту

Суперцикл (while(1)) ломается: длинная операция блокирует всё остальное. Прерывания помогают, но сложная логика в прерываниях — путь к хаосу.

FreeRTOS решает это элегантно: каждая задача — отдельный "поток" со своим стеком и приоритетом. Планировщик переключает их так быстро (обычно каждые 1 мс), что кажется будто они работают одновременно. Задача с высоким приоритетом всегда получает процессор раньше.


Ключевые концепции FreeRTOS

Task (Задача)

// Прототип задачи — бесконечный цикл!
void vTaskFunction(void *pvParameters)
{
    // Инициализация задачи
    int *param = (int *)pvParameters;
    
    for (;;)  // Никогда не выходит!
    {
        // Работа задачи...
        
        // Уступить процессор (обязательно в каждом цикле!)
        vTaskDelay(pdMS_TO_TICKS(100));  // Пауза 100 мс
    }
    
    vTaskDelete(NULL);  // Никогда не достигается, но хорошая практика
}

// Создание задачи:
TaskHandle_t xTaskHandle = NULL;

xTaskCreate(
    vTaskFunction,       // Функция задачи
    "TaskName",          // Имя (для отладки)
    configMINIMAL_STACK_SIZE * 4,  // Размер стека в словах
    NULL,                // Параметр (pvParameters)
    tskIDLE_PRIORITY + 2, // Приоритет (выше = важнее)
    &xTaskHandle         // Хендл задачи
);

Приоритеты

configMAX_PRIORITIES = 7 (типично)

Приоритет 6: КРИТИЧЕСКИЙ (ISR-уровень, прерывания)
Приоритет 5: Коммуникации реального времени (CAN, UART)
Приоритет 4: Управление (ПИД-контроллеры)
Приоритет 3: Мониторинг, аварийная логика
Приоритет 2: UI, дисплей, кнопки
Приоритет 1: Логирование, некритичные задачи
Приоритет 0: Idle task (только когда все остальные ждут)

Архитектура многозадачного приложения

Реальный пример: контроллер насосной станции на STM32F4.

// ===== ЗАГОЛОВКИ =====
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "timers.h"

// ===== ГЛОБАЛЬНЫЕ ОБЪЕКТЫ FreeRTOS =====
QueueHandle_t   xSensorQueue;       // Данные датчиков
QueueHandle_t   xCommandQueue;      // Команды управления
QueueHandle_t   xLogQueue;          // Сообщения лога
SemaphoreHandle_t xI2CMutex;        // Защита I2C шины
SemaphoreHandle_t xUARTMutex;       // Защита UART (printf)
TimerHandle_t   xHeartbeatTimer;    // Мигание LED watchdog

// ===== СТРУКТУРЫ ДАННЫХ =====

typedef struct {
    float    temperature;
    float    pressure;
    float    flow;
    uint32_t timestamp_ms;
    uint8_t  quality;   // 0=BAD, 1=UNCERTAIN, 2=GOOD
} SensorData_t;

typedef enum {
    CMD_START,
    CMD_STOP,
    CMD_SET_SETPOINT,
    CMD_RESET_FAULT,
} CommandType_t;

typedef struct {
    CommandType_t type;
    float         value;
    uint8_t       source;  // 0=HMI, 1=Modbus, 2=Auto
} Command_t;

typedef struct {
    char     message[80];
    uint8_t  level;   // 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR
    uint32_t timestamp_ms;
} LogMessage_t;

// ===== ВСПОМОГАТЕЛЬНЫЙ МАКРОС ДЛЯ PRINTF =====
// Потокобезопасный printf через мьютекс
#define LOG(level, fmt, ...) do { \
    LogMessage_t msg; \
    msg.level = (level); \
    msg.timestamp_ms = xTaskGetTickCount(); \
    snprintf(msg.message, sizeof(msg.message), fmt, ##__VA_ARGS__); \
    xQueueSend(xLogQueue, &msg, 0); \
} while(0)

#define LOG_INFO(fmt,...)  LOG(1, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt,...)  LOG(2, "[WARN] " fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt,...) LOG(3, "[ERR!] " fmt, ##__VA_ARGS__)

// ===== ЗАДАЧА 1: ЧТЕНИЕ ДАТЧИКОВ (Приоритет 4) =====
static void vSensorTask(void *pvParam)
{
    SensorData_t data;
    TickType_t   xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(100);  // 10 Гц
    
    LOG_INFO("Sensor task started");
    
    for (;;)
    {
        // Ждём ровно 100 мс от последнего пробуждения
        // vTaskDelayUntil гарантирует точный период!
        vTaskDelayUntil(&xLastWakeTime, xPeriod);
        
        data.timestamp_ms = xTaskGetTickCount();
        
        // Захватываем I2C шину
        if (xSemaphoreTake(xI2CMutex, pdMS_TO_TICKS(50)) == pdTRUE)
        {
            data.temperature = BMP280_ReadTemperature();
            data.pressure    = BMP280_ReadPressure();
            data.quality     = 2;  // GOOD
            xSemaphoreGive(xI2CMutex);
        }
        else
        {
            // I2C занята дольше 50 мс — что-то пошло не так
            data.quality = 0;  // BAD
            LOG_WARN("I2C timeout in sensor task");
        }
        
        // Читаем расходомер через 4-20мА
        data.flow = ADC_ReadFlow();
        
        // Отправляем данные в очередь (не блокируем — если полная, пропускаем)
        if (xQueueSend(xSensorQueue, &data, 0) != pdTRUE)
        {
            LOG_WARN("Sensor queue full!");
        }
    }
}

// ===== ЗАДАЧА 2: ПИД-УПРАВЛЕНИЕ (Приоритет 5) =====
static void vControlTask(void *pvParam)
{
    SensorData_t sensorData;
    Command_t    command;
    
    float setpoint = 5.0f;   // Уставка давления, бар
    float output   = 0.0f;
    bool  running  = false;
    bool  fault    = false;
    
    // ПИД параметры
    float kp = 2.0f, ki = 0.5f, kd = 0.1f;
    float integral = 0.0f, prevError = 0.0f;
    const float TS = 0.1f;  // Совпадает с периодом датчиков
    
    LOG_INFO("Control task started");
    
    for (;;)
    {
        // Ждём новые данные датчиков (блокирующий ждём до 200 мс)
        if (xQueueReceive(xSensorQueue, &sensorData, pdMS_TO_TICKS(200)) == pdTRUE)
        {
            // Проверяем входящие команды (неблокирующий)
            while (xQueueReceive(xCommandQueue, &command, 0) == pdTRUE)
            {
                switch (command.type)
                {
                    case CMD_START:
                        running = true;
                        fault   = false;
                        integral = 0;
                        LOG_INFO("Pump STARTED by source %d", command.source);
                        break;
                    
                    case CMD_STOP:
                        running = false;
                        output  = 0;
                        LOG_INFO("Pump STOPPED by source %d", command.source);
                        break;
                    
                    case CMD_SET_SETPOINT:
                        setpoint = command.value;
                        LOG_INFO("Setpoint changed to %.1f bar", setpoint);
                        break;
                    
                    case CMD_RESET_FAULT:
                        fault = false;
                        LOG_INFO("Fault reset");
                        break;
                }
            }
            
            // Защиты
            if (sensorData.quality == 0)
            {
                running = false;
                fault   = true;
                output  = 0;
                LOG_ERROR("Sensor fault! Emergency stop.");
            }
            
            if (sensorData.pressure > 12.0f)
            {
                running = false;
                fault   = true;
                output  = 0;
                LOG_ERROR("High pressure! %.2f bar > 12.0 bar", sensorData.pressure);
            }
            
            if (sensorData.temperature > 90.0f)
            {
                running = false;
                fault   = true;
                output  = 0;
                LOG_ERROR("Motor overtemp! %.1f°C > 90°C", sensorData.temperature);
            }
            
            // ПИД вычисление
            if (running && !fault)
            {
                float error = setpoint - sensorData.pressure;
                
                integral += ki * error * TS;
                integral  = fmaxf(-50.0f, fminf(50.0f, integral));  // Anti-windup
                
                float derivative = -(sensorData.pressure - prevError) / TS;
                prevError = sensorData.pressure;
                
                output = kp * error + integral + kd * derivative;
                output = fmaxf(0.0f, fminf(100.0f, output));
            }
            else
            {
                output  = 0.0f;
                integral = 0.0f;
            }
            
            // Применяем управляющий сигнал
            VFD_SetFrequency(output * 0.5f);  // 0-100% → 0-50 Гц
        }
        else
        {
            // Таймаут ожидания данных датчика — авария
            LOG_ERROR("Sensor data timeout!");
            running = false;
            output  = 0;
            VFD_SetFrequency(0);
        }
    }
}

// ===== ЗАДАЧА 3: MODBUS SLAVE (Приоритет 3) =====
static void vModbusTask(void *pvParam)
{
    uint8_t rxBuf[64];
    uint8_t rxLen = 0;
    
    LOG_INFO("Modbus task started");
    
    for (;;)
    {
        // Ждём байт из UART (через семафор от прерывания)
        if (UART_WaitForData(rxBuf, &rxLen, pdMS_TO_TICKS(100)))
        {
            // Обрабатываем Modbus запрос
            if (Modbus_ProcessRequest(rxBuf, rxLen))
            {
                // Если команда — отправляем в очередь управления
                Command_t cmd;
                if (Modbus_ExtractCommand(&cmd))
                {
                    xQueueSend(xCommandQueue, &cmd, pdMS_TO_TICKS(10));
                }
            }
        }
    }
}

// ===== ЗАДАЧА 4: ЛОГИРОВАНИЕ (Приоритет 1, самый низкий) =====
static void vLoggingTask(void *pvParam)
{
    LogMessage_t msg;
    const char *levelNames[] = {"DBG", "INF", "WRN", "ERR"};
    
    for (;;)
    {
        // Ждём сообщение из очереди
        if (xQueueReceive(xLogQueue, &msg, portMAX_DELAY) == pdTRUE)
        {
            // Пишем в UART (захватываем мьютекс)
            if (xSemaphoreTake(xUARTMutex, pdMS_TO_TICKS(50)) == pdTRUE)
            {
                printf("[%6lu][%s] %s\r\n",
                       (unsigned long)msg.timestamp_ms,
                       levelNames[msg.level % 4],
                       msg.message);
                xSemaphoreGive(xUARTMutex);
            }
            
            // Пишем на SD-карту (низкий приоритет = не мешаем критичным задачам)
            // SD_AppendLog(&msg);
        }
    }
}

// ===== ТАЙМЕР HEARTBEAT =====
static void vHeartbeatCallback(TimerHandle_t xTimer)
{
    // Мигаем светодиодом — система жива
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}

// ===== ИНИЦИАЛИЗАЦИЯ =====
void App_Init(void)
{
    // Создаём очереди
    xSensorQueue  = xQueueCreate(5,  sizeof(SensorData_t));
    xCommandQueue = xQueueCreate(10, sizeof(Command_t));
    xLogQueue     = xQueueCreate(20, sizeof(LogMessage_t));
    
    // Создаём мьютексы
    xI2CMutex  = xSemaphoreCreateMutex();
    xUARTMutex = xSemaphoreCreateMutex();
    
    // Создаём задачи
    xTaskCreate(vSensorTask,  "Sensors",  512, NULL, 4, NULL);
    xTaskCreate(vControlTask, "Control",  1024, NULL, 5, NULL);
    xTaskCreate(vModbusTask,  "Modbus",   512, NULL, 3, NULL);
    xTaskCreate(vLoggingTask, "Logging",  256, NULL, 1, NULL);
    
    // Создаём таймер heartbeat (500 мс)
    xHeartbeatTimer = xTimerCreate("Heartbeat", pdMS_TO_TICKS(500),
                                    pdTRUE, NULL, vHeartbeatCallback);
    xTimerStart(xHeartbeatTimer, 0);
    
    // Запуск планировщика
    vTaskStartScheduler();
    
    // Никогда не должно дойти сюда!
    for (;;);
}

Очереди: безопасная передача данных между задачами

Очередь — это основной механизм коммуникации в FreeRTOS. Thread-safe, FIFO, блокирующий.

// Создание очереди на 10 элементов типа uint32_t
QueueHandle_t xQueue = xQueueCreate(10, sizeof(uint32_t));

// Отправка (из задачи)
uint32_t value = 42;
xQueueSend(xQueue, &value, pdMS_TO_TICKS(100));  // Ждём 100мс если полная

// Отправка с высоким приоритетом (в начало очереди)
xQueueSendToFront(xQueue, &value, 0);

// Отправка из прерывания (другая функция!)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(xQueue, &value, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);  // Уступить если нужно

// Приём (блокирует до данных или таймаута)
uint32_t received;
if (xQueueReceive(xQueue, &received, portMAX_DELAY) == pdTRUE) {
    // Данные получены
}

// "Подсмотреть" без извлечения
xQueuePeek(xQueue, &received, 0);

// Мониторинг
UBaseType_t count = uxQueueMessagesWaiting(xQueue);  // Сколько элементов
UBaseType_t space = uxQueueSpacesAvailable(xQueue);  // Сколько свободно

Семафоры и мьютексы: защита разделяемых ресурсов

// ===== МЬЮТЕКС (Mutual Exclusion) =====
// Для защиты ресурсов (I2C, SPI, UART, глобальные переменные)

SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();

// Правильный паттерн:
void safe_i2c_read(uint8_t addr, uint8_t reg, uint8_t *buf, uint8_t len)
{
    if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE)
    {
        HAL_I2C_Mem_Read(&hi2c1, addr, reg, 1, buf, len, 100);
        xSemaphoreGive(xMutex);
    }
    else
    {
        // Таймаут — логируем, возвращаем ошибку
    }
}

// ===== ДВОИЧНЫЙ СЕМАФОР (уведомление о событии) =====
// Задача ждёт событие от ISR

SemaphoreHandle_t xDataReadySemaphore = xSemaphoreCreateBinary();

// В прерывании (данные готовы):
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    BaseType_t xWoken = pdFALSE;
    xSemaphoreGiveFromISR(xDataReadySemaphore, &xWoken);
    portYIELD_FROM_ISR(xWoken);
}

// В задаче (ждём данные):
void vProcessingTask(void *pvParam)
{
    for (;;) {
        // Эффективное ожидание — задача не потребляет CPU!
        xSemaphoreTake(xDataReadySemaphore, portMAX_DELAY);
        
        // Данные готовы — обрабатываем
        process_uart_data();
    }
}

// ===== СЧЁТНЫЙ СЕМАФОР (ограничение конкурентного доступа) =====
// Пример: максимум 3 одновременных подключения

SemaphoreHandle_t xConnectionSlots = xSemaphoreCreateCounting(3, 3);

void handle_new_connection() {
    if (xSemaphoreTake(xConnectionSlots, pdMS_TO_TICKS(5000)) == pdTRUE)
    {
        // Слот получен
        serve_client();
        xSemaphoreGive(xConnectionSlots);  // Освободить слот
    }
    else
    {
        // Нет свободных слотов
        send_busy_response();
    }
}

Программные таймеры

// Таймер однократный (one-shot) vs периодический
TimerHandle_t xOneShotTimer;
TimerHandle_t xPeriodicTimer;

void vTimerCallback(TimerHandle_t xTimer)
{
    // pvTimerGetTimerID позволяет использовать один callback для многих таймеров
    uint32_t timerID = (uint32_t)pvTimerGetTimerID(xTimer);
    
    switch (timerID) {
        case 1:
            // Однократный таймер — отключить нагреватель через 30 сек
            Heater_Off();
            break;
        case 2:
            // Периодический — опрос watchdog
            External_WDT_Kick();
            break;
    }
}

void setup_timers(void)
{
    // One-shot таймер (не перезапускается автоматически)
    xOneShotTimer = xTimerCreate(
        "Heater",
        pdMS_TO_TICKS(30000),  // 30 секунд
        pdFALSE,               // pdFALSE = one-shot
        (void *)1,             // ID таймера
        vTimerCallback
    );
    
    // Периодический таймер
    xPeriodicTimer = xTimerCreate(
        "WDT",
        pdMS_TO_TICKS(500),    // 500 мс
        pdTRUE,                // pdTRUE = периодический
        (void *)2,
        vTimerCallback
    );
    
    xTimerStart(xPeriodicTimer, 0);
    
    // Запустить один-шот когда нужно:
    // xTimerStart(xOneShotTimer, 0);
    
    // Сбросить периодический (перезапустить отсчёт):
    // xTimerReset(xPeriodicTimer, 0);
    
    // Изменить период на лету:
    // xTimerChangePeriod(xPeriodicTimer, pdMS_TO_TICKS(1000), 0);
}

Управление памятью и отладка

// Мониторинг стека задачи (важно для нахождения переполнений!)
void vCheckStackTask(void *pvParam)
{
    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(5000));
        
        // Минимальный остаток стека (в словах) с начала работы
        UBaseType_t hwm = uxTaskGetStackHighWaterMark(NULL);
        
        if (hwm < 50) {  // Меньше 50 слов — опасно!
            printf("WARNING: Task '%s' stack low! HWM=%lu words\r\n",
                   pcTaskGetName(NULL), (unsigned long)hwm);
        }
    }
}

// Вывод информации о всех задачах (для отладки)
void vPrintTaskStats(void)
{
    char buffer[512];
    vTaskList(buffer);  // Требует configUSE_TRACE_FACILITY=1
    printf("Task Name\t\tState\tPrio\tStack\tNum\r\n%s", buffer);
    
    // Загрузка CPU по задачам (требует configGENERATE_RUN_TIME_STATS=1)
    vTaskGetRunTimeStats(buffer);
    printf("\r\nTask\t\t\tTime\t\t%%\r\n%s", buffer);
}

// Обработчик нехватки памяти
void vApplicationMallocFailedHook(void)
{
    taskDISABLE_INTERRUPTS();
    printf("FATAL: malloc failed! Heap exhausted.\r\n");
    for (;;);
}

// Переполнение стека задачи
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
    taskDISABLE_INTERRUPTS();
    printf("FATAL: Stack overflow in task '%s'!\r\n", pcTaskName);
    for (;;);
}

FreeRTOS на ESP32

ESP-IDF (официальный SDK ESP32) использует FreeRTOS как основу. На двухядерном ESP32 задачи можно привязывать к конкретному ядру:

// ESP32-специфичное создание задачи с указанием ядра
xTaskCreatePinnedToCore(
    vWiFiTask,       // Функция
    "WiFi",          // Имя
    8192,            // Стек (в байтах для ESP32!)
    NULL,            // Параметры
    5,               // Приоритет
    &xWiFiHandle,    // Хендл
    0                // Ядро: 0 = Protocol CPU, 1 = Application CPU
);

// WiFi и Bluetooth — всегда на ядре 0 (Protocol CPU)
// Ваш код — лучше на ядре 1 (Application CPU)
// Это разделяет сетевой стек и бизнес-логику

// Встроенный мониторинг задач ESP-IDF:
void print_esp_task_info(void)
{
    printf("Free heap: %u bytes\r\n", esp_get_free_heap_size());
    printf("Min free heap: %u bytes\r\n", esp_get_minimum_free_heap_size());
}

Типичные ошибки FreeRTOS

1. Вызов обычных функций из ISR

// НЕПРАВИЛЬНО — заблокирует прерывание!
void HAL_GPIO_EXTI_Callback(uint16_t pin) {
    xQueueSend(xQueue, &data, portMAX_DELAY);  // ОШИБКА: блокирует ISR!
}

// ПРАВИЛЬНО — FromISR версии функций:
void HAL_GPIO_EXTI_Callback(uint16_t pin) {
    BaseType_t xWoken = pdFALSE;
    xQueueSendFromISR(xQueue, &data, &xWoken);
    portYIELD_FROM_ISR(xWoken);
}

2. Бесконечная задача без yield

// НЕПРАВИЛЬНО — монополизирует процессор!
void vBadTask(void *p) {
    for (;;) {
        do_something();
        // Нет vTaskDelay или блокирующего ожидания!
    }
}

// ПРАВИЛЬНО:
void vGoodTask(void *p) {
    for (;;) {
        do_something();
        vTaskDelay(pdMS_TO_TICKS(10));  // Уступаем хотя бы 10 мс
    }
}

3. Доступ к глобальным данным без защиты

// НЕПРАВИЛЬНО — race condition!
float g_temperature = 0;

void vSensor(void *p) { g_temperature = read_sensor(); }
void vControl(void *p) { if (g_temperature > 80) alarm(); }

// ПРАВИЛЬНО — через очередь или мьютекс

Заключение

FreeRTOS превращает микроконтроллер из последовательного автомата в полноценную многозадачную систему. Это не усложнение ради усложнения — это решение реальных проблем: независимость задач, чёткие интерфейсы через очереди, защита ресурсов через мьютексы.

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

FreeRTOS поддерживается на STM32, ESP32, Arduino (с ограничениями), Raspberry Pi Pico и десятках других платформ. Документация на freertos.org — отличная, с примерами и объяснениями.

User Feedback

Create an account or sign in to leave a review

There are no reviews to display.

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.