Зачем 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 — отличная, с примерами и объяснениями.
Create an account or sign in to leave a review
There are no reviews to display.