Skip to main content

RTOS — Операционные системы реального времени

RTOS — Операционные системы реального времени

RTOS (Real-Time Operating System) обеспечивает детерминированное выполнение задач с гарантированным временем отклика. В отличие от Linux, где задержки могут составлять миллисекунды, RTOS гарантирует микросекундную предсказуемость.


Зачем нужна RTOS

Проблема суперцикла (bare-metal)

void loop() {
  readSensors();      // 5 мс
  calculatePID();     // 1 мс
  updateMotors();     // 0.1 мс
  sendTelemetry();    // 50 мс (блокирует!)
  checkButtons();     // пропущено на 50 мс
}

Проблемы:

  • Блокирующие операции останавливают всё
  • Невозможно гарантировать периодичность PID
  • Сложно масштабировать

Решение RTOS

// Задачи выполняются "параллельно"
xTaskCreate(taskPID,      "PID",     1024, NULL, 3, NULL);  // Высокий приоритет
xTaskCreate(taskSensors,  "Sensors", 2048, NULL, 2, NULL);
xTaskCreate(taskTelemetry,"Telemetry",4096, NULL, 1, NULL); // Низкий приоритет

Теория планирования

Модель периодических задач

Задача $\tau_i$ характеризуется:

  • $C_i$ — время выполнения (WCET)
  • $T_i$ — период
  • $D_i$ — дедлайн (обычно $D_i = T_i$)
$$U = \sum_{i=1}^{n} \frac{C_i}{T_i}$$

Rate Monotonic Scheduling (RMS)

Статический приоритет: чем короче период, тем выше приоритет.

$$U \leq n(2^{1/n} - 1)$$
nПредел U
11.000
20.828
30.780
0.693

Пример: 3 задачи с $U = 0.1 + 0.2 + 0.3 = 0.6 < 0.780$ — планируемы.

Earliest Deadline First (EDF)

Динамический приоритет: выполняется задача с ближайшим дедлайном.

$$U \leq 1$$

EDF оптимален, но сложнее в реализации.


FreeRTOS

Архитектура

┌─────────────────────────────────────┐
│           Application Tasks         │
├─────────────────────────────────────┤
│  Queues │ Semaphores │ Timers │ ... │
├─────────────────────────────────────┤
│           FreeRTOS Kernel           │
├─────────────────────────────────────┤
│         Hardware Abstraction        │
└─────────────────────────────────────┘

API задач

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// Создание задачи
BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // Функция задачи
    const char *pcName,             // Имя (для отладки)
    uint32_t usStackDepth,          // Размер стека (слова)
    void *pvParameters,             // Параметры
    UBaseType_t uxPriority,         // Приоритет (0 = idle)
    TaskHandle_t *pxCreatedTask     // Handle (опционально)
);

// Пример задачи
void vTaskPID(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(10);  // 10 мс = 100 Гц
    
    for (;;) {
        float error = setpoint - readEncoder();
        float output = computePID(error);
        setMotorPWM(output);
        
        // Точная периодичность (не vTaskDelay!)
        vTaskDelayUntil(&xLastWakeTime, xPeriod);
    }
}

Очереди (Queues)

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

#include "freertos/queue.h"

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

// Отправка (из ISR используйте xQueueSendFromISR)
void vTaskSensor(void *pvParameters) {
    SensorData data;
    for (;;) {
        data.distance = readUltrasonic();
        data.timestamp = xTaskGetTickCount();
        
        // Блокировка до 100 мс если очередь полна
        xQueueSend(xSensorQueue, &data, pdMS_TO_TICKS(100));
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

// Приём
void vTaskProcess(void *pvParameters) {
    SensorData data;
    for (;;) {
        if (xQueueReceive(xSensorQueue, &data, portMAX_DELAY)) {
            processData(&data);
        }
    }
}

Семафоры и мьютексы

#include "freertos/semphr.h"

// Бинарный семафор (сигнализация)
SemaphoreHandle_t xBinarySem = xSemaphoreCreateBinary();

// ISR сигнализирует о событии
void IRAM_ATTR encoderISR() {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// Задача ждёт события
void vTaskEncoder(void *pvParameters) {
    for (;;) {
        if (xSemaphoreTake(xBinarySem, portMAX_DELAY)) {
            encoderCount++;
        }
    }
}

// Мьютекс (защита ресурса)
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();

void safeSerialPrint(const char *msg) {
    if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100))) {
        Serial.println(msg);
        xSemaphoreGive(xMutex);
    }
}

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

#include "freertos/timers.h"

TimerHandle_t xHeartbeatTimer;

void vHeartbeatCallback(TimerHandle_t xTimer) {
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}

void setup() {
    xHeartbeatTimer = xTimerCreate(
        "Heartbeat",
        pdMS_TO_TICKS(500),  // Период
        pdTRUE,              // Auto-reload
        NULL,
        vHeartbeatCallback
    );
    xTimerStart(xHeartbeatTimer, 0);
}

Zephyr RTOS

Современная RTOS с поддержкой 500+ плат, сетевых стеков, Bluetooth, USB.

Конфигурация (prj.conf)

CONFIG_MULTITHREADING=y
CONFIG_TIMESLICING=y
CONFIG_NUM_PREEMPT_PRIORITIES=16
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_HEAP_MEM_POOL_SIZE=16384

Потоки (Threads)

#include <zephyr/kernel.h>

#define STACK_SIZE 1024
#define PRIORITY 5

K_THREAD_STACK_DEFINE(pid_stack, STACK_SIZE);
struct k_thread pid_thread;

void pid_entry(void *p1, void *p2, void *p3) {
    while (1) {
        // PID вычисления
        k_sleep(K_MSEC(10));
    }
}

void main(void) {
    k_thread_create(&pid_thread, pid_stack, STACK_SIZE,
                    pid_entry, NULL, NULL, NULL,
                    PRIORITY, 0, K_NO_WAIT);
}

Сравнение FreeRTOS vs Zephyr

ПараметрFreeRTOSZephyr
Размер ядра5-10 KB20-50 KB
Плат~40500+
Сетевой стекОтдельно (FreeRTOS+TCP)Встроенный
BluetoothНетДа (BLE 5.x)
ДрайверыМинимумОбширные
СертификацияIEC 61508 SIL 4Нет
ЛицензияMITApache 2.0

Инверсия приоритетов

Проблема

Задача H (высокий) ждёт мьютекс
Задача M (средний) выполняется
Задача L (низкий) держит мьютекс
→ H ждёт M, хотя H важнее!

Решение: Priority Inheritance

// FreeRTOS: мьютекс с наследованием приоритета
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// Автоматически поднимает приоритет L до H

Отладка RTOS

Stack Overflow Detection

// FreeRTOSConfig.h
#define configCHECK_FOR_STACK_OVERFLOW 2

void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    Serial.printf("Stack overflow in %s!\n", pcTaskName);
    while(1);
}

Runtime Stats

char buffer[512];
vTaskGetRunTimeStats(buffer);
Serial.println(buffer);

// Вывод:
// Task          Abs Time      % Time
// PID           12345         45%
// Sensors       5432          20%
// IDLE          9500          35%

Трассировка (Segger SystemView)

#include "SEGGER_SYSVIEW.h"
SEGGER_SYSVIEW_Conf();
SEGGER_SYSVIEW_Start();
// Визуализация в реальном времени

Паттерны проектирования

Producer-Consumer

QueueHandle_t xDataQueue;

void vProducer(void *pv) {
    Data d;
    for (;;) {
        d = acquireData();
        xQueueSend(xDataQueue, &d, portMAX_DELAY);
    }
}

void vConsumer(void *pv) {
    Data d;
    for (;;) {
        xQueueReceive(xDataQueue, &d, portMAX_DELAY);
        processData(&d);
    }
}

Event Groups

#include "freertos/event_groups.h"

#define BIT_SENSOR_READY  (1 << 0)
#define BIT_WIFI_CONNECTED (1 << 1)

EventGroupHandle_t xEvents = xEventGroupCreate();

// Ожидание нескольких событий
EventBits_t bits = xEventGroupWaitBits(
    xEvents,
    BIT_SENSOR_READY | BIT_WIFI_CONNECTED,
    pdTRUE,   // Clear on exit
    pdTRUE,   // Wait for ALL bits
    portMAX_DELAY
);

Рекомендации

  1. Размер стека — начните с 2048 слов, уменьшайте по uxTaskGetStackHighWaterMark()
  2. Приоритеты — не более 5-7 уровней, избегайте равных приоритетов
  3. ISR — минимум кода, только xSemaphoreGiveFromISR / xQueueSendFromISR
  4. Мьютексы — только для защиты данных, не для сигнализации
  5. Watchdog — обязательно в продакшене

Ссылки