kit8nino/RTOS.md

29 KiB
Raw Permalink 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 должна отвечать быстро, иначе хост отвалится.

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