FreeRTOS — операционная система реального времени

от автора

Существует целый ряд классов задач, которые должны быть выполнены за строго определённое время и не более. Среди подобных задач можно назвать: срабатывание предохранительных клапанов, катапультирование пилота из терпящего бедствие воздушного судна, срабатывание подушек безопасности у автомобиля, промышленную робототехнику и т.д. В сфере программного обеспечения также существует целый ряд задач, которые должны быть выполнены точно срок — для этого и служат операционные системы реального времени, одной из которых является FreeRTOS. Именно о ней мы и поговорим в этой статье.

Говоря об операционных системах реального времени, можно условно разделить их на:

  • системы жёсткого реального времени;
  • системы мягкого реального времени.

Где ОС жёсткого реального времени — используются для решения задач, описанных выше, то есть требующих выполнения за строго определённое время и не более, так как превышение этого времени может вызвать катастрофические последствия.

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

Поговорим об использовании ОС реального времени в микроконтроллерах, тут следует начать с главного: а для чего вообще нужно использовать операционную систему для микроконтроллеров? Потому что именно этот вопрос зачастую возникает у каждого, кто задумывается о чём-то более сложном, чем просто код.

И на него можно ответить достаточно просто: подобная операционная система вам нужна, если вы разрабатываете достаточно сложную систему, которая обладает множеством функций. Причём ситуация может усугубляться ещё и тем, что эти функции являются достаточно разнородными: отображение информации на экране, межсетевой обмен, постоянный опрос ряда датчиков и пользовательских клавиатур и т. д. Во всём этом комплексе задач вам сможет помочь FreeRTOS. Благодаря своему достаточно богатому API и популярности, система портирована на множество архитектур и содержит подробные примеры своего использования.

Почему стоит выбрать именно эту операционную систему?

Дело в том, что она разрабатывается уже в течение 18 лет в сотрудничестве с ведущими производителями микросхем и имеет поддержку даже новейших микроконтроллеров RISC-V и ARMv8-M, есть большая экосистема разработчиков, масса настроенных примеров (нет нужды самому программировать «с нуля» — можно использовать «заготовки»).

Как заявляется на самом сайте FreeRTOS, операционная система занимает лидирующие места в каждом обзоре рынка, начиная с 2011 года. Я не поленился поднять один из таких обзоров, за 2019 год — свежее просто нет, и судя по ретроспективе подобных исследований, которые посвящены встраиваемым системам — embedded.com, они их выпускают раз в два года. Так что, по идее, должны были уже выпустить новый отчёт, но что-то там произошло, и нового отчёта пока нет.

Так вот, на 57-58 страницах этого отчёта имеется любопытная информация, касающаяся FreeRTOS. На 57 странице говорится о том, какие операционные системы разработчики по всему миру уже используют, а на 58 странице — какие операционные системы планируют использовать в последующие 12 месяцев. И действительно, FreeRTOS занимает лидирующие позиции в 37% рынка по азиатско-тихоокеанскому региону. В целом по миру система находится на втором месте, так как первое место занимает embedded linux.

Видимо, это связано с тем, что система пока не обладает таким внушительным списком поддерживаемых драйверов, а также инструментами, касающимися сети или работой с памятью, какие имеются у linux. Зато потребление ресурсов этой системы намного меньше — 0,5 КБ оперативной памяти и от 5 до 10 КБ ПЗУ. О чём, кстати говоря, упоминает и обзор встраиваемых систем от itweek.

И кроме того — она абсолютно бесплатна!

В рамках FreeRTOS возможно достижение многозадачности, когда каждая отдельная задача может функционировать без оглядки на все остальные, то есть, можно сказать, что в основе FreeRTOS лежит ключевое понятие «самостоятельной задачи».

Подобная многозадачность достигается за счёт того, что задачи выполняются поделёнными на маленькие частички, каждая из которых обрабатывается за один так называемый «тик таймера». То есть когда один из таймеров микроконтроллера один раз «тикнул», произошло прерывание, сработал диспетчер, который выполнил маленькую часть одной из параллельно работающих задач, после чего подобный цикл повторился ещё и ещё раз.

Говоря о задачах, следует сказать, что в этой операционной системе они представляют из себя бесконечные циклы без выхода из них.

Для создания задачи используется функция xTaskCreate (), характеристики которой выглядят следующим образом:

  BaseType_t xTaskCreate(    TaskFunction_t pvTaskCode,                             const char * const pcName,                             configSTACK_DEPTH_TYPE usStackDepth,                             void *pvParameters,                             UBaseType_t uxPriority,                             TaskHandle_t *pxCreatedTask                           );

Если параметры этой задачи представить на русском языке, то выглядеть она будет следующим образом:

 BaseType_t xTaskCreate( Указатель на функцию,                             имя_задачи,                             размер_стека_под_задачу,                             параметры_задачи,                             приоритет_задачи,                             ссылка_на_задачу_для_передачи_наружу                           );

Одним из довольно спорных и сложных вопросов для понимания является — а насколько большим нужно делать стек, выделяемый под задачу и указываемый в её параметрах? Поэтому на этот счёт был написан достаточно развёрнутый FAQ, который можно глянуть вот по этой ссылке.

Кроме создания задачи, возможно вызвать функцию для её удаления:

void vTaskDelete( TaskHandle_t xTask );

Пример использования:

 void vOtherFunction( void )  {  TaskHandle_t xHandle = NULL;       // Create the task, storing the handle.      xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );       // Use the handle to delete the task.      if( xHandle != NULL )      {          vTaskDelete( xHandle );      }  }

Или для приостановки задачи на некоторое количество системных тиков:

void vTaskDelay( const TickType_t xTicksToDelay );

Пример использования:

 void vTaskFunction( void * pvParameters )  {  /* Block for 500ms. */  const TickType_t xDelay = 500 / portTICK_PERIOD_MS;       for( ;; )      {          /* Simply toggle the LED every 500ms, blocking between each toggle. */          vToggleLED();          vTaskDelay( xDelay );      } }

В приведённом ниже коде вы можете видеть, как происходит работа с рассмотренными выше концепциями создания задачи, а также включения задержки. Этот код мигает светодиодом и считывает с аналогового пина значения в бесконечном цикле. Подробные комментарии, содержащиеся в самом коде, позволяют достаточно хорошо понять происходящее и не требуют дополнительного пояснения:

Код

#include <Arduino_FreeRTOS.h>  // define two tasks for Blink & AnalogRead void TaskBlink( void *pvParameters ); void TaskAnalogRead( void *pvParameters );  // the setup function runs once when you press reset or power the board void setup() {     // initialize serial communication at 9600 bits per second:   Serial.begin(9600);     while (!Serial) {     ; // wait for serial port to connect. Needed for native USB, on LEONARDO, MICRO, YUN, and other 32u4 based boards.   }    // Now set up two tasks to run independently.   xTaskCreate(     TaskBlink     ,  "Blink"   // A name just for humans     ,  128  // This stack size can be checked & adjusted by reading the Stack Highwater     ,  NULL     ,  2  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.     ,  NULL );    xTaskCreate(     TaskAnalogRead     ,  "AnalogRead"     ,  128  // Stack size     ,  NULL     ,  1  // Priority     ,  NULL );    // Now the task scheduler, which takes over control of scheduling individual tasks, is automatically started. }  void loop() {   // Empty. Things are done in Tasks. }  /*--------------------------------------------------*/ /*---------------------- Tasks ---------------------*/ /*--------------------------------------------------*/  void TaskBlink(void *pvParameters)  // This is a task. {   (void) pvParameters;  /*   Blink   Turns on an LED on for one second, then off for one second, repeatedly.    Most Arduinos have an on-board LED you can control. On the UNO, LEONARDO, MEGA, and ZERO   it is attached to digital pin 13, on MKR1000 on pin 6. LED_BUILTIN takes care   of use the correct LED pin whatever is the board used.     The MICRO does not have a LED_BUILTIN available. For the MICRO board please substitute   the LED_BUILTIN definition with either LED_BUILTIN_RX or LED_BUILTIN_TX.   e.g. pinMode(LED_BUILTIN_RX, OUTPUT); etc.     If you want to know what pin the on-board LED is connected to on your Arduino model, check   the Technical Specs of your board  at https://www.arduino.cc/en/Main/Products     This example code is in the public domain.    modified 8 May 2014   by Scott Fitzgerald     modified 2 Sep 2016   by Arturo Guadalupi */    // initialize digital LED_BUILTIN on pin 13 as an output.   pinMode(LED_BUILTIN, OUTPUT);    for (;;) // A Task shall never return or exit.   {     digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)     vTaskDelay( 1000 / portTICK_PERIOD_MS ); // wait for one second     digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW     vTaskDelay( 1000 / portTICK_PERIOD_MS ); // wait for one second   } }  void TaskAnalogRead(void *pvParameters)  // This is a task. {   (void) pvParameters;   /*   AnalogReadSerial   Reads an analog input on pin 0, prints the result to the serial monitor.   Graphical representation is available using serial plotter (Tools > Serial Plotter menu)   Attach the center pin of a potentiometer to pin A0, and the outside pins to +5V and ground.    This example code is in the public domain. */    for (;;)   {     // read the input on analog pin 0:     int sensorValue = analogRead(A0);     // print out the value you read:     Serial.println(sensorValue);     vTaskDelay(1);  // one tick delay (15ms) in between reads for stability   } }

Как вы, наверное, уже догадались, имеющийся в наличии API не ограничивается только этими функциями. Полный список можно посмотреть здесь.

Кстати сказать, так как мы упомянули возможность приостановки задачи на n-системных тиков, в этот момент задача переходит в статус Blocked (задача простаивает).

В принципе, таких статусов только четыре: Ready (в данный момент задача запущена), Running (в данный момент задача исполняется на процессоре), Blocked (простой), Suspended (в данный момент задача полностью выключена).

image
Источник картинки: microsin

image
Полная машина состояний задачи. Источник картинки: microsin

Среди параметров создания задачи одним из интересным является приоритет. Понятие приоритета означает, в каком порядке задачи будут получать процессорное время. То есть задача с низким приоритетом его не получит до тех пор, пока задачи с более высоким приоритетом не перейдут в статус ожидания (Blocked). Причём количество возможных приоритетов не ограничено, и вы можете создавать их сами как на этапе создания задачи, в её параметрах, так и «на лету», используя функцию vTaskPrioritySet():

void vTaskPrioritySet( TaskHandle_t xTask,                        UBaseType_t uxNewPriority );

Пример использования:

void vAFunction( void )  {  TaskHandle_t xHandle;      // Create a task, storing the handle.      xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );      // ...      // Use the handle to raise the priority of the created task.      vTaskPrioritySet( xHandle, tskIDLE_PRIORITY + 1 )      // ...      // Use a NULL handle to raise our priority to the same value.      vTaskPrioritySet( NULL, tskIDLE_PRIORITY + 1 );  }

Если задачи имеют одинаковые приоритеты, то они будут выполняться в бесконечном цикле и получать процессорное время одна за другой. Количество задач с одинаковыми или разными приоритетами не ограничено.

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

Любая задача может поместить информацию в очередь: в её начало, используя функцию xQueueSendToFront():

image
Источник картинки: easyelectronics

 BaseType_t xQueueSendToFront( QueueHandle_t xQueue,                                const void * pvItemToQueue,                                TickType_t xTicksToWait );

Пример использования:

Код размещения в начало очереди

struct AMessage {     char ucMessageID;     char ucData[ 20 ]; } xMessage;  unsigned long ulVar = 10UL;  void vATask( void *pvParameters ) { QueueHandle_t xQueue1, xQueue2; struct AMessage *pxMessage;      /* Create a queue capable of containing 10 unsigned long values. */     xQueue1 = xQueueCreate( 10, sizeof( unsigned long ) );      /* Create a queue capable of containing 10 pointers to AMessage     structures.  These should be passed by pointer as they contain a lot of     data. */     xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) );      /* ... */      if( xQueue1 != 0 )     {         /* Send an unsigned long.  Wait for 10 ticks for space to become         available if necessary. */         if( xQueueSendToFront( xQueue1,                               ( void * ) &ulVar,                               ( TickType_t ) 10 ) != pdPASS )         {             /* Failed to post the message, even after 10 ticks. */         }     }      if( xQueue2 != 0 )     {         /* Send a pointer to a struct AMessage object.  Don't block if the         queue is already full. */         pxMessage = & xMessage;         xQueueSendToFront( xQueue2, ( void * ) &pxMessage, ( TickType_t ) 0 );     }      /* ... Rest of task code. */ }

или, соответственно, в её конец — xQueueSendtoBack():

 BaseType_t xQueueSendToBack(                                    QueueHandle_t xQueue,                                    const void * pvItemToQueue,                                    TickType_t xTicksToWait                                );

Пример использования:

Код размещения в конец очереди

struct AMessage {     char ucMessageID;     char ucData[ 20 ]; } xMessage;  unsigned long ulVar = 10UL;  void vATask( void *pvParameters ) { QueueHandle_t xQueue1, xQueue2; struct AMessage *pxMessage;      /* Create a queue capable of containing 10 unsigned long values. */     xQueue1 = xQueueCreate( 10, sizeof( unsigned long ) );      /* Create a queue capable of containing 10 pointers to AMessage     structures.  These should be passed by pointer as they contain a lot of     data. */     xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) );      /* ... */      if( xQueue1 != 0 )     {         /* Send an unsigned long.  Wait for 10 ticks for space to become         available if necessary. */         if( xQueueSendToBack( xQueue1,                              ( void * ) &ulVar,                              ( TickType_t ) 10 ) != pdPASS )         {             /* Failed to post the message, even after 10 ticks. */         }     }      if( xQueue2 != 0 )     {         /* Send a pointer to a struct AMessage object.  Don't block if the         queue is already full. */         pxMessage = & xMessage;         xQueueSendToBack( xQueue2, ( void * ) &pxMessage, ( TickType_t ) 0 );     }      /* ... Rest of task code. */ }

Также есть простая команда xQueueSend() — которая по своей сути эквивалентна функции, добавляющей в конец очереди.

image
Источник картинки: easyelectronics

Разумеется, работа с очередями не исчерпывается указанными командами. Полный список доступных функций находится вот здесь. До начала работы с очередью она должна быть создана с использованием команды xQueueCreate().

Код, приведённый ниже, иллюстрирует один из самых простых вариантов создания очереди:

Пример кода очереди

/*  * Example of a basic FreeRTOS queue  * https://www.freertos.org/Embedded-RTOS-Queues.html  */  // Include Arduino FreeRTOS library #include <Arduino_FreeRTOS.h>  // Include queue support #include <queue.h>  /*  * Declaring a global variable of type QueueHandle_t  *  */ QueueHandle_t integerQueue;  void setup() {    /**    * Create a queue.    * https://www.freertos.org/a00116.html    */   integerQueue = xQueueCreate(10, // Queue length                               sizeof(int) // Queue item size                               );     if (integerQueue != NULL) {          // Create task that consumes the queue if it was created.     xTaskCreate(TaskSerial, // Task function                 "Serial", // A name just for humans                 128,  // This stack size can be checked & adjusted by reading the Stack Highwater                 NULL,                 2, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.                 NULL);       // Create task that publish data in the queue if it was created.     xTaskCreate(TaskAnalogRead, // Task function                 "AnalogRead", // Task name                 128,  // Stack size                 NULL,                 1, // Priority                 NULL);        }     xTaskCreate(TaskBlink, // Task function               "Blink", // Task name               128, // Stack size               NULL,               0, // Priority               NULL );  }  void loop() {}   /**  * Analog read task  * Reads an analog input on pin 0 and send the readed value through the queue.  * See Blink_AnalogRead example.  */ void TaskAnalogRead(void *pvParameters) {   (void) pvParameters;     for (;;)   {     // Read the input on analog pin 0:     int sensorValue = analogRead(A0);      /**      * Post an item on a queue.      * https://www.freertos.org/a00117.html      */     xQueueSend(integerQueue, &sensorValue, portMAX_DELAY);      // One tick delay (15ms) in between reads for stability     vTaskDelay(1);   } }  /**  * Serial task.  * Prints the received items from the queue to the serial monitor.  */ void TaskSerial(void * pvParameters) {   (void) pvParameters;    // Init Arduino serial   Serial.begin(9600);    // Wait for serial port to connect. Needed for native USB, on LEONARDO, MICRO, YUN, and other 32u4 based boards.   while (!Serial) {     vTaskDelay(1);   }    int valueFromQueue = 0;    for (;;)   {      /**      * Read an item from a queue.      * https://www.freertos.org/a00118.html      */     if (xQueueReceive(integerQueue, &valueFromQueue, portMAX_DELAY) == pdPASS) {       Serial.println(valueFromQueue);     }   } }  /*  * Blink task.  * See Blink_AnalogRead example.  */ void TaskBlink(void *pvParameters) {   (void) pvParameters;    pinMode(LED_BUILTIN, OUTPUT);    for (;;)   {     digitalWrite(LED_BUILTIN, HIGH);     vTaskDelay( 250 / portTICK_PERIOD_MS );     digitalWrite(LED_BUILTIN, LOW);     vTaskDelay( 250 / portTICK_PERIOD_MS );   } }

В программировании есть такая весьма полезная вещь, как система флагов, в FreeRTOS подобной сущностью выступают семафоры. Полный список их разновидностей, можно найти вот здесь.

Также для доступа множественных задач к единому ресурсу, существуют мьютексы. Для тех, кто не знает, это такая сущность, которая гарантирует, что уникальный ресурс в один момент времени будет использован только одним потребителем, который заблокирует доступ к нему. В качестве примера такого мьютекса можно привести пример зарплатной карты, которая находятся только у вас (и пока она находится у вас, жена не будет иметь доступ к уникальному ресурсу, которым является ваш банковский счёт. Но как только вы оставите свою карту на тумбочке…) Впрочем, не будем о грустном.

Один из примеров работы с концепцией мьютекса показан в коде ниже:

Использование мьютекса

/*    Example of a FreeRTOS mutex    https://www.freertos.org/Real-time-embedded-RTOS-mutexes.html */  // Include Arduino FreeRTOS library #include <Arduino_FreeRTOS.h>   // Include mutex support #include <semphr.h>  /*    Declaring a global variable of type SemaphoreHandle_t  */ SemaphoreHandle_t mutex;  int globalCount = 0;  void setup() {    Serial.begin(9600);    /**        Create a mutex.        https://www.freertos.org/CreateMutex.html   */   mutex = xSemaphoreCreateMutex();   if (mutex != NULL) {     Serial.println("Mutex created");   }    /**      Create tasks   */   xTaskCreate(TaskMutex, // Task function               "Task1", // Task name for humans               128,               1000, // Task parameter               1, // Task priority               NULL);    xTaskCreate(TaskMutex, "Task2", 128, 1000, 1, NULL);  }  void loop() {}  void TaskMutex(void *pvParameters) {   int delay = *((int*)pvParameters); // Use task parameters to define delay    for (;;)   {     /**        Take mutex        https://www.freertos.org/a00122.html     */     if (xSemaphoreTake(mutex, 10) == pdTRUE)     {       Serial.print(pcTaskGetName(NULL)); // Get task name       Serial.print(", Count read value: ");       Serial.print(globalCount);        globalCount++;        Serial.print(", Updated value: ");       Serial.print(globalCount);        Serial.println();       /**          Give mutex          https://www.freertos.org/a00123.html       */       xSemaphoreGive(mutex);     }      vTaskDelay(delay / portTICK_PERIOD_MS);   } }

Кстати сказать, эта операционная система доступна и через стандартную Arduino IDE. Для этого нужно зайти в менеджер библиотек и установить себе, например, следующую библиотеку:

Библиотека использует в качестве генератора «тиков» Watchdog Timer, что даёт разрешение «тиков» в 15 миллисекунд между ними.

Или же вы можете скачать её с официального github-а системы. Или просто с сайта freertos.org. Вместе с этой библиотекой вы получите ряд примеров, на которых можете научиться работать с системой, что очень удобно.

Пример использования FreeRTOS для незамысловатой задачи мигания массивом светодиодов можно посмотреть в видео ниже. Код, показанный в видео, находится по ссылке.

А вот здесь чуть более сложный пример использования FreeRTOS:

И листинги программы с объяснениями.

Многие пользователи операционной системы FreeRTOS подчёркивают, что не нужно её выбирать, если вы хотите только «использовать, чтобы просто использовать». Однако если вам нужна надёжная, проработанная, многозадачная операционная система реального времени, которая обладает весьма скромными потребностями в плане ПЗУ и ОЗУ — то она для вас. Например, размер ядра системы в двоичном коде составляет всего лишь от 6 до 12 КБ.

Кроме того, интересным моментом является то, что система бесплатна, даже когда она используется для коммерческих применений. Здесь также существует отдельный вариант Safe RTOS, который имеет соответствующие сертификаты для применения его даже в рамках медицинских, автомобильных и промышленных секторов.

Посмотреть список устройств, на которые портирована система, можно вот по этой ссылке.

Тема эта в целом достаточно обширна, и мы прошлись только «по самому входу кроличьей норы», а она достаточно глубока, чтобы получить общее представление, так что если кто-то действительно заинтересуется этой темой — следует более подробно поизучать систему на головном сайте freertos.org либо просто в интернете.


НЛО прилетело и оставило здесь промокод для читателей нашего блога:

15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.


ссылка на оригинал статьи https://habr.com/ru/company/first/blog/668498/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *