kit8nino/RTOS.md

29 KiB
Raw Blame History

Слушай, хочешь расскажу про операционки реального времени? Тема на первый взгляд сложная, но на самом деле это как диспетчер на стройке, только для крошечных мозгов микроконтроллеров. Я сам когда-то путался, а сейчас объясню так, что сам будешь друзьям пересказывать.

Как вообще появились эти RTOS?

На самом деле идее уже полвека. В 60-х годах, когда NASA собиралось лететь на Луну, им понадобился компьютер, который мог бы обрабатывать кучу датчиков и реагировать мгновенно — никаких тебе «подожди, я загружаюсь». Так появилась первая RTOS для Apollo Guidance Computer. Потом, в 8090-х, подтянулись военные и промышленники: VxWorks, QNX, pSOS — всё это было зверски дорого и закрыто. Обычному студенту или стартапу такое не светило. И только в 2003 году парень по имени Ричард Бэрри сказал: «А почему бы не сделать бесплатную и открытую RTOS для всех?» Так родился FreeRTOS. Сейчас она принадлежит Amazon, но код по‑прежнему бесплатный, и её ставят куда угодно — от китайских датчиков до марсоходов.

Суть на пальцах

Представь, что ты бригадир на стройке, а у тебя в распоряжении всего одна дрель. И куча рабочих, которым эта дрель нужна: один должен дыру сверлить, другой — шуруп закрутить, третий — вообще срочно выключатель починить, а то проводка заискрит. Обычная операционка (как Windows на твоём ноуте) сказала бы: «Ждите, пока первый закончит, потом второй, потом третий». Но если третий — экстренный случай, проводка уже дымится, а Windows ещё и «Пожалуйста, подождите, устанавливаются обновления» — пиши пропало.

RTOS работает иначе. Она назначает каждому заданию приоритет. Если задача с самым высоким приоритетом (потушить пожар) просыпается, планировщик тут же отбирает дрель у того, кто сверлит, и отдаёт спасателю. Как только опасность устранена — возвращаем дрель обратно. Всё это происходит за миллисекунды, и никто не замечает подмены.

Как это устроено внутри (схема)

Чтобы ты визуально понял, набросал схемку. Тут три задачи с разными приоритетами, и планировщик решает, кто сейчас работает.

graph TD
    Start[Старт системы] --> Init[Инициализация RTOS]
    Init --> Scheduler{Планировщик}
    
    subgraph "Задачи"
        Task1[Задача 1: выс. приоритет<br>Аварийное отключение]
        Task2[Задача 2: ср. приоритет<br>Чтение датчика]
        Task3[Задача 3: низк. приоритет<br>Мигание светодиодом]
    end

    Scheduler -->|приор. 1| Task1Running[Выполняется Задача1]
    Scheduler -->|приор. 2| Task2Running[Выполняется Задача2]
    Scheduler -->|приор. 3| Task3Running[Выполняется Задача3]

    Task1Running --> Event{Прерывание таймера<br>или событие}
    Task2Running --> Event
    Task3Running --> Event
    Event --> Scheduler
    
    style Scheduler fill:#f9f,stroke:#333,stroke-width:2px
    style Event fill:#bbf,stroke:#333

Каждая задача может находиться в одном из состояний: «Готова к работе» (Ready), «Выполняется» (Running) или «Ждёт события» (Blocked). Планировщик (это розовый ромбик) постоянно смотрит: кто сейчас самый важный из готовых? И переключает контекст — сохраняет регистры одной задачи, загружает регистры другой. Это происходит так быстро, что кажется, будто задачи работают одновременно.

Живой пример: кофемашина с экраном

Представь умную кофемашину. В ней одновременно должны работать:

  • Задача 1 (Термостат): следить за температурой воды. Если вода остыла — срочно включить нагрев. Приоритет — самый высокий, иначе кофе будет холодным.
  • Задача 2 (Кнопки и дисплей): реагировать на нажатия, рисовать анимацию. Приоритет средний — можно чуть‑чуть подождать, пока вода греется.
  • Задача 3 (Логирование статистики): записывать во flash-память, сколько чашек сварили. Приоритет низкий — делать, когда все важные дела переделаны.

RTOS следит, чтобы нагреватель включался моментально, а мигание экрана никого не отвлекало. Если ты нажмёшь кнопку «Капучино», система среагирует за десятки миллисекунд — никаких тебе «зависших» интерфейсов.

FreeRTOS против коммерческих монстров

Ты наверняка слышал про VxWorks (ставят в беспилотники и марсоходы) или QNX (используется в автомобилях и медицинских приборах). Они реально мощные, но:

  • Цена: лицензии стоят миллионы долларов, документация — под NDA.
  • Сложность: чтобы просто запустить пример, нужна команда профи.

FreeRTOS же:

  • Бесплатный и открытый: можешь залезть в код и посмотреть, как он там тикает.
  • Легчайший: ядро занимает 49 килобайт оперативки. Помещается даже в самый дешёвый STM32 за 50 рублей.
  • Массовый: если возникнет проблема — гугл выдаст тысячу готовых решений. На нём строят свои проекты и студенты, и SpaceX.

Поэтому, если ты хочешь сделать умный замок, метеостанцию или робота‑пылесоса, бери FreeRTOS. Это индустриальный стандарт, с которым ты не пропадёшь.

Итог

RTOS — это не какая‑то страшная магия, а просто очень дисциплинированный диспетчер, который не даёт задачам мешать друг другу. Он знает, что важно сейчас, а что может подождать. Благодаря ему даже дешёвый микроконтроллер способен одновременно варить кофе, рисовать на экране и записывать статистику — и всё это с гарантированным временем отклика.

Если хочешь, следующим заходом расскажу, как с нуля собрать проект на FreeRTOS в CubeIDE или VSCode. Там тоже всё просто, главное — понять принцип.

Создание проекта с FreeRTOS

О, отличный запрос! Из теории про бригадира с дрелью мы переходим к суровой практике — как этого "диспетчера" (FreeRTOS) поселить в твоём VSCode и заставить работать на железе. Расскажу по шагам, как я сам это делаю.

🎯 Самый простой и современный путь (рекомендую)

Если честно, раньше это была боль — надо было ручками пути прописывать, компиляторы настраивать. Но сейчас STMicroelectronics сделала офигенное официальное расширение "STM32Cube for Visual Studio Code" . Оно берёт на себя всю грязную работу.

Что ставим

  1. STM32CubeMX — это программа, где мы мышкой тыкаем, какие ножки на что задействовать и включаем FreeRTOS галочкой.
  2. VSCode с расширением "STM32Cube for VS Code" (ищешь прямо в маркете расширений).
  3. ARM тулчейн — проще всего поставить STM32CubeCLT (Command Line Tools) с сайта ST. Там и компилятор, и программатор в одном флаконе .

Процесс сборки проекта

Шаг 1. Генерируем основу в CubeMX Открываешь CubeMX, выбираешь свой контроллер (например, STM32F103C8 — классика для старта). Во вкладке Pinout находишь MiddlewareFreeRTOS и включаешь его (я советую версию CMSIS_V2 — она современнее) .

Дальше самое важное: идёшь во вкладку Project Manager:

  • Toolchain/IDE выбираешь Makefile (это ключевой момент! именно так VSCode поймёт структуру) .
  • Жмёшь GENERATE CODE. На выходе получаешь папку с исходниками, драйверами HAL и FreeRTOS.

Шаг 2. Открываем в VSCode Запускаешь VSCode, открываешь папку с проектом. Расширение STM32 само предложит импортировать проект — соглашаешься. Оно прочитает Makefile, подхватит все пути к заголовочным файлам, настроит автодополнение (IntelliSense) .

Шаг 3. Компилируем и шьём Внизу слева появится панелька STM32. Там кнопки:

  • Build — собрать проект.
  • Run — прошить в контроллер. Если у тебя плата Nucleo или Discovery с встроенным программатором ST-Link, всё заработает из коробки .

🛠️ Альтернативный путь (EIDE)

Если по каким-то причинам официальное расширение не зашло, есть классный плагин Embedded IDE (EIDE) . Принцип похожий:

  • В CubeMX генерируешь Makefile проект.
  • В VSCode ставишь EIDE, создаёшь новый проект "STM32 Cube Project" и ручками копируешь в него папки Core, Drivers, Middlewares из сгенерированного проекта .
  • В EIDE нужно будет добавить пути к заголовочным файлам и определить макросы (типа USE_HAL_DRIVER, STM32F407xx) — это как раз те галочки, которые в CubeMX проставляются .

⚠️ Две типичные проблемы (чтобы ты не матерился)

Когда будешь собирать, могут вылезти две классические ошибки. Я их проходил, так что вот решение.

1. Ошибка компиляции "FPU does not support instruction"

Симптом: компилятор ругается на инструкции vstmdb, vldmia в файле port.c . Причина: у твоего контроллера (например, STM32F4) есть аппаратное FPU (сопроцессор для float), а компилятор пытается собрать без поддержки float'ов. Лечение: в настройках компилятора нужно включить аппаратный float. В EIDE или в расширении STM32 ищешь опцию FPU и ставишь fpv4-sp-d16 (для Cortex-M4) или соответствующую твоему камню .

2. Ошибка линковки "syntax error" в .ld файле

Симптом: линковщик падает с ошибкой в файле *_FLASH.ld на строках типа _estack = ORIGIN() + LENGTH(); . Причина: CubeMX генерирует кривоватый скрипт линковки — забывает указать, откуда брать начало памяти. Лечение: открываешь .ld файл и правишь 4 места :

// Было:
_estack = ORIGIN() + LENGTH();
// Стало:
_estack = ORIGIN(RAM) + LENGTH(RAM);

// Было (в секции .data):
} > AT> FLASH
// Стало:
} >RAM AT> FLASH

// Было (в секции .bss и в конце файла):
} >
// Стало:
} >RAM

После этих правок всё собирается на ура.

🐞 Отладка и RTOS View

Самое крутое, что VSCode умеет показывать, что творится внутри FreeRTOS в реальном времени. Ставишь расширение Cortex-Debug , запускаешь отладку (F5) и в панели Debug открываешь RTOS View . Там увидишь список задач, их приоритеты, состояние (Running, Ready, Blocked) — прямо как в учебнике! Безумно полезно, когда что-то виснет.

Короткий итог

  1. CubeMX → генерируешь проект с FreeRTOS и Makefile.
  2. VSCode + официальное расширение STM32 → открываешь, магия автоконфигурации.
  3. Правишь .ld файл (4 строчки) и включаешь FPU.
  4. Жмёшь Build, потом Run — и твоя первая многозадачная прошивка летит в контроллер.

Дальше уже можно писать задачи — создавать их через xTaskCreate, настраивать очереди (queues) для общения между задачами, и всё это дебажить с RTOS View. Если хочешь, следующим шагом расскажу, как реально развести несколько мигающих светодиодов разными задачами — это как "Hello, World!" в мире RTOS.

IMU-мышь с FreeRTOS

Отличная идея для проекта! Это классическая задача, где FreeRTOS раскрывается во всей красе: нужно одновременно читать сенсор по I2C, обрабатывать данные и эмулировать USB-устройство. Поехали.

🧠 Архитектура проекта: как это будет работать

Прежде чем писать код, давай прикинем структуру. У нас будет несколько задач (tasks), которые общаются между собой через очереди (queues). Это самый наглядный способ понять межзадачное взаимодействие в RTOS.

graph TD
    subgraph "Задачи FreeRTOS"
        A[Task_ReadIMU<br>приоритет 2]
        B[Task_ProcessData<br>приоритет 1]
        C[Task_USB_HID<br>приоритет 2]
    end
    
    subgraph "Очереди"
        Q1["Queue_RawData<br>вектор (ax,ay,az,gx,gy,gz)"]
        Q2["Queue_MouseDelta<br>dx, dy, кнопки"]
    end
    
    A -->|"отправляет сырые данные"| Q1
    Q1 -->|"забирает на обработку"| B
    B -->|"отправляет движения"| Q2
    Q2 -->|"забирает и шлёт по USB"| C
    
    IMU[(MPU6050<br>сенсор)] -->|I2C| A
    C -->|"HID Report"| USB[(USB<br>компьютер)]
    
    style A fill:#f9f,stroke:#333
    style B fill:#bbf,stroke:#333
    style C fill:#9f9,stroke:#333

Как это работает:

  • Task_ReadIMU — зудит по таймеру, читает акселерометр и гироскоп, кидает в очередь .
  • Task_ProcessData — забирает данные, превращает сырые значения в смещение мыши (dx, dy), определяет клики (по резкому ускорению).
  • Task_USB_HID — самый ответственный, отправляет HID-отчёты в компьютер.

🛠️ Шаг 1. Настройка проекта в CubeMX

1.1. Выбираем контроллер и периферию

Запускаешь CubeMX, создаёшь проект под STM32F401CCU6 (Black Pill). Включаем:

  • RCC → HSE: Crystal/Ceramic Resonator .
  • Clock Configuration — разгоняем до 84 MHz (это штатная частота для F401).
  • I2C1 (или любой другой) — для подключения MPU6050. Пины: обычно PB8 (SCL), PB9 (SDA). Скорость 100 kHz .
  • TIM2 — для тактирования чтения сенсора (скажем, 100 Гц). Prescaler и Counter Period считаешь под свою частоту .
  • USB — включаем USB_OTG_FS в режиме Device_Only.
  • MiddlewareUSB_DEVICE → выбираем Human Interface Device Class (HID). Там можно выбрать готовый шаблон Mouse.
  • FreeRTOS — включаем, версию CMSIS_V2 (она проще и современнее) .

1.2. Настройка FreeRTOS

Во вкладке Pinout & ConfigurationMiddlewareFreeRTOS:

  • В Task and Queues создаём три задачи с именами (например, defaultTask переименовываем в TaskReadIMU, добавляем TaskProcessData и TaskUSBMouse).
  • Создаём две очереди: QueueRawData (длина 5, размер — под структуру с float-ами) и QueueMouseDelta (длина 3, размер — под структуру с int16_t).

1.3. Генерируем код

В Project Manager:

  • Toolchain/IDEMakefile (чтобы потом работать в VSCode) .
  • Жмём GENERATE CODE.

📝 Шаг 2. Пишем код (самое интересное)

2.1. Структуры данных

В каком-нибудь заголовочном файле (например, imu_data.h) определяем:

typedef struct {
    float ax, ay, az;      // акселерометр
    float gx, gy, gz;       // гироскоп
} IMU_RawData_t;

typedef struct {
    int16_t dx;             // смещение по X
    int16_t dy;             // смещение по Y
    uint8_t buttons;        // биты: 0x01 — левая кнопка
} MouseReport_t;

2.2. Задача чтения IMU (TaskReadIMU)

В файле TaskReadIMU.c (или в коде, сгенерированном CubeMX внутри /* USER CODE BEGIN */):

void TaskReadIMU(void *argument) {
    IMU_RawData_t raw;
    uint32_t tick = osKernelGetTickCount();  // для точных интервалов

    for(;;) {
        // Читаем акселерометр (регистры 0x3B..0x40)
        uint8_t accel_data[6];
        HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, 0x3B, 1, accel_data, 6, HAL_MAX_DELAY);
        
        // Преобразуем два байта в 16-битное число, затем в g (чувствительность ±2g = 16384 LSB/g)
        raw.ax = (int16_t)((accel_data[0] << 8) | accel_data[1]) / 16384.0f;
        raw.ay = (int16_t)((accel_data[2] << 8) | accel_data[3]) / 16384.0f;
        raw.az = (int16_t)((accel_data[4] << 8) | accel_data[5]) / 16384.0f;

        // Читаем гироскоп (регистры 0x43..0x48)
        uint8_t gyro_data[6];
        HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, 0x43, 1, gyro_data, 6, HAL_MAX_DELAY);
        raw.gx = (int16_t)((gyro_data[0] << 8) | gyro_data[1]) / 131.0f; // для ±250°/s
        raw.gy = (int16_t)((gyro_data[2] << 8) | gyro_data[3]) / 131.0f;
        raw.gz = (int16_t)((gyro_data[4] << 8) | gyro_data[5]) / 131.0f;

        // Отправляем в очередь
        osMessageQueuePut(QueueRawDataHandle, &raw, 0, 0);

        // Ждём строго 10 мс (100 Гц)
        osDelayUntil(tick, 10);
    }
}

Примечание: это упрощённый вариант. В реальности нужно учесть, что регистры MPU6050 читаются последовательно, и правильно обработать ошибки I2C .

2.3. Задача обработки данных (TaskProcessData)

Здесь мы превращаем сырые данные в движение мыши. Идея: по углу наклона платы (из акселерометра) определяем скорость курсора.

void TaskProcessData(void *argument) {
    IMU_RawData_t raw;
    MouseReport_t mouse = {0, 0, 0};
    const float threshold = 0.3f;  // порог для клика

    for(;;) {
        // Ждём данные из очереди
        osMessageQueueGet(QueueRawDataHandle, &raw, NULL, osWaitForever);

        // Вычисляем углы наклона (в радианах)
        float pitch = atan2(-raw.ax, sqrt(raw.ay*raw.ay + raw.az*raw.az));
        float roll  = atan2(raw.ay, raw.az);

        // Превращаем углы в смещение курсора (коэффициенты подбираются)
        mouse.dx = (int16_t)(roll * 100);
        mouse.dy = (int16_t)(pitch * 100);

        // Определяем клик: если есть резкое ускорение по Z
        if (raw.az < -1.5f) {  // тряхнули платой вниз
            mouse.buttons = 0x01;  // левая кнопка
        } else {
            mouse.buttons = 0x00;
        }

        // Отправляем в очередь для USB-задачи
        osMessageQueuePut(QueueMouseDeltaHandle, &mouse, 0, 0);
    }
}

2.4. Задача USB HID (TaskUSBMouse)

Тут самое простое — берём готовый HID-класс от ST и шлём отчёты.

extern USBD_HandleTypeDef hUsbDeviceFS;  // глобальный дескриптор USB

void TaskUSBMouse(void *argument) {
    MouseReport_t mouse;

    for(;;) {
        // Ждём новые данные
        osMessageQueueGet(QueueMouseDeltaHandle, &mouse, NULL, osWaitForever);

        // Формируем HID-отчёт (4 байта: кнопки, X, Y, колесо)
        uint8_t hid_report[4] = {
            mouse.buttons,
            (uint8_t)(mouse.dx & 0xFF),
            (uint8_t)(mouse.dy & 0xFF),
            0  // колесо не используем
        };

        // Отправляем через USB
        USBD_HID_SendReport(&hUsbDeviceFS, hid_report, 4);
    }
}

🔧 Шаг 3. Настройка в VSCode и сборка

3.1. Открываем проект

Запускаешь VSCode, открываешь папку с проектом. Если у тебя установлено расширение STM32Cube for VS Code, оно само подхватит настройки. Если нет — придётся немного подредактировать Makefile, как в статье про добавление C++ файлов , но у нас всё на C, так что должно собраться и так.

3.2. Важный нюанс с USB

В сгенерированном коде от CubeMX уже есть файл usbd_custom_hid_if.c. Там нужно поправить структуру отчёта, чтобы она соответствовала тому, что мы шлём. В функции USBD_CUSTOM_HID_ReportDesc_FS должно быть примерно так:

static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] = {
    0x05, 0x01,        // Usage Page (Generic Desktop)
    0x09, 0x02,        // Usage (Mouse)
    0xA1, 0x01,        // Collection (Application)
    0x09, 0x01,        //   Usage (Pointer)
    0xA1, 0x00,        //   Collection (Physical)
    0x05, 0x09,        //     Usage Page (Button)
    0x19, 0x01,        //     Usage Minimum (1)
    0x29, 0x03,        //     Usage Maximum (3)
    0x15, 0x00,        //     Logical Minimum (0)
    0x25, 0x01,        //     Logical Maximum (1)
    0x95, 0x03,        //     Report Count (3)
    0x75, 0x01,        //     Report Size (1)
    0x81, 0x02,        //     Input (Data,Var,Abs)
    0x95, 0x01,        //     Report Count (1)
    0x75, 0x05,        //     Report Size (5)
    0x81, 0x03,        //     Input (Const,Var,Abs)
    0x05, 0x01,        //     Usage Page (Generic Desktop)
    0x09, 0x30,        //     Usage (X)
    0x09, 0x31,        //     Usage (Y)
    0x16, 0x00, 0x80,  //     Logical Minimum (-32768)
    0x26, 0xFF, 0x7F,  //     Logical Maximum (32767)
    0x75, 0x10,        //     Report Size (16)
    0x95, 0x02,        //     Report Count (2)
    0x81, 0x06,        //     Input (Data,Var,Rel)
    0xC0,              //   End Collection
    0xC0               // End Collection
};

Этот дескриптор говорит, что мы шлём 3 кнопки (по 1 биту), затем 2 относительных значения по 16 бит — X и Y.

🐞 Отладка: смотрим, что внутри

Самое крутое, что VSCode с расширением Cortex-Debug и RTOS View позволяет заглянуть внутрь FreeRTOS . Ты увидишь:

  • Все три задачи, их приоритеты и состояние (Running, Ready, Blocked).
  • Очереди: сколько элементов внутри, сколько свободно.
  • Если какая-то задача зависла — сразу видно.

Также в коде можно использовать printf через UART (как в статье про _write) , чтобы выводить отладочную информацию, пока мышь не заработала.

🎯 Что тут тренируется в FreeRTOS

  1. Создание задач с разными приоритетами.
  2. Очереди — передача данных между задачами (сырые данные → обработка → USB).
  3. Таймеры — задача чтения IMU работает по строгому расписанию (100 Гц).
  4. Межзадачное взаимодействие без глобальных переменных и гонок данных.
  5. Реальное время — задача USB должна отвечать быстро, иначе хост отвалится.

Попробуй собрать — и у тебя получится настоящая беспроводная (по проводу, но всё же) мышь из платы и датчика от дрона. Если что-то зависнет — пиши, разберёмся.