Введение
До этой статьи весь код цикла представлял собой единственный цикл while(1) с прерываниями — простая модель, подходящая для небольших задач, но усложняющаяся с ростом числа независимых, параллельно выполняющихся обязанностей системы. FreeRTOS — операционная система реального времени (RTOS), широко используемая на STM32, дающая полноценную многозадачность с приоритетами задач, очередями сообщений между задачами и таймерами — концептуально некоторое сходство с многопоточностью Qt из основного цикла, но адаптированное для ограниченных ресурсов микроконтроллера.
Концепция
FreeRTOS позволяет разбить программу на отдельные «задачи» (tasks) — независимо планируемые единицы выполнения, каждая со своим стеком и приоритетом, переключаемые планировщиком ОС по истечении времени или по более приоритетному событию (аналогично потокам обычной ОС, но со значительно меньшими накладными расходами, подходящими для ограниченных ресурсов микроконтроллера). Очереди FreeRTOS (xQueue) обеспечивают безопасный обмен данными между задачами без явной ручной синхронизации мьютексами в простых случаях передачи данных от одной задачи к другой.
Пример кода: FreeRTOS на STM32
// FreeRTOS-задачи: одна периодически считывает датчик, другая отправляет данные по UART
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
QueueHandle_t sensorDataQueue;
typedef struct {
float temperature;
float humidity;
} SensorData;
void SensorTask(void *parameter)
{
while (1) {
SensorData data;
data.temperature = readAdcValue() / 4095.0f * 3.3f * 10.0f; // упрощённый пересчёт для примера
data.humidity = 45.0f; // имитация для примера
// Отправка в очередь — безопасно с точки зрения одновременного доступа из другой задачи
xQueueSend(sensorDataQueue, &data, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(1000)); // аналог HAL_Delay, но НЕ блокирует другие задачи —
// планировщик передаёт время другим задачам пока эта ждёт
}
}
void UartTransmitTask(void *parameter)
{
SensorData receivedData;
while (1) {
// Блокируется ЭТА задача (а не вся система), пока в очереди нет новых данных
if (xQueueReceive(sensorDataQueue, &receivedData, portMAX_DELAY) == pdTRUE) {
char message[64];
int length = snprintf(message, sizeof(message), "TEMP:%.1f,HUMID:%.1f\n",
receivedData.temperature, receivedData.humidity);
HAL_UART_Transmit(&huart2, (uint8_t *)message, length, HAL_MAX_DELAY);
}
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
UART2_Init();
ADC1_Init();
sensorDataQueue = xQueueCreate(10, sizeof(SensorData)); // очередь на 10 элементов
// Создание задач с разными приоритетами — более высокий приоритет важнее
xTaskCreate(SensorTask, "Sensor", 256, NULL, 2, NULL);
xTaskCreate(UartTransmitTask, "UartTx", 256, NULL, 1, NULL);
vTaskStartScheduler(); // запуск планировщика FreeRTOS — управление никогда не возвращается сюда
while (1) {} // эта строка теоретически никогда не должна выполниться
}
Пояснения к коду
Разделение на SensorTask и UartTransmitTask показывает основной выигрыш FreeRTOS по сравнению с единственным циклом while(1) — каждая задача написана так, как если бы она была единственной во всей системе (со своим собственным, концептуально «бесконечным» циклом и блокирующими вызовами вроде vTaskDelay()/xQueueReceive()), но планировщик FreeRTOS автоматически переключается между ними, давая процессорное время той задаче, которая в данный момент готова к выполнению, и переходя к другой задаче, когда текущая блокируется в ожидании (таймера или данных в очереди). xQueueSend()/xQueueReceive() дают потокобезопасный канал передачи структурированных данных между задачами — SensorTask не обязана знать ничего о том, кто и как обрабатывает отправленные ею данные, что обеспечивает то же разделение ответственности, что и паттерн сигналов/слотов в Qt-многопоточности, но применительно к значительно более ограниченной среде микроконтроллера.
Подводные камни
- Выбор слишком малого размера стека для задачи (
256слов в примере — значение, которое в реальном проекте требует точной настройки под конкретные потребности конкретной задачи) — переполнение стека задачи FreeRTOS, особенно без включённой защиты переполнения стека (configCHECK_FOR_STACK_OVERFLOW), может привести к трудно диагностируемому, непредсказуемому повреждению памяти соседних структур данных, а не к понятной, явной ошибке; размер стека каждой задачи стоит подбирать с запасом и явно проверять через включённые средства диагностики переполнения стека во время разработки. - Использование блокирующих HAL-вызовов (
HAL_Delay(),HAL_UART_Transmit()сHAL_MAX_DELAY) внутри FreeRTOS-задачи без понимания, что эти вызовы блокируют не только текущую задачу, но потенциально и весь процессор, в зависимости от того, как именно реализована блокировка — для полноценной многозадачности стоит использовать FreeRTOS-совместимые аналоги (vTaskDelay()вместоHAL_Delay(), что и показано в примере) везде, где это возможно, поскольку именноvTaskDelay()явно сообщает планировщику, что текущая задача готова уступить процессорное время другим задачам на время ожидания, в отличие отHAL_Delay(), обычно реализованного через простое активное ожидание, блокирующее весь процессор независимо от FreeRTOS. - Создание слишком большого количества задач с одинаковым, не продуманным приоритетом, без явного понимания, как круговое планирование FreeRTOS (round-robin) распределяет процессорное время между задачами равного приоритета — для системы с реальными требованиями к времени отклика критичных задач приоритеты должны быть продуманы осознанно, отражая реальную важность каждой задачи, а не назначаться произвольно или оставляться равными «по умолчанию» для всех задач.
- Игнорирование настройки
configTOTAL_HEAP_SIZE(общего объёма памяти, доступного FreeRTOS для динамического выделения стеков задач, очередей и других объектов RTOS) относительно реального, ограниченного объёма RAM конкретного используемого чипа STM32 — слишком большое значение этой настройки относительно реальной физической RAM либо не позволит системе вообще запуститься, либо, что хуже, может конфликтовать с памятью, используемой остальным кодом приложения, и эту настройку стоит явно согласовывать с объёмом RAM, документированным для конкретной используемой модели STM32.