FreeRTOS - операционная система для микроконтроллеров

FreeRTOS — многозадачная, мультиплатформенная операционная система жесткого реального времени (RTOS) для встраиваемых систем. Написана на языке Си с ассемблерными вставками для конкретной аппаратной платформы. Планировщик FreeRTOS поддерживает три типа многозадачности: вытесняющую с приоритетами, кооперативную и гибридную. Какая из них лучше ? В большинстве случаев вытесняющая многозадачность является более предпочтительной т.к. в отличие от кооперативной многозадачности управление операционной системе передаётся вне зависимости от состояния работающих задач, благодаря чему гарантируется своевременная реакция системы на какое-либо более приоритетное событие. Такие системы принято называть системами жесткого реального времени. Соответственно кооперативная многозадачность по своей природе является системой мягкого реального времени т.к. планировщик самостоятельно не может прервать выполнение текущей задачи даже если появилась готовая к выполнению задача с более высоким приоритетом — тут каждая задача должна самостоятельно передать управление планировщику. Гибридная и кооперативная многозадачность во FreeRTOS является опциональными и в основном служат для более рационального использования ресурсов микроконтроллера, которых всегда очень мало :)

MSP430.js | исходники

screenshot

При создании приложения на FreeRTOS рекомендуется брать за основу демонстрационный проект из официальных примеров. В нашем случае микроконтроллер MSP430F1611, организация файлов проекта makefile и компилятор (msp430-gcc 6.2.1.16) самый свежий на текущий момент от производителя. Все настройки конфигурации, касающиеся FreeRTOS, принято помещать в один отдельный заголовочный файл FreeRTOSConfig.h. Большинство настроек в этом файле являются опциональными т.к. уже имеют значения по умолчанию, однако некоторые нужно обязательно определять в каждом проекте ибо сломается компиляция:

FreeRTOSConfig.h

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

#include <msp430.h>

/*-----------------------------------------------------------
 * Application specific definitions.
 *
 * These definitions should be adjusted for your particular hardware and
 * application requirements.
 *
 * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
 * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE. 
 *
 * See http://www.freertos.org/a00110.html.
 *----------------------------------------------------------*/

#define configUSE_PREEMPTION        1
#define configUSE_IDLE_HOOK         0
#define configUSE_TICK_HOOK         0
#define configCPU_CLOCK_HZ          ( ( unsigned long ) 130000 )
#define configTICK_RATE_HZ          ( ( TickType_t ) 10 )
#define configMAX_PRIORITIES        2
#define configMINIMAL_STACK_SIZE    ( ( unsigned short ) 50 )
#define configTOTAL_HEAP_SIZE       ( ( size_t ) ( 8 * 1024 ) )
#define configUSE_16_BIT_TICKS      1

#endif /* FREERTOS_CONFIG_H */

Итак пробежимся по порядку по всем указанным настройкам:

  • configUSE_PREEMPTION задаёт режим многозадачности — кооперативная (0) или вытесняющая (1)

  • configUSE_IDLE_HOOK, configUSE_TICK_HOOK — хуки сейчас не используем (0), но будем в следующем материале

  • configCPU_CLOCK_HZ — в подавляющем большинстве задач необходимо отмерять интервалы времени, данная величина — тактовая частоты микроконтроллера (похоже порт MSP430 её нигде не использует, сейчас там при расчёте временных квантов используется portACLK_FREQUENCY_HZ из расчёта LFXT1CLK = 32768 Hz)

  • configTICK_RATE_HZ — переключение между задачами осуществляется через равные кванты времени работы планировщика и время реакции FreeRTOS на внешние события в режиме вытесняющей многозадачности не превышает одного кванта. По идее чем меньше квант тем лучше, однако за увеличением частоты переключений следует то, что ядро системы использует больше процессорного времени и тогда соответственно меньше процессорного время остаётся под задачи. Чем выше тактовая частота, тем большую частоту переключения можно задавать

  • configMAX_PRIORITIES — каждой задаче назначается приоритет от 0 до (configMAX_PRIORITIES - 1). Наиболее низкий приоритет у задачи «бездействие», значение которого по умолчанию определено как 0. Уменьшение configMAX_PRIORITIES позволяет уменьшить объем ОЗУ, потребляемый ядром

  • configMINIMAL_STACK_SIZE, configTOTAL_HEAP_SIZE — пока что пусть это будут некие магические числа, определяющие достаточный объём памяти для нормального функционирования задач. Что будет если их поменять или задать неправильно посмотрим в следующем материале

  • configUSE_16_BIT_TICKS — разрядность счётчика квантов времени, прошедших с начала работы системы 16 бит (1) или 32 бит (0)

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

Теперь собственно пора определить и сами задачи. У нас две группы светодиодов — левые и правые и соответственно две независимые задачи с анимациями для каждой группы.

task_r_leds.c

#include <FreeRTOS.h>
#include <task.h>

#include "task_r_leds.h"

void vTaskLedsR( void *pvParameters ) {
   for( ;; ) {
     P3OUT = (P3OUT ^ 0xFF) & ~0b11 /* don't toggle LCD pins 0b11 */;
     for( volatile uint16_t i = 10000 ; i--; ); // delay
   }

  /* Should the task implementation ever break out of the above loop, then the task
    must be deleted before reaching the end of its implementing function. The NULL
    parameter passed to the vTaskDelete() API function indicates that the task to be
    deleted is the calling (this) task. */

   vTaskDelete( NULL );
}

task_l_leds.c

#include <FreeRTOS.h>
#include <task.h>

#include "task_l_leds.h"

static void rotate_rgb(volatile unsigned char * const port) {
    *port = 0b10000;
    do {
        for( volatile uint16_t i = 5000 ; i--; ); // delay
        *port >>= 1;
    } while (*port);
}

void vTaskLedsL( void *pvParameters ) {
   for( ;; ) {
     for( volatile uint16_t i = 42000 ; i--; );   // delay
     rotate_rgb(&P4OUT);
     rotate_rgb(&P5OUT);
     rotate_rgb(&P6OUT);
   }

   vTaskDelete( NULL );
}

Имена идентификаторов в исходном коде ядра FreeRTOS и демонстрационных проектах подчиняются определенным соглашениям об именовании, зная которые проще понимать тексты программ. Имена переменных и функций представлены в префиксной форме (так называемая Венгерская нотация), например ulMemCheck — переменная типа unsigned long, pxCreatedTask — переменная типа «указатель на структуру», в нашем случае vTaskDelete — функция, которая ничего не возвращает void. Функции-задачи никогда не прерываются, поэтому обычно реализуются при помощи непрерывного цикла, а если все-таки произойдет выход из бесконечного цикла, то задача должна быть уничтожена до конца функции — по крайней мере так показано в официальных примерах к FreeRTOS. Параметр NULL обозначает, что уничтожается сама задача, из которой непосредственно происходит вызов API-функции vTaskDelete() (харакири).

Фрагмент кода с disable_watchdog не имеет отношения к FreeRTOS — это обход граблей компилятора. Без этого костыля при инициализация больших массивов при старте срабатывает сторожевой таймер ещё до точки входа int main( void ) и сбрасывает микроконтроллер :(

main.c

#include <FreeRTOS.h>
#include <task.h>

#include "task_r_leds.h"
#include "task_l_leds.h"

// !!!!   msp430-gcc 6.2.1.16   !!!!
// msp430_gcc/examples/watchdog.txt

static void __attribute__((naked, section(".crt_0042"), used))
disable_watchdog (void)
{
  WDTCTL = WDTPW | WDTHOLD; // Stop watchdog timer
}

/* Demo task priorities. */

enum { 
  main_TASK_PRIORITY_LEDS_L = tskIDLE_PRIORITY + 1,
  main_TASK_PRIORITY_LEDS_R = main_TASK_PRIORITY_LEDS_L,
};

/*
 * Start the demo application tasks - then start the real time scheduler.
 */
int main( void ) {

  /* Setup the hardware ready for the demo. */

  // DCO = 3, RSEL = 0, f = 0.13 MHz
  DCOCTL = /* DCO2 + */ DCO1 + DCO0; 
  BCSCTL1 = XT2OFF /* + RSEL1 + RSEL0 + RSEL2 */;

  // Shared between L/R tasks 8*RGB LEDS 
  P4OUT = 0; P5OUT = 0; P6OUT = 0;
  P4DIR = P5DIR = P6DIR = 0xFF;

  // 2 RGB LEDS (pins 2..7)
  P3OUT = 0; P3DIR = 0xFF;

  // create tasks
  // Passing a uxPriority value above (configMAX_PRIORITIES – 1) 
  // will result in the priority assigned to the task
  // being capped silently to the maximum legitimate value.

  /* Task: left side LEDS animation */
  xTaskCreate(vTaskLedsL, 
          "LedsL", 
          configMINIMAL_STACK_SIZE, 
          NULL, 
          main_TASK_PRIORITY_LEDS_L, 
          NULL );

  /* Task: right side LEDS animation */
  xTaskCreate(vTaskLedsR, 
          "LedsR", 
          configMINIMAL_STACK_SIZE, 
          NULL, 
          main_TASK_PRIORITY_LEDS_R, 
          NULL );

  /* Start the scheduler. */
  vTaskStartScheduler();

  /* As the scheduler has been started the demo applications tasks will be
  executing and we should never get here! */
  return 0;
}

Все задачи могут находиться в одном из следующих состояний:

  • Running (запущена) — задача выполняется, процессор занят ее выполнением

  • Ready (готова) — готова выполнению, но в данный момент времени процессор занят выполнением другой задачи. По окончании текущего кванта времени из всех готовых к выполнению задач будет запущена (перейдёт в состояние выполнения) задача с наибольшим приоритетом. Если к выполнению готовы несколько задач с одинаковым приоритетом, то бишь как в нашем случае с vTaskLedsL и vTaskLedsR, то они по очереди переходят в состояние выполнения и пребывают в нем в течение одного системного кванта

  • Blocked (заблокирована) — задача ожидает временного или внешнего события. Например, вызвав API-функцию vTaskDelay(42), задача переведет себя в блокированное состояние до тех пор, пока не пройдет временной период задержки 42. Блокированная задача не расходует процессорного времени, это время можно с пользой для дела использовать в менее приоритетных задачах

  • Suspended (приостановлена) — такие задачи также не получает процессорного времени, однако в отличие от блокированного состояния, переход в приостановленное состояние и выход из него осуществляется в явном виде вызовом API-функций vTaskSuspend() и xTaskResume(). Задача может оставаться приостановленной сколь угодно долго

В приведенном примере задачи vTaskLedsL и vTaskLedsR выполняют полезное действие (в нашем случае — мигают), после чего ожидают определенный промежуток времени. Реализация задержки в виде пустого цикла for(volatile uint16_t i = 42; i--;); крайне не эффективна — она как бы слишком «жадная». Что будет, если таким задачам назначить разный приоритет ? Высокоприоритетная задача все время остается в состоянии готовности к выполнению (не переходит ни в блокированное, ни в приостановленное состояние), она поглощает все процессорное время, вследствие чего низкоприоритетные задачи никогда не выполняются. Для корректной реализации задержек средствами FreeRTOS предусмотрена API-функция vTaskDelay(), которая переводит задачу, вызывающую эту функцию, в блокированное состояние на требуемое количество квантов времени. Для использования этой функции необходимо в файле конфигурации FreeRTOSConfig.h добавить макроопределение #define INCLUDE_vTaskDelay 1, а если забыть это сделать, то проект попросту не соберётся:

task_r_leds.c

#include <FreeRTOS.h>
#include <task.h>

#include "task_r_leds.h"

void vTaskLedsR( void *pvParameters ) {
   for( ;; ) {
     P3OUT = (P3OUT ^ 0xFF) & ~0b11 /* don't toggle LCD pins 0b11 */;
     vTaskDelay(15 /* ticks */);
   }

   vTaskDelete( NULL );
}

task_l_leds.c

#include <FreeRTOS.h>
#include <task.h>

#include "task_l_leds.h"

static void rotate_rgb(volatile unsigned char * const port) {
    *port = 0b10000;
    do {
        vTaskDelay(5 /* ticks */);
        *port >>= 1;
    } while (*port);
}

void vTaskLedsL( void *pvParameters ) {
   for( ;; ) {
     vTaskDelay(42 /* ticks */);
     rotate_rgb(&P4OUT);
     rotate_rgb(&P5OUT);
     rotate_rgb(&P6OUT);
   }

   vTaskDelete( NULL );
}

Ок, а почему в файле конфигурации FreeRTOSConfig.h отсутствует макроопределение #define INCLUDE_vTaskDelete 1, а проект проходит компиляцию без ошибок ? Тут всё дело в оптимизации компилятора — перед вызовом vTaskDelete компилятор видит бесконечный цикл и просто игнорирует весь код после него, что в принципе правильно. Но стоит например по ошибке выйти из цикла break и будет ошибка при сборке.

P.S. В официальной документации имена подключаемых файлов FreeRTOS берутся в кавычки например #include "FreeRTOS.h", в текущих примерах есть небольшое отличие #include <FreeRTOS.h>. Это легко при желании поменять:

~$ ls -1 *.h *.c | xargs -n1 sed -i                          \
      -e 's/#include\s\+<FreeRTOS.h>/#include "FreeRTOS.h"/' \
      -e 's/#include\s\+<task.h>/#include "task.h"/'         \
      -e 's/#include\s\+<semphr.h>/#include "semphr.h"/'     \
      -e 's/#include\s\+<queue.h>/#include "queue.h"/'

На результат компиляции данных примеров это никак не влияет — бинарник на выходе a.out будет одинаков в обоих случаях.

Далее хуки.

links

social