Bare Metal vs FreeRTOS

Ітак, ви маєте ідею, маєте плату, потрібно створити прототип пристрою. Ви знаєте схемотехніку, знаєте програмування ( зазвичай С), створили макет пристрою і потрібно вдихнути в нього життя — написати програму.

У вас є два шляхи — створити програму самостійно (Bare Metal ), чи використати доступну операційну систему ( зазвичай FreeRTOS). Вибір варіанту залежить в першу чергу від пристрою — якщо процеси в пристрої потрібно обробляти швидше 1 мілісекунди і цей процес не один, а декілька — RTOS вам, мабуть, уже не підійде. Наприклад, ядро FreeRTOS має об’єм біля 6 KB і квантується ( виконується) кожну мілісекунду ( в default конфігурації).

Embedded розробнику в такій ситуації потрібно знати схемотехніку, мову програмування і також вчитися користуватися RTOS, але неможливо знати все. При цьому в кожній програмі є помилки, в більшості випадків це недописані макроси, і без належного досвіду з цим важко розібратися.

В Bare Metal програмі присутній лише код виконавця, він тут цар і бог в одній особі. Йому лише потрібно правильно організувати програму — основне визначити пріоритети, розбити програму на задачі і максимально запаралелити процеси, по максимуму використовуючи існуючу в мікроконтролері периферію.

Задачею цієї статті є можливість показати алгоритм організаціїї Bare
Metal програми таким чином, щоб вона виконувалася подібно RTOS, але повністю під вашим контролем.

Допустим, ви розбили програму на 4 задачі:

Task1 — самий високий пріоритет
Task2 — високий пріоритет
Task3 — середній пріоритет
Task4 — низький пріоритет

Cтворюємо глобальні змінні uint32_t Stat_Main і Stat_Work. Бітові поля
Stat_Main мають наступне значення:

Біт 3
— Запит на виконання Task1
Біт 2-1-0
— Стан (кроки) виконання Task1
/**/
Біт 7
— Запит на виконання Task2
Біт 6-5-4
— Стан виконання Task2
/**/
Біт 11
— Запит на виконання Task3
Біт 10-9-8
— Стан виконання Task3
/**/
Біт 15
— Запит на виконання Task4
Біт 14-13-12 — Стан виконання Task4
Біти 31..27 використовуються для тимчасової зміни пріоритету ( поки не виконається задача, яка запросила екстренне виконання).

Для контролю за часом виконання задач створюємо програмний або
апаратний тімер і для кожної задачі ставимо в таймері контрольний час
виконання. Якщо в якійсь задачі час очікування вийшов, таймер ставить біт
зміни пріоритету для цієї задачі і вона виконується в першу чергу.

Створюємо допоміжні константи для установлення та скиду біта запиту.

const uint32_t Set_Q1 = 0×00000008;
const uint32_t Set_Q2 = 0×00000080;const uint32_t Set_Q3 = 0×00000800;
const uint32_t Set_Q4 = 0×00008000;
const uint32_t RSet_Q1 = 0xfffffff7;
const uint32_t RSet_Q2 = 0xffffff7f;
const uint32_t RSet_Q3 = 0xffffff7ff;
const uint32_t RSet_Q4 = 0xfffff7fff;
//
const uint32_t Mask_OverTime = 0xf0000000;

Task1 найбільш пріоритетна і коротка задача (обробка даних АЦП, аварійні ситуації), вона зазвичай готовить дані для менш пріоритетних задач.

Зміни для запиту виконання вносятся в Status; Stat_Work є копією Stat_Main і використовується оператором switch(); Формуємо змінну Stat_Main для оператора switch();

if((Stat_Main & Mask_OverTime) == 0) { //Stat_Work = Stat_Main; if((Stat_Main & Set_Q3) > 0×00U) { Stat_Work = (Stat_Main & RSet_Q4); } if((Stat_Main & Set_Q2) > 0×00U) { Stat_Work = (Stat_Main & RSet_Q3 & RSet_Q4); } if((Stat_Main & Set_Q1) > 0×00U) { Stat_Work = (Stat_Main & RSet_Q2 & RSet_Q3 & RSet_Q4); /* High } } else { Stat_Work = (Stat_Main & 0×30000000); } switch(Stat_Work) { case 0×00U: // задача з найбільшим пріоритетом {} break; case 0×01U: {} break; default: {} } 

Алгоритм такий — більше пріоритетна задача маскує виконання менше пріоритетної і так до максимального пріоритету. Задача (або частина задачі) виконується і знімає свій біт вибору в глобальній змінній Stat_Main і (можливо) запускає іншу задачу, обумовлену логікою роботи пристрою.Зазвичай переривання модифікують Stat_Main, обумовлюючи роботу пристрою. Таким чином можна реалізувати алгоритм роботи пристрою без чужого коду (RTOS) швидше і надійніше і з контроле

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

часто тебе нужно вытеснение задачи (если она потенциально может длится долго), временные гарантии такого вытеснения и, естественно, выполнять много кода в прерывании ты тоже не хочешь
и, очевидно, что писать линейный код, ни о чем не парясь, сильно проще, чем разрывать его на куски через FSM, чтобы он не отжирал за раз сильно много времени

Ембедед розробник може використовувати підхід при якому одна за одною в мейнлупі дьоргаються функції тасок, кожна функція одна ітерація задачі по алгоритму кінцевих автоматів. Фріртос в 99.9% не потрібна, бо не дає жодних переваг по суті. В усіх випадках це було просто збільшення об’єму коду і додаткові проблеми пов’язані з наявністю фріртос.

RTOS надає кожному пріоритету свій стек. Приклад:
— Маємо задачу, котра довго ковирсається з флешом (наприклад, сортує телефонну книгу)
— Маємо логіку дзвінків з купою дрібних задач
— Маємо взаємодію з радіочастиною, де треба швидко виймати дані з та пушити дані в регістри, бо інакше поб’ються пакети

* Зараз працює в бекграунді сортування з флешом.
* Прийшов інтерапт від радіочастини.
* Обробник інтерапту переписав дані з регістрів до статичного буфера і сигналізував високопріоритетну задачу.
* Скедюлєр RTOS при виході з інтерапту побачив, що є високопріоритетний потік, що має активну задачу і запустив її.
* Задача аллокувала пам’ять, скопіювала туди дані зі статичного буфера, надіслала це меседжом на актор з бізнес-логікою і сіла чекати наступного інтерапту, котрий покладе ще даних до буфера.
* Скедюлєр бачить, що є меседж для задачі середнього пріоритета і передає їй управління. Актори з бізнес-логікою щось собі думають, обробляють, і в результаті кидають відповідь меседжем на радіочастину.
* Щойно з’являється меседж для високопріоритетного потоку, потік з бізнес-логікою переривається і запускається високопріоритетний потік, котрий пушає дані в регістри радіо.
* Коли регістри заповнені, радіопотік вийшов, і бізнес-логіка продовжує бігти з того місця, де вона відсилала меседж.
* Коли радіо схавало що було в регістрах, інтерапт перериває бізнес-логіку й додає в регістри ще даних з меседжу на відправку.
* Коли бізнес-логіка завершила усі свої задачі, сортування файлів на флеші продавжується рівно з того місця, де воно було, коли стався інтерапт з вхідним пакетом на радіоінтерфейсі.

По суті, замість того, щоб мати один цикл та інтерапти, ти отримав 3 цикли, кожен з котрих є інтераптом для інших, котрі мають більш низький пріоритет.
micrium.atlassian.net/...​347/Preemptive Scheduling

мені здається, що у кожного хто ковиряє ембеддед є ще третій варіант, якась своя саморобочка на лупі та task queue яка вміє менджети таски, зазвичай для чогось простого типу клавіатура-екранчик-модем-сенсори цього вистачає і воно у рамках сумісності на рівні С++ мігрує з проекта на проєкт і на нові платформи

class Runlet {
public:
  Runlet(String &name, int runletId) : _state(Okay), _stateInfo("Booting"), _name(name), _runletId(runletId) {
  }
  Runlet(const char *name, int runletId) : _state(Okay), _stateInfo("Booting"), _name(name), _runletId(runletId) {
  }
  virtual ~Runlet() {
  }

  String getName() const {
    return _name;
  }
  int getRunletId() const {
    return _runletId;
  }

  TaskDisplayState getState() const {
    return _state;
  }
  String getStateInfo() const {
    return _stateInfo;
  }

  virtual bool setup(System &system) = 0;
  virtual bool loop(System &system)  = 0;

protected:
  TaskDisplayState _state;
  String           _stateInfo;

private:
  String _name;
  int    _runletId;
};

class RunletManager {
public:
  RunletManager();
  ~RunletManager() {
  }

  void                 addRunlet(Runlet *runlet);
  void                 addAlwaysRunRunlet(Runlet *runlet);
  std::list<Runlet *>  getRunlets();

  bool setup(System &system);
  bool loop(System &system);

private:
  std::list<Runlet *>           _runlets;
  std::list<Runlet *>::iterator _nextRunlet;
  std::list<Runlet *>           _alwaysRunRunlets;
};

Підписатись на коментарі