Freertos мануал на русском

[1.1. Введение: о чем говорится в части 1 (дополнения также предоставляют практическую информацию по специфике использования исходного кода FreeRTOS)]

Введение в многозадачность, применяемую в малых встраиваемых (embedded) системах

Различные системы многозадачности имеют разные сферы применения. На примере рабочих станций и десктопов:

· В ‘старые времена’ процессоры были дороги, и многозадачность использовалась, чтобы обеспечить доступ некоторому количеству пользователей к одному процессору. В системах этого типа использовались алгоритмы управления задачами (шедулинг), разработанные с целью ‘честного совместного использования’ процессорного времени всеми пользователями системы. Каждый пользователь запускал как правило только одну задачу.

· В последнее время мощность процессоров подешевела, и ситуация коренным образом изменилась. Теперь каждый пользователь может получить эксклюзивный доступ к одному или большему количеству процессоров. Алгоритмы шедулинга в этих типах систем разработаны так, чтобы позволить пользователю запустить несколько приложений (программ) одновременно, без выхода компьютера в перегруженное, нерабочее состояние (когда он перестает отвечать на запросы). Например, пользователь мог запустить текстовый редактор (Word), редактор таблиц (Excel), почтовый клиент, WEB-браузер, и при этом в любое время ожидается адекватный ответ на все действия пользователя в этих программах.

Примечание: это перевод руководства [1].

Обработка задач (работа программ) на компьютере-десктопе может быть классифицирована как ‘мягкий реалтайм’ (soft real time). Чтобы обеспечить наилучшее использование компьютера для пользователя, система должна отвечать на каждый ввод в течение минимального желаемого лимита времени, однако если незначительно выйти за пределы этого лимита, то компьютерная система останется для пользователя работоспособной. Например, нажатия на клавиши должны визуально регистрироваться в течение определенного времени после нажатия. Регистрирование нажатий вне этого времени выглядит как потеря отзывчивости системой, но её работоспособность в целом сохраняется.

Многозадачность встраиваемых систем реального времени (контроллер дисплея, стиральной машины, промышленного робота, бортовой компьютер автомобиля, космического шаттла и так далее) концептуально устроена очень похоже на многозадачность десктопов, если рассматривать их с точки зрения запуска нескольких потоков выполнения (задач) на одном процессоре. Однако цель встраиваемых систем реального времени полностью отличается от десктопов — от встраиваемых систем ожидается обеспечение ‘жесткого реалтайма’ (hard real time).

Функции жесткого реалтайма ДОЛЖНЫ (и никак иначе) быть завершены в указанном лимите времени — невыполнение этого условия приводит к полному отказу в работе всей системы. Механизм срабатывания подушки безопасности в автомобиле является примером функции жесткого реалтайма. Подушка должна раскрыться при ударе в отведенный лимит времени. Ответ системы вне этого лимита времени может привести к получению травмы водителем, которой иначе можно было бы избежать.

Многие встраиваемые системы реализуют выполнение смеси требований жесткого и мягкого реалтайма одновременно.

Терминология

В системе FreeRTOS каждый поток выполнения называется ‘задачей’ (task). Нет абсолютного согласия в терминологии по компонентам внутри встраиваемых систем, но наверное предпочтительнее использовать вместо термина ‘поток’ (thread) термин ‘задача’ (task), поскольку thread (поток) может иметь более специфичное значение в зависимости от внешних ранее приобретенных знаний.

Общий обзор части 1

Часть 1 дает читателю хорошее понимание следующего:

· Как FreeRTOS выделяет процессорное время для каждой задачи внутри приложения
· Как FreeRTOS выбирает, какая задача должна выполняться в любой имеющийся интервал времени
· Как относительные приоритеты каждой задачи влияют на поведение системы
· В каких состояниях может находиться задача

Дополнительно читатели получат хорошее понимание:

· Как реализовывать задачи
· Как создать один или большее количество экземпляров задачи
· Как использовать параметр задачи
· Как изменить приоритет созданной ранее задачи
· Как уничтожить задачу
· Как реализовать периодическое выполнение задачи
· Когда запускается задача ожидания (idle task), и как это можно использовать

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

[1.2. Функции задачи]

Задачи реализованы как функции на языке C. Есть только одна особенность в прототипе такой функции — она должна возвращать void и принимать в качестве параметра указатель на void. Прототип показан в листинге 1.

void ATaskFunction( void *pvParameters );

Листинг 1. Прототип для функции задачи.

Каждая задача — это маленькая программа со своими собственными правами. Она имеет точку входа, нормально выполняет свой бесконечный цикл, из которого никогда не делает выход. Структура типичной задачи показана в листинге 2.

Задачи FreeRTOS не должны никоим образом делать возврат (выход) из своей функции — она не должна содержать оператор ‘return’, и выполнению не должно быть позволено доходить до конца функции. Если в функции больше нет надобности, вместо выхода из неё нужно явно удалить запущенную задачу. Это также демонстрируется в листинге 2.

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

void ATaskFunction( void *pvParameters )
{
   /* Переменные могут быть определены точно так же, как и в обычной функции.
      Каждый экземпляр созданной по этой функции задачи будет иметь собственную копию
      переменной iVariableExample. Это не верно, если переменная была продекларирована
      как статическая (static) – в этом случае будет существовать только одна копия
      переменной, и она будет использоваться совместно всеми созданными экземплярами
      задачи. */

   int iVariableExample = 0;

   /* Задача должна быть нормально реализована как бесконечный цикл. */
    for( ;; )
    {
       /* Код, который реализует функционал задачи, должен быть помещен здесь. */
    }

    /* Код должен быть организован так, чтобы в случае выхода (break) из указанного
       

выше бесконечного цикла задачи, задача должна быть удалена ПРЕЖДЕ чем
       
управление достигнет конца этой функции. Параметр NULL, переданный
       vTaskDelete(), 
показывает, что должна быть удалена вызванная (эта, которая
       работает) задача. */

    vTaskDelete( NULL );
}

Листинг 2. Структура типичной функции задачи.

[1.3. Состояния задачи на верхнем уровне]

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

Если задача находится в состоянии Running, то процессор в настоящий момент выполняет её код. Когда задача находится в состоянии Not Running, то задача находится в бездействии, её текущий статус сохранен в готовности продолжить выполнение, как только шедулер примет решение перевести задачу в состояние Running. Когда задача продолжает выполнение, она начнет работу именно с той инструкции, где выполнение было прервано при выходе задачи из последнего состояния Running.

FreeRTOS-pict01-running.png

Рис. 1. Состояния и переходы задачи на верхнем уровне.

О задаче, переходящей из состояния Not Running в состояние Running говорят, что она «подключилась» («switched in» или «swapped in»). И аналогично, о задаче, переходящей из состояния Running в состояние Not Running говорят, что она «отключилась» («switched out» или «swapped out»). Шедулер FreeRTOS — всего лишь некая сущность, которая может включить (in) и выключить (out) задачу.

[1.4. Создание задач]

API функция xTaskCreate()

Задачи создаются с использованием API функции xTaskCreate(). Очень важен тот факт, что задачи должны быть предварительно специальным образом подготовлены, чтобы стать фундаментальным компонентом многозадачной системы. Все примеры, приведенные в этой книге, используют для создания задач функцию xTaskCreate().

В Дополнении 5 описываются типы данных и используемые условные соглашения по именованию типов данных и функций.

portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,
                           const signed portCHAR * const pcName,
                           unsigned portSHORT usStackDepth,
                           void *pvParameters,
                           unsigned portBASE_TYPE uxPriority,
                           xTaskHandle *pxCreatedTask );

Листинг 3. Прототип API функции xTaskCreate().

Таблица 1. Параметры и значение возврата функции xTaskCreate().

Имя параметра
/
возвращаемое значение
Описание
 pvTaskCode Задачи — это простые C-функции, которые никогда не делают возврата из своего тела (постоянно выполняют свой бесконечный цикл). Параметр pvTaskCode — простой указатель на функцию (т. е. просто имя функции), которая реализует задачу.
 pcName Описательное имя для задачи. Оно никак не используется внутри FreeRTOS, и нужно только для целей отладки. Идентификация задачи по легкочитаемому имени намного проще, чем по хендлу задачи (handle).

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

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

Значение usStackDepth указывает количество слов, которое можно сохранить в стеке, а не количество байт. Например, если стек имеет ширину 32 бита, и переданное значение usStackDepth равно 100, то под стек будет выделено 400 байт (100 * 4 байт). Глубина стека, умноженная на его ширину, не должна превышать максимальное значение, которое может содержать переменная типа size_t.

Размер стека, используемого для задачи ожидания (idle task, об этой задаче подробнее говорится далее), задается константой configMINIMAL_STACK_SIZE. Значение, назначенное этой константе в демо-приложении FreeRTOS (для определенной архитектуры микроконтроллера) может быть использовано как минимально рекомендованное для любой задачи. Если Ваша программа использует пространство в стеке, то нужно указать для константы configMINIMAL_STACK_SIZE увеличенное значение.

Нет простого способа узнать, какого размера стек нужен для задачи. Этот размер можно вычислить, но в большинстве случаев можно просто назначить подходящее значение, подобранное опытным путем, либо взятое приблизительно. Подобрать правильный размер стека важно, чтобы обеспечить адекватное использование RAM без ненужных затрат. Часть 6 содержит информацию о том, как запросить размер стека, используемого задачей.

 pvParameters Функции задач принимают параметр, имеющий тип указателя на void (т. е. void*). Значение, указанное в pvParameters, будет передано в задачу. Несколько примеров в этом документе демонстрируют, как этот параметр может быть использован в реальных задачах.
 uxPriority Задает приоритет, с которым будет выполняться задача. Приоритеты могут быть назначены в любое значение от 0 минимальный приоритет до (configMAX_PRIORITIES – 1) максимальный приоритет.

Константа configMAX_PRIORITIES определяется пользователем. Нет ограничения на верхний предел для количества приоритетов, которые можно задать (кроме ограничения на лимит используемого типа данных ограничения по количеству RAM, доступному в микроконтроллере), но желательно использовать как можно меньшее количество приоритетов, которое действительно необходимо — с целью уменьшения расхода RAM.

Передача в параметре значения uxPriority выше (configMAX_PRIORITIES – 1) приведет к молчаливому назначению приоритета задачи в максимально допустимое значение configMAX_PRIORITIES.

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

Если Ваше приложение не использует хендл задачи, то pxCreatedTask может быть установлен в NULL.

Возвращаемое значение Имеется два возможных возвращаемых значения:

1. pdTRUE показывает, что задача успешно создана.
2. errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY показывает, что задача не создана, так как в куче (heap) недостаточно свободной памяти для FreeRTOS, чтобы она могла выделить место для структур данных задачи и стека. Часть 5 предоставляет больше информации по управлению памятью.

Пример 1. Создание задачи

Дополнение 1 содержит информацию об инструментарии, необходимом для сборки проектов примеров. 

Этот пример демонстрирует необходимые шаги для создания двух простых задач и для последующего запуска этих задач на выполнение. Задачи просто периодически выводят на печать строку, используя грубый пустой цикл для создания задержки. Обе задачи создаются с одинаковым приоритетом и абсолютно идентичны, отличаясь только строками, которые они выводят — см. Листинг 4 и Листинг 5 для их соответствующих реализаций.

void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul;

  /* Как и большинство задач, эта задача реализована на основе бесконечного цикла. */
  for( ;; )
  {
     /* Вывод на печать имени этой задачи. */
     vPrintString( pcTaskName );

     /* Задержка на некоторый период времени. */
     for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
     {
         /* Этот цикл просто реализует задержку очень грубым методом.
            В цикле не производится никаких действий. Далее будут приведены
            примеры, в которых этот пустой цикл будет заменен соответствующей
            функцией задержки / приостановки задачи (введение задачи в состояние
            сна - sleep). */
     }
  }
}

Листинг 4. Реализация первой задачи, используемой в примере 1.

void vTask2( void *pvParameters )
{
const char *pcTaskName = "Task 2 is running\r\n";
volatile unsigned long ul;

  /* Как и большинство задач, эта задача реализована на основе бесконечного цикла. */
  for( ;; )
  {
     /* Вывод на печать имени этой задачи. */
     vPrintString( pcTaskName );
     /* Задержка на некоторый период времени. */
     for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
     {
         /* Этот цикл просто реализует задержку очень грубым методом.
            В цикле не производится никаких действий. Далее будут приведены
            примеры, в которых этот пустой цикл будет заменен соответствующей
            функцией задержки / приостановки задачи (введение задачи в состояние
            сна - sleep). */
     }
  }
}

Листинг 5. Реализация второй задачи, используемой в примере 1.

Функция main() просто создает задачи перед запуском шедулера — см. Листинг 6.

int main( void )
{
  /* Создание одной из двух задач. Имейте в виду, что реальное приложение должно
     проверить возвращаемое значение из вызова xTaskCreate(), чтобы удостовериться,
     что задача была успешно создана. */
  xTaskCreate( vTask1,  /* Указатель на функцию, которая реализует задачу. */
               "Task 1",/* Текстовое имя задачи. Этот параметр нужен только для
                           упрощения отладки. */
               1000,    /* Глубина стека - самые маленькие микроконтроллеры будут
                           использовать значение намного меньше, чем здесь
                           указано. */

               NULL,    /* Мы не используем параметр задачи. */
               1,       /* Задача будет запущена с приоритетом 1. */
               NULL );  /* Мы не будем использовать хендл задачи. */
  /* Создание другой задачи полностью совпадает с созданием первой,
     приоритет задачи тот же. */
  xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );

  /* Запуск шедулера, после чего задачи запустятся на выполнение. */
  vTaskStartScheduler();

  /* Если все хорошо, то управление в main() никогда не дойдет до этой точки,
     и теперь шедулер будет управлять задачами. Если main() довела 

управление
     до
этого места, то это может означать, что не хватает памяти кучи
    
(heap) для создания специальной задачи ожидания (idle task, об этой задаче
    
далее). Часть 5 предоставляет больше информации по управлению памятью. */
  for( ;; );
}

Листинг 6. Запуск задач примера 1.

Выполнение примера 1 производит вывод, показанный на рисунке 2.

FreeRTOS-pict02-example1-output.PNG

Рис. 2. Вывод, который производит при выполнении пример 1.

На рисунке 2 видно, что две задачи работают вместе, но поскольку они обе выполняются на одном процессоре, то реально все происходит несколько иначе. В действительности обе задачи быстро входят в состояние Running (запущено) и быстро выходят из него. Обе задачи работают с одинаковым приоритетом, и делят между собой процессорное время. Диаграмма реального процесса выполнения показана на рисунке 3.

FreeRTOS-pict03-timing.png

Рис. 3. Реальный паттерн выполнения двух задач примера 1.

Пример 1 создает обе задачи в теле функции main() перед запуском шедулера. Также есть возможность создания задачи из кода другой задачи. В другом примере мы создаем задачу 1 из кода функции main(), и затем создаем задачу 2 из кода задачи 1. Для того, чтобы сделать это, нужно ввести изменения, как показано в листинге 7. Задача 2 не будет создана, пока не запустится шедулер, однако вывод, генерируемый измененным примером, должен быть таким же.

void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul;

  /* Если этот код выполняется, то шедулер уже запустился. Создаем
     здесь другую задачу перед входом в бесконечный цикл. */
     xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );

  for( ;; )
  {
     /* Вывод на печать имени этой задачи. */
     vPrintString( pcTaskName );

     /* Задержка на некоторый период времени. */
     for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
     {
         /* Этот цикл просто реализует задержку очень грубым методом.
            В цикле не производится никаких действий. Далее будут приведены
            примеры, в которых этот пустой цикл будет заменен соответствующей
            функцией задержки / приостановки задачи (введение задачи в состояние
            сна - sleep). */
     }
  }
}

Листинг 7. Создание задачи из кода другой задачи — после того, как шедулер уже стартовал.

Пример 2. Использование параметра задачи

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

Листинг 8 содержит код единой функции для всех задач (vTaskFunction), используемых в примере 2. Одиночная функция заменяет две функции задачи (vTask1 и vTask2), используемые в примере 1. Посмотрим, как параметр задачи преобразуется в тип char* для получения строки, которую нужно вывести на печать.

void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
volatile unsigned long ul;

  /* Строка для вывода на печать передается через параметр. Здесь
     он преобразуется в указатель на символьную строку. */
  pcTaskName = ( char * ) pvParameters;

  /* Как и большинство задач, эта задача реализована на основе бесконечного цикла. */
  for( ;; )
  {
     /* Вывод на печать имени этой задачи. */
     vPrintString( pcTaskName );

     /* Задержка на некоторый период времени. */
     for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
     {
         /* Этот цикл просто реализует задержку очень грубым методом.
            В цикле не производится никаких действий. Далее будут приведены
            примеры, в которых этот пустой цикл будет заменен соответствующей
            функцией задержки / приостановки задачи (введение задачи в состояние
            сна - sleep). */
     }
  }
}

Листинг 8. Одна функция задачи, используемая для создания двух задач примера 2.

Несмотря на то, что теперь реализация задач только одна (vTaskFunction), можно задать бОльшее, чем один, количество экземпляров задачи, Каждый созданный экземпляр задачи будет выполняться независимо от другого под управлением шедулера FreeRTOS.

Параметр pvParameters, передаваемый в функцию xTaskCreate(), используется для передачи указателя на строку текста, как показано в листинге 9.

/* Определение строк, которые будут переданы через параметры задачи. Они определены
  как константы (const) и не находятся в стеке, чтобы обеспечить их сохранность,
  когда задачи выполняются. */
static const char *pcTextForTask1 = “Task 1 is running\r\n”;
static const char *pcTextForTask2 = “Task 2 is running\t\n”;

int main( void )
{
  /* Создание одной из двух задач. */
  xTaskCreate( vTaskFunction, /* Указатель на функцию, которая реализует
                                 задачу. */
               "Task 1",      /* Текстовое имя задачи. Используется только
                                 для упрощения отладки. */
               1000,          /* Глубина стека - самые маленькие микроконтроллеры
                                 

будут использовать значение намного меньше,
                                 чем здесь указано. */
              (void*)pcTextForTask1, /* Передача печатаемого задачей текста как
                                       
параметр задачи. */
               1,              /* Задача будет запущена с приоритетом 1. */
               NULL );         /* Мы не будем использовать хендл задачи. */
  /* Создание другой задачи происходит точно так же. Обратите внимание, что несколько
     задач создается из ОДНОЙ И ТОЙ ЖЕ реализации задачи (vTaskFunction). Различие
     только в величине параметра, переданного для вывода строки. Создается два
     экземпляра одной и той же задачи. */
  xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );

  /* Запуск шедулера, чтобы наши задачи смогли выполняться. */
  vTaskStartScheduler();

  /* Если все хорошо, то управление в main() никогда не дойдет до этой точки,
     и теперь шедулер будет управлять задачами. Если main() довела управление до
     этого места, то это может означать, что не хватает памяти кучи (heap)
     для создания специальной задачи ожидания (idle task, об этой задаче далее).
     Часть 5 предоставляет больше информации по управлению памятью. */
  for( ;; );
}

Листинг 9. Функция main() для примера 2.

Вывод, производимый примером 2, абсолютно такой же, как в примере 1 (показано на рисунке 2).

[1.5. Приоритеты задачи]

Параметр uxPriority API функции xTaskCreate() назначает начальный приоритет для создаваемой задачи. Приоритет может быть изменен после запуска шедулера при помощи API функции vTaskPrioritySet().

Максимально возможное количество доступных приоритетов задается в приложении константой времени компиляции configMAX_PRIORITIES в файле FreeRTOSConfig.h. Система FreeRTOS сама по себе не ограничивает максимально возможное значение для этой константы, однако нужно помнить, что чем больше значение configMAX_PRIORITIES, тем больше потребляется ядром памяти RAM, поэтому рекомендуется всегда устанавливать эту константу на минимально возможное значение.

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

Значения приоритетов, имеющих малую числовую величину, предназначены для низкоприоритетных задач, значение 0 соответствует самому низкому возможному приоритету. Таким образом, диапазон возможных приоритетов лежит от 0 до (configMAX_PRIORITIES – 1).

Шедулер всегда предоставляет возможность запуститься для задачи с наивысшим приоритетом и войти для неё в состояние Running. Когда несколько задач имеют одинаковый приоритет, и они могут быть запущены, то шедулер будет переводить каждую из этих задач в состояние Running и обратно по очереди в цикле. Это поведение было показано в недавних примерах, где обе тестовые задачи были созданы с одинаковым приоритетом, и обе всегда могли быть запущены. Каждая из этих задач выполнялась в фиксированном интервале времени, так называемом «слайсе времени» («time slice«, далее этот интервал будем называть просто «слайс»), когда задача входит в режим Running на начале слайса и выходит из режима Running в конце этого слайса и в начале следующего. На рисунке 3 интервал времени между t1 и t2 равен одному слайсу времени.

Чтобы шедулер мог определить — какую задачу нужно в начале каждого слайса, шедулер сам запускается на выполнение в конце каждого слайса времени. Для этой цели используются периодическое прерывание, называемое тиком (tick interrupt). Продолжительность слайса времени устанавливается по частоте срабатывания прерываний (тиков), которая конфигурируется константой времени компиляции configTICK_RATE_HZ в файле FreeRTOSConfig.h. Например, если configTICK_RATE_HZ установлена в 100 (Гц), то длительность слайса составит 10 мс. Рисунок 3 может быть расширен, чтобы показать также работу и самого шедулера во всей последовательности выполнения задач. Это показано на рисунке 4.

Имейте в виду, что вызовы API функций FreeRTOS всегда указывают время в тиках прерываний (обычно их просто называют ‘тики’, ‘ticks’). Константа portTICK_RATE_MS предоставлена для того, чтобы можно было преобразовать интервал времени в тиках в интервал времени в миллисекундах. Доступная разрешающая способность зависит от частоты тиков.

Значение счетчика тиков ‘tick count’ равно количеству произошедших прерываний тиков с момента старта шедулера; предполагается, что в счетчике тиков не было переполнения. Приложения пользователя не должны отслеживать переполнения при указании периода задержки, так как целостность отсчета времени обеспечивается внутри ядра FreeRTOS.

FreeRTOS-pict04-timing.png

Рис. 4. Последовательность выполнения, расширенная, чтобы показать выполнение прерывания тиков.

На рисунке 4 красной линией показано, когда выполняется само ядро (обработчик прерывания тиков). Черные стрелки показывают последовательность выполнения от задачи до прерывания, и затем обратно от прерывания к другой задаче.

Пример 3. Экспериментирование с приоритетами

Как уже говорилось, шедулер всегда обеспечивает для задачи с наивысшим приоритетом возможность запуска при выборе задачи для входа в состояние Running. В наших недавних примерах были созданы две задачи с одинаковым приоритетом, так они входили по циклу в состояние Running и выходили из него по очереди. Этот пример рассматривает что произойдет, когда мы изменим приоритет одной из двух задач, созданных в примере 2. Это произойдет, когда первая задача будет создана с приоритетом 1, а вторая задача с приоритетом 2. Код для создания задач показан в листинге 10. Одиночная функция, которая используется для реализации обоих задач, остается неизменной, она просто периодически выводит на печать строку, используя пустой цикл для организации задержки.

/* Определение строк, которые будут переданы через параметры задачи. Они определены
  как константы (const) и не находятся в стеке, чтобы обеспечить их сохранность,
  когда задачи выполняются. */
static const char *pcTextForTask1 = “Task 1 is running\r\n”;
static const char *pcTextForTask2 = “Task 2 is running\t\n”;

int main( void )
{
  /* Создание первой задачи с приоритетом 1. Приоритет - предпоследний
     параметр. */
  xTaskCreate( vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL );

  /* Создание второй задачи с приоритетом 2. */
  xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );

  /* Запуск шедулера, чтобы наши задачи смогли выполняться. */
  vTaskStartScheduler();
  return 0;
}

Листинг 10. Создание двух задач с разными приоритетами.

Вывод, производимый примером 3, показан на рисунке 5. Шедулер всегда будет выбирать для запуска задачу с наивысшим приоритетом. Задача 2 имеет приоритет выше, чем у задачи 1, и задача 2 всегда допустима для запуска; поэтому только задача 2 всегда входит в режим Running. Так как задача 1 никогда не входит в режим Running, то она не выводит строк на печать. О задаче 1 говорят как о ‘зависшей’, поскольку задача 2 не дает её свободного процессорного времени.

FreeRTOS-pict05-example3-output.PNG

Рис. 5. Запуск обоих тестовых задач с разными приоритетами.

Задача 2 всегда готова к запуску, потому что она никогда не ждет какого-то события — она либо крутится в пустом цикле, либо печатает вывод строки на терминал.

Рисунок 6 показывает последовательность выполнения примера 3.

FreeRTOS-pict06-timing.png

Рис. 6. Диаграмма выполнения, когда одна задача имеет более высокий приоритет, чем другая.

[1.6. Что означает состояние задачи ‘NOT RUNNING’ (не запущено)]

В прошлых примерах каждая создаваемая задача всегда нуждалась в каких-то действиях (пусть даже пустых в виде цикла задержки), и в них никогда не нужно было ожидать чего-либо. Поскольку такие задачи не ожидают никаких событий, то они всегда были готовы войти в состояние Running. Такой тип задачи с продолжающейся обработкой имеет ограниченное применение, потому что такую задачу можно создать только с самым низким приоритетом. Если же такая задача будет запущена не с самым низким приоритетом, то она не даст запуститься задачам с более низким приоритетом.

Чтобы сделать Ваши задачи в приложении действительно полезными, нам нужен метод, который позволит управлять задачей по событию. Задача, управляемая событием, запускается в работу (делает обработку) только после возникновения переключающего состояние задачи события, и такая задача не может войти в состояние Running, пока такое событие не произойдет. Как уже говорилось, шедулер всегда выбирает для запуска задачу с наивысшим приоритетом, которая МОЖЕТ запуститься. То, что высокоприоритетные задачи в настоящий момент НЕ МОГУТ запуститься означает, что шедулер их пока не может выбрать и должен вместо этого выбрать одну из задач с более низким приоритетом. Таким образом, использование управляемых событиями задач означает, что задачи могут быть созданы с некоторыми разными приоритетами, причем высокоуровневые задачи не будут полностью отнимать процессорное время у низкоуровневых.

Состояние Blocked (заблокировано)

Говорят, что ожидающая событие задача находится в состоянии Заблокировано (Blocked state), которое является подсостоянием состояния Not Running.

Задачи могут войти в Blocked state для ожидания событий двух разных типов:

1. События времени — событие, которое возникает при истечении периода задержки или при достижении абсолютного времени. Например, задача может войти в Blocked state для ожидания прохождения 10 миллисекунд времени.

2. События синхронизации — событие, поступившее от другой задачи или от прерывания. Например, задача может войти в Blocked state для ожидания поступления данных (появления их в очереди). События синхронизации покрывают широкий диапазон типов событий.

Во FreeRTOS могут использоваться очереди, двоичные семафоры, семафоры со счетчиком, рекурсивные семафоры, мьютексы — для создания событий синхронизации. Части 2 и 3 рассматривают это более подробно.

Можно устроить для задачи событие синхронизации с таймаутом, и эффективно обрабатывать оба типа событий (события времени и синхронизации). Например, задача может ожидать максимум 10 миллисекунд события появления данных в очереди. Задача покинет состояние Blocked state либо при поступлении данных на очереди, либо по истечении 10 миллисекунд (что означает таймаут поступления данных).

Состояние Suspended (приостановлено)

Suspended также является подсостоянием состояния Not Running. Задачи в состоянии suspended недоступны для шедулера. Есть только один способ входа в состояние Suspended — через вызов API функции vTaskSuspend(), и только один способ выхода из состояния Suspended — через вызов API функции vTaskResume() или (если вызов происходит из прерывания) xTaskResumeFromISR(). Большинство приложений никогда не используют состояние Suspended.

void vTaskSuspend( xTaskHandle pxTaskToSuspend );

Чтобы функция vTaskSuspend была доступна, нужно включить заголовочный файл task.h, и задать макроопределение INCLUDE_vTaskSuspend в значение 1. Функция vTaskSuspend приостанавливает задачу, переводя её в состояние Suspended, освобождая тем самым процессорное время для других задач. Вызовы vTaskSuspend не аккумулятивны, то есть вызов vTaskSuspend дважды для той же самой задачи все равно требует однократного вызова vTaskResume() для возобновления этой приостановленной задачи. Параметр pxTaskToSuspend передает хендл задачи, которая должна быть приостановлена. Если передать в качестве параметра NULL, то будет приостановлена вызывающая задача (т. е. если задача вызовет vTaskSuspend с параметром NULL, то она приостановит саму себя). Пример использования:

void vAFunction( void )
{ xTaskHandle xHandle;   // Создание задачи, сохранение значения хендла. xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );   // ...   // Использование хендла для приостановки созданной задачи. vTaskSuspend( xHandle );   // ...   // Созданная задача не будет работать в это время, за исключением случаев, // когда другая задача вызовет vTaskResume( xHandle ).   //...   // Приостановка задачей самой себя. vTaskSuspend( NULL );   // Мы не сможем попасть в эту точку кода, пока другая задача не вызовет // vTaskResume с хендлом этой задачи в качестве параметра.
}

Таким образом, если задача находится в состоянии Suspended (приостановлено), то вывести из этого состояния может только вызов функции vTaskResume.

void vTaskResume( xTaskHandle pxTaskToResume );

Чтобы функция vTaskResume была доступна для использования в коде программы, нужно подключить заголовочный файл task.h и определить макро INCLUDE_vTaskSuspend в значение 1. В качестве параметра передается хендл возобновляемой задачи. После вызова vTaskResume задача переводится в состояние Ready (готова к запуску), т. е. выполнение задачи будет возобновлено шедулером. Пример использования:

void vAFunction( void )
{ xTaskHandle xHandle;   // Создание задачи, сохранение хендла. xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );   // ...   // Использование этого хендла для приостановки созданной задачи. vTaskSuspend( xHandle );   // ...   // Созданная задача не будет работать в течение этого периода, пока // другая задача не вызовет vTaskResume( xHandle ).   //...   // Самостоятельное возобновление приостановленной задачи. vTaskResume( xHandle );   // Созданная задача еще раз получит процессорное время микроконтроллера // в соответствии с назначенным приоритетом в системе.
}

Для возобновления функций из тела обработчика прерывания (ISR) служит функция xTaskResumeFromISR.

portBASE_TYPE xTaskResumeFromISR( xTaskHandle pxTaskToResume );

Чтобы функция xTaskResumeFromISR была доступна для использования в коде программы, нужно подключить заголовочный файл task.h и определить макросы INCLUDE_vTaskSuspend и INCLUDE_xTaskResumeFromISR в значение 1. Подробности см. в секции конфигурирования FreeRTOS. В качестве параметра передается хендл возобновляемой задачи.

Функция xTaskResumeFromISR() не должна использоваться в целях синхронизации задачи с прерыванием, если есть шанс, что задача еще не была остановлена, так как такая ситуация может привести к пропуску прерываний. Использование семафора для синхронизации позволит обойти эту проблему.

На выходе функция xTaskResumeFromISR возвратит pdTRUE, если возобновление задачи должно произойти при переключении контекста, иначе будет возвращено pdFALSE. Это используется обработчиком прерывания, чтобы определить, нужно ли делать переключение контекста после завершения ISR. Пример использования:

xTaskHandle xHandle; 
void vAFunction( void )
{
// Создание задачи, сохранение хендла.
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle ); 
   // ... остальная часть кода.
}  
void vTaskCode( void *pvParameters )
{ // Задача, которая приостановлена и возобновлена. for( ;; ) { // ... здесь выполните некоторую функцию.   // Эта задача приостанавливает сама себя. vTaskSuspend( NULL );   // Задача теперь приостановлена, так что в это место управление не дойдет,
// пока ISR не возобновит эту задачу.
}
}  void vAnExampleISR( void )
{ portBASE_TYPE xYieldRequired;   // Возобновление приостановленной задачи. xYieldRequired = xTaskResumeFromISR( xHandle );   if( xYieldRequired == pdTRUE ) { // Мы должны переключить контекст, поэтому ISR вернет управление
// в другую задачу.
// Примечание: как это будет осуществлено, зависит от порта FreeRTOS,
// который Вы используете. Для получения информации ознакомьтесь
// с документацией на Ваш порт. portYIELD_FROM_ISR(); }
}

Состояние Ready (готово к запуску)

О задачах, которые находятся в состоянии Not Running, а также не в состояниях Blocked или Suspended, говорят, что они находятся в состоянии Ready. Они могут быть запущены и, таким образом, ‘готовы к запуску’ (Ready), но в настоящий момент не находятся в состоянии Running.

Полная диаграмма перехода задачи из одного состояния в другое

Рисунок 7 расширяет предыдущую упрощенную диаграмму состояний, чтобы показать все подсостояния Not Running, описанные в этой секции. Задачи, создаваемые в недавних примерах, не использовали состояния Blocked или Suspended и переходили только между состояниями Ready и Running — как показано толстыми стрелками на рисунке 7.

FreeRTOS-pict07-state-transitions.png

Рис. 7. Полная машина состояний задачи.

Пример 4. Использование состояния Blocked для создания задержки

Все задачи, создаваемые в недавних примерах, были ‘периодическими’ — они делали задержку на некоторый период, печатали свою строку, генерировали задержку снова, снова печатали строку, и так далее по кругу. Задержка генерировалась очень грубым методом с использованием пустого цикла — задача просто опрашивала значение инкрементируемой переменной цикла на достижение им фиксированного значения. В примере 3 хорошо виден недостаток такого метода. При прохождении пустого цикла задача остается в состоянии Ready, впустую отнимая полезное процессорное время от других задач.

Имеются также некоторые другие недостатки любой формы опроса (polling), заключающиеся не только в неэффективности. Во время опроса задача не делает действительно полезной работы, однако использует при этом процессорное время по максимуму. В примере 4 такое поведение исправлено путем замены опроса в пустом цикле вызовом API функции vTaskDelay(), прототип которой показан в листинге 11. Новое определение задачи показано в листинге 12.

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

void vTaskDelay( portTickType xTicksToDelay );

Листинг 11. Прототип API функции vTaskDelay().

Таблица 2. Параметры функции vTaskDelay().

Имя параметра Описание
 xTicksToDelay Количество тиков прерываний, в течение которых вызывающая задача должна оставаться в состоянии Blocked перед переходом обратно в состояние Ready.

Например, если задача сделала вызов vTaskDelay( 100 ), а счетчик тиков (системная переменная FreeRTOS) при этом был равен 10000, то задача немедленно войдет в состояние Blocked и останется в нем до тех пор, пока счетчик тиков не достигнет 10100.

Константа portTICK_RATE_MS может использоваться для преобразования миллисекунд в тики.

void vTaskFunction( void *pvParameters )
{
char *pcTaskName;

  /* Строка для вывода на печать, переданная через параметр. Здесь
     параметр преобразуется в указатель на строку. */
  pcTaskName = ( char * ) pvParameters;

  /* Как и большинство других задач, эта задача реализована как бесконечный цикл. */
  for( ;; )
  {
     /* Печать имени этой задачи. */
     vPrintString( pcTaskName );

     /* Задержка на некоторый период времени. Эта задержка создается благодаря
        использованию вызова vTaskDelay(), которым задача помещается в состояние
        Blocked до истечения периода задержки. Период задержки указывается в 'тиках',
        но можно использовать константу portTICK_RATE_MS для преобразования
        (более удобной для пользователя) величины миллисекунд в тики.
        В нашем случае указан период 250 миллисекунд. */
     vTaskDelay( 250 / portTICK_RATE_MS );
  }
}

Листинг 12. Исходный код примера задачи после того, как пустой цикл был заменен на вызов vTaskDelay().

Несмотря на то, что на этот раз обе задачи также были созданы с разными приоритетами, они теперь обе могут запускаться. Вывод примера 4 показан на рисунке 8, что подтверждает такое ожидаемое поведение.

FreeRTOS-pict08-example4-output.PNG

Рис. 8. Вывод, который производит при выполнении пример 4.

Последовательность выполнения, показанная на рисунке 9, объясняет, почему обе задачи теперь работают, несмотря на то, что они созданы с разными приоритетами. Для упрощения выполнение самого ядра на рисунке не показано.

Задача ожидания (idle task) всегда создается автоматически, когда запускается шедулер, чем обеспечивается выполнение всегда как минимум одной задачи, которая всегда может быть запущена (т. е. найдется всегда как минимум одна задача, находящаяся в состоянии Ready). В секции главы 1.7 задача Idle Task описывается более подробно.

FreeRTOS-pict09-timing.png

Рис. 9. Последовательность выполнения, когда задачи используют vTaskDelay() вместо пустого цикла.

Изменена только реализация наших двух задач, их функциональность не поменялась. Сравнение рисунков 9 и 4 показывает, что эта функциональность достигнута более эффективным способом.

Рисунок 4 показывает диаграмму выполнения, когда задачи используют пустой цикл для создания задержки — из за этого они всегда могут быть запущены, и используют процессорное время впустую. Рисунок 9 показывает диаграмму выполнения, когда задачи входят в состояние Blocked на весь период задержки, поэтому процессорное время используется только для действительно необходимой работы (для этого примера — простой вывод сообщения на печать в экран консоли).

В сценарии на рисунке 9 каждый раз задачи покидают состояние Blocked и работают только в части периода тика, перед тем как снова войти в состояние Blocked. Почти все процессорное время оказывается свободным — нет задач приложения, которые могут запуститься (нет задач приложения в состоянии Ready), и нет задач, чтобы шедулер их выбрал для входа в состояние Running. Поэтому будет запущена задача Idle Task. Время, в котором работает задача Idle Task, дает возможность измерить запас процессорного времени в системе.

Толстыми линиями на рисунке 10 показаны переходы состояний, которые делаются задачами в примере 4, где каждая из задач теперь переходит через состояние Blocked перед тем, как вернуться в состояние Ready.

FreeRTOS-pict10-state-transitions.png

Рис. 10. Толстые линии показывают переходы состояний, выполняемые задачами в примере 4.

API функция vTaskDelayUntil()

Функция vTaskDelayUntil() работает аналогично vTaskDelay(). Как было продемонстрировано, параметр функции vTaskDelay() указывает количество тиков прерываний, которые должны произойти между вызовом из задачи vTaskDelay() и моментом времени, когда та же самая задача выйдет снова из состояния Blocked. Величина времени, в течение которого задача остается заблокированной, указывается в параметре vTaskDelay(), но реальное время, в которое задача покинет заблокированное состояние, отсчитывается относительно времени, когда был произведен вызов vTaskDelay(). Вместо этого в параметре функции vTaskDelayUntil() указывается явное значение счетчика тиков, на котором вызывающая эту функцию задача должна перейти из состояние Blocked в состояние Ready. API функция vTaskDelayUntil() должна использоваться, когда требуется фиксированный период выполнения задачи (например, Вы хотите, чтобы задача выполнялась периодически с фиксированной частотой). Так как время разблокировки вызывающей задачи является абсолютным (в отличие от относительного, отсчитываемого от вызова функции, как в случае с vTaskDelay()).

void vTaskDelayUntil( portTickType * pxPreviousWakeTime,
                      portTickType xTimeIncrement );

Листинг 13. Прототип API функции vTaskDelayUntil().

Таблица 3. Параметры функции vTaskDelayUntil().

Имя параметра Описание
 pxPreviousWakeTime Этот параметр поименован так из предположения, что vTaskDelayUntil() выполняется периодически и с фиксированной частотой. В этом случае переменная, на которую указывает pxPreviousWakeTime, удерживает время, в которое задача покинула состояние Blocked (т. е. время, когда задача ‘проснулась’). Это время используется как точка отсчета для вычисления момента времени, когда произойдет следующий выход из состояния Blocked.

Переменная, на которую указывает pxPreviousWakeTime, обновляется автоматически внутри функции vTaskDelayUntil(), и она обычно не должна быть модифицирована кодом приложения за исключением первоначальной инициализации. В листинге 14 показано, как это нужно делать.

 xTimeIncrement Этот параметр также поименован в предположении, что функция vTaskDelayUntil() используется для реализации, которая выполняется периодически и с фиксированной частотой — частота устанавливается значением параметра xTimeIncrement. Величина xTimeIncrement указывается в ‘тиках’. Можно использовать константу portTICK_RATE_MS для преобразования миллисекунд в тики.

Пример 5. Преобразование задач из примера 4 для использования функции vTaskDelayUntil()

Две задачи, созданные в примере 4, являются периодическими, но использование vTaskDelay() не гарантирует, что частота запуска задачи будет фиксированной, так как время, в которое задача покидает состояние Blocked, отсчитывается относительно момента вызова vTaskDelay(). Преобразование задач на использование vTaskDelayUntil() вместо vTaskDelay() решит эту потенциальную проблему.

void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
portTickType xLastWakeTime;

  /* Строка для вывода на печать, переданная через параметр. Здесь
     параметр преобразуется в указатель на строку. */
  pcTaskName = ( char * ) pvParameters;

  /* Переменная xLastWakeTime нуждается в инициализации текущим
     значением счетчика тиков. Имейте в виду, переменная записывается
     явно только в этот момент. Затем xLastWakeTime обновляется
     автоматически внутри функции vTaskDelayUntil(). */
  xLastWakeTime = xTaskGetTickCount();

  /* Как и большинство других задач, эта задача реализована как бесконечный цикл. */
  for( ;; )
  {
     /* Печать имени этой задачи. */
     vPrintString( pcTaskName );

     /* Эта задача должна выполняться точно каждые 250 миллисекунд. Как и
        в функции vTaskDelay(), время измеряется в тиках, и константа
        portTICK_RATE_MS используется для преобразования миллисекунд в тики.
        Переменная xLastWakeTime автоматически обновляется внутри функции
        vTaskDelayUntil(), и нигде явно в коде задачи переменная xLastWakeTime
        не обновляется. */
     vTaskDelayUntil( &xLastWakeTime, ( 250 / portTICK_RATE_MS ) );
  }
}

Листинг 14. Реализация примера задачи с использованием vTaskDelayUntil().

Вывод, производимый примером 5, абсолютно совпадает с выводом из примера 4, показанным на рисунке 8.

Пример 6. Комбинирование блокирующихся и не блокирующихся задач

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

· две задачи создаются с приоритетом 1. Они ничего не делают кроме того, что постоянно выводят на печать строку. Эти задачи не делают никаких вызовов API, которые вводят их в состояние Blocked, поэтому они всегда находятся в состоянии либо Ready, либо Running. Задачи с таким поведением называют задачами ‘с непрерывной обработкой’ (‘continuous processing’) — это задачи, которые всегда должны что-то делать, выполняя обычно не такую тривиальную работу, как в нашем примере. Исходный код задач с непрерывной обработкой показан в листинге 15.

· третья задача создается с приоритетом 2, который выше приоритета двух других задач. Третья задача также просто выводит строку на печать, однако она периодически использует вызовы API функции vTaskDelayUntil(), чтобы поместить саму себя в состояние Blocked между каждым повтором печати. Исходный код этой периодической задачи показан в листинге 16.

void vContinuousProcessingTask( void *pvParameters )
{
char *pcTaskName;

  /* Строка для вывода на печать, переданная через параметр. Здесь
     параметр преобразуется в указатель на строку. */
  pcTaskName = ( char * ) pvParameters;

  /* Как и большинство других задач, эта задача реализована как бесконечный цикл. */
  for( ;; )
  {
     /* Печать имени этой задачи, что постоянно повторяется без блокирования
        или задержки. */
     vPrintString( pcTaskName );
  }
}

Листинг 15. Задача с непрерывной обработкой, используемая в примере 6.

void vPeriodicTask( void *pvParameters )
{
portTickType xLastWakeTime;

  /* Переменная xLastWakeTime нуждается в инициализации текущим
     значением счетчика тиков. Имейте в виду, переменная записывается
     явно только в этот момент. Затем xLastWakeTime обновляется
     автоматически внутри функции vTaskDelayUntil(). */
  xLastWakeTime = xTaskGetTickCount();

  /* Как и большинство других задач, эта задача реализована как бесконечный цикл. */
  for( ;; )
  {
     /* Печать имени этой задачи. */
     vPrintString( "Periodic task is running\r\n" );
     /* Эта задача должна выполняться точно через каждые 10 миллисекунд. */
     vTaskDelayUntil( &xLastWakeTime, ( 10 / portTICK_RATE_MS ) );
  }
}

Листинг 16. Периодически выполняемая задача, используемая в примере 6.

На рисунке 11 показан вывод в консоль примера 6, с разъяснением наблюдаемого поведения, которое дает последовательность выполнения, показанная на рисунке 12.

FreeRTOS-pict11-example6-output.PNG

Рис. 11. Вывод, который производит при выполнении пример 6.

Примечание: вывод был получен с использованием симулятора DOSBox — для замедления выполнения до уровня, когда поведение всех задач приложения можно рассмотреть на одном скриншоте.

FreeRTOS-pict12-timing.png

Рис. 12. Диаграмма выполнения примера 6.

[1.7. Специальная Задача Ожидания (IDLE TASK) и хук задачи ожидания (IDLE TASK HOOK)]

Задачи, созданные в примере 4, почти все время находятся в состоянии Blocked. В этом состоянии они не могут быть запущены и не могут быть выбраны шедулером. Любой процессор всегда должен что-то делать, поэтому как минимум одна задача должна быть в состоянии войти в режим Running. Чтобы обеспечить это условие, при вызове vTaskStartScheduler() шедулер автоматически создает специальную задачу ожидания Idle Task. Задача Idle Task почти ничего не делает, кроме того как находится в цикле — так что все другие задачи, наподобие задач из наших примеров, всегда могут быть запущены.

Запуск Idle Task с самым низким приоритетом (приоритетом 0) дает гарантию, что Idle Task немедленно выйдет из состояния Running, как только любая задача (которая конечно имеет более высокий приоритет, чем Idle Task) войдет в состояние Ready. Это можно увидеть на рисунке 9, где Idle Task немедленно приостанавливается, чтобы позволить задаче 2 выполниться, как только задача 2 выйдет из состояния Blocked. Говорят, что задача 2 ВЫТЕСНЯЕТ задачу Idle Task (Task 2 pre-empted Idle Task). Вытеснение происходит автоматически, без необходимости что-то знать о вытесняющей задаче.

Функции IDLE TASK HOOK

В задачу Idle Task можно добавить функционал приложения пользователя. Это делается через функцию хука Idle (иначе её называют callback-функцией) — она автоматически будет вызываться изнутри Idle Task, каждый раз в цикле ожидания.

Можно использовать Idle Task hook следующим образом:

· Выполнение низкоприоритетных задач в фоновом режиме, или продолжительные обработки данных.

· Измерение свободного процессорного времени (т. е. загруженности процессора) — задача Idle Task будет работать только тогда, когда все другие задачи не выполняют свою работу (им нечего делать), поэтому измерение процессорного времени, выделенного на Idle Task, явно показывает, сколько процессорного времени имеется в запасе.

· Перевод процессора в режим пониженного энергопотребления — предоставление автоматического метода сохранения энергии, когда приложением не выполняется полезная обработка данных.

Ограничения, связанные с использованием функций Idle Task hook

Функции Idle Task hook должны удовлетворять следующим правилам:

1. Они никогда не должны делать попыток приостановки (переход в состояние Suspended) или блокировки (переход в состояние Blocked). Задача Idle Task будет выполняться только тогда, когда другим задачам нечего делать (за исключением тех случаев, когда задачи приложения имеют тот же приоритет, что и Idle Task). Поэтому блокировка Idle Task приведет к тому, что не будет ни одной задачи, которая могла бы войти в состояние Running!

2. Если приложение использует вызовы API функции vTaskDelete(), то функция Idle Task hook должна всегда быть завершена в течение подходящего периода времени. Причина этого в том, что задача Idle Task отвечает за очистку ресурсов ядра после удаления задачи. Если управление потоком выполнения Idle Task остается постоянно в коде Idle Task hook, то тогда очистка не может быть выполнена.

Функции Idle Task hook должны иметь имя и прототип, показанные в листинге 17.

void vApplicationIdleHook( void );

Листинг 17. Имя и прототип функции хука задачи ожидания (Idle Task hook).

Пример 7. Определение функции Idle Task hook

Использование блокирующих вызовов API функции vTaskDelay() в примере 4 создает некоторый интервал времени ожидания — в это время выполняется задача Idle Task, так как обе задачи приложения находятся в состоянии Blocked. Пример 7 использует это время ожидания путем добавления функции Idle Task hook, исходный код которой показан в листинге 18.

/* Определение переменной, которая будет инкрементирована функцией хука. */
unsigned long ulIdleCycleCount = 0UL;

/* Функции хука Idle ДОЛЖНЫ называться vApplicationIdleHook(), не принимать
  никаких параметров, и возвращать void. */
void vApplicationIdleHook( void )
{
  /* Эта функция хука ничего не делает, кроме инкрементирования счетчика. */
  ulIdleCycleCount++;
}

Листинг 18. Очень простой пример функции Idle Task hook.

Чтобы функция Idle Task hook vApplicationIdleHook вызывалась, в файле FreeRTOSConfig.h нужно установить в 1 макрос configUSE_IDLE_HOOK.

Функция, которая реализует созданные задачи, получила незначительные изменения, чтобы выводить на печать значение ulIdleCycleCount, как показано в листинге 19.

void vTaskFunction( void *pvParameters )
{
char *pcTaskName;

  /* Строка для вывода на печать, переданная через параметр. Здесь
     параметр преобразуется в указатель на строку. */
  pcTaskName = ( char * ) pvParameters;

  /* Как и большинство других задач, эта задача реализована как бесконечный цикл. */
  for( ;; )
  {
     /* Печать имени этой задачи и количества инкрементов переменной
        ulIdleCycleCount. */
     vPrintStringAndNumber( pcTaskName, ulIdleCycleCount );
     /* Задержка на 250 миллисекунд. */
     vTaskDelay( 250 / portTICK_RATE_MS );
  }
}

Листинг 19. Исходный код для примера задачи, которая теперь печатает значение переменной ulIdleCycleCount.

Вывод, производимый примером 7, показан на рисунке 13 и на нем видно, что функция Idle Task hook вызывается (очень приблизительно) 4.5 миллиона раз между каждой итерацией задач приложения.

FreeRTOS-pict13-example7-output.PNG

Рис. 13. Вывод, который производит при выполнении пример 7.

[1.8. Изменение приоритета задачи]

API функция vTaskPrioritySet()

Для изменения приоритета любой задачи после старта шедулера может быть использована API функция vTaskPrioritySet().

void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority );

Листинг 20. Прототип API функции vTaskPrioritySet().

Таблица 4. Параметры функции vTaskPrioritySet().

Имя параметра Описание
 pxTask Хендл задачи (субъект задачи), у которой будет изменен приоритет — см. параметр pxCreatedTask функции API xTaskCreate() для более подробной информации по получению хендлов для задач. Задача может изменить собственный приоритет путем передачи NULL вместо действительного хендла задачи.
 uxNewPriority Приоритет, в который будет установлен субъект задачи. Значение, переданное в этом параметре, автоматически ограничивается величиной максимально доступного приоритета (configMAX_PRIORITIES – 1), где configMAX_PRIORITIES опция времени компиляции, установленная в заголовочном файле FreeRTOSConfig.h.

API функция uxTaskPriorityGet()

Для получения текущего приоритета задачи может быть использована API функция uxTaskPriorityGet().

unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask );

Листинг 21. Прототип API функции uxTaskPriorityGet().

Таблица 5. Параметры и значение возврата функции uxTaskPriorityGet().

Имя параметра
/
возвращаемое значение
Описание
 pxTask Хендл задачи, приоритет которой запрашивается — см. параметр pxCreatedTask функции API xTaskCreate() для более подробной информации по получению хендлов для задач. Задача может запросить собственный приоритет путем передачи NULL вместо действительного хендла задачи.
Возвращаемое значение Значение запрошенного приоритета, который в настоящий момент назначен задаче.

Пример 8. Изменение приоритета задачи

В качестве задачи для входа в состояние Running шедулер всегда выбирает задачу в состоянии Ready, которая имеет наивысший приоритет. Пример 8 демонстрирует это, используя API функцию vTaskPrioritySet() для изменения приоритета одной задачи относительно другой.

Две задачи создаются с разными приоритетами. Никакая из задач не делает вызовов API для входа в состояние Blocked, так что обе задачи всегда находятся либо в состоянии Ready, либо Running — так что задача с более высоким приоритетом (относительно другой задачи) будет всегда выбираться шедулером для входа в состояние Running.

Пример 8 работает следующим образом:

· Задача 1 (см. листинг 22) создается с самым высоким приоритетом — это гарантирует, что она запустится первой. Задача 1 печатает набор строк перед тем, как повысит приоритет задачи 2 (см. листинг 23) выше собственного приоритета.

· Задача 2 запускается на выполнение (входит в режим Running), как только она получит более высокий относительный приоритет. В любой момент времени в состоянии Running может находиться только одна задача, поэтому когда задача 2 находится в состоянии Running, задача 1 находится в состоянии Ready.

· Задача 2 выводит на печать сообщение перед тем как установить свой приоритет в значение ниже приоритета задачи 1.

· Задача 2 устанавливает свой приоритет обратно вниз, что означает получение задачей 1 снова наивысшего приоритета. Поэтому задача 1 снова входит в состояние Running, принуждая задачу 2 вернуться в состояние Ready.

void vTask1( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;

  /* Эта задача 1 всегда стартует первой, перед задачей 2, так как она создана
     с более высоким приоритетом. Ни задача 1, ни задача 2 никогда не блокируются
     (не находятся в состоянии Blocked), они всегда находятся либо в состоянии
     Running, либо в состоянии Ready.
     Запрос приоритета, при котором эта задача работает. Передача NULL в параметре
     означает "выдайте мой приоритет". */
  uxPriority = uxTaskPriorityGet( NULL );

  for( ;; )
  {
     /* Печать имени этой задачи. */
     vPrintString( "Task1 is running\r\n" );

     /* Установка приоритета задачи 2 выше приоритета задачи 1 приведет к тому,
        что задача 2 немедленно запустится на выполнение (так как задача 2
        будет иметь самый высокий приоритет из двух созданных задач). Обратите
        внимание, что используется хендл задачи 2 (xTask2Handle) в вызове
        vTaskPrioritySet(). Листинг 24 показывает, как этот хендл был получен. */
     vPrintString( "About to raise the Task2 priority\r\n" );
     vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );

     /* Задача 1 запустится только тогда, когда у неё приоритет станет выше,
        чем у задачи 2. Таким образом, при достижении потоком выполнения этой
        точки кода задача 2 уже выполнилась и понизила свой приоритет обратно,
        так что он снова стал ниже приоритета этой задачи 1. */
  }
}

Листинг 22. Реализация задачи 1 в примере 8.

void vTask2( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;

  /* Эта задача 1 всегда стартует первой, перед задачей 2, так как она создана
     с более высоким приоритетом. Ни задача 1, ни задача 2 никогда не блокируются
     (не находятся в состоянии Blocked), они всегда находятся либо в состоянии
     Running, либо в состоянии Ready.
     Запрос приоритета, при котором эта задача работает. Передача NULL в параметре
     означает "выдайте мой приоритет". */
  uxPriority = uxTaskPriorityGet( NULL );

  for( ;; )
  {
     /* Когда управление потоком выполнения дошло до этой точки, задача 1 уже
        отработала и установила приоритет этой задачи 2 выше своего.
        Печать имени этой задачи. */
     vPrintString( "Task2 is running\r\n" );

     /* Установка своего приоритета вниз, обратно к своему первоначальному значению.
        Передача NULL в качестве параметра vTaskPrioritySet() означает "измените
        мой приоритет". Установка приоритета ниже, чем у задачи 1 приведет к тому,
        что задача 1 сразу снова запустится на выполнение и вытеснит эту задачу 2. */
     vPrintString( "About to lower the Task2 priority\r\n" );
     vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
  }
}

Листинг 23. Реализация задачи 2 в примере 8.

Каждая задача может и запросить, и установить собственный приоритет без использования действительного хендла задачи — заместо него просто используется NULL. Хендл задачи нужен только тогда, когда задача хочет запросить или изменить приоритет другой задачи, как например когда задача 1 меняет приоритет задачи 2. Чтобы позволить задаче 1 сделать это, полученный хендл задачи 2 сохраняется, когда создается задача 2 — как пояснено в комментариях листинга 24.

/* Определение переменной для сохранения хендла задачи 2. */
xTaskHandle xTask2Handle;

int main( void )
{
  /* Создание задачи 1 с приоритетом 2. Параметры задачи не используются и
     установлены в NULL. Хендл задачи также не используется и тоже
     установлен в NULL. */
  xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
  /* Задача 1 создана с приоритетом 2 _______^. */

  /* Создание задачи 2 с приоритетом 1, который меньше приоритета задачи 1.
     Снова параметр задачи не используется и установлен в NULL, но на этот
     раз нужен хендл задачи, и в качестве последнего параметра передается
     адрес переменной xTask2Handle. */
  xTaskCreate( vTask2, "Task 2", 1000, NULL, 1,       &xTask2Handle );
  /* В последнем параметре будет сохранен хендл задачи ^^^^^^^^^^^^^ */

  /* Запуск шедулера для начала выполнения задач. */
  vTaskStartScheduler();

  /* Если все хорошо, то управление в main() никогда не дойдет до этой точки,
     и теперь шедулер будет управлять задачами. Если main() довела управление до
     этого места, то это может означать, что не хватает памяти кучи (heap)
     для создания специальной задачи ожидания (idle task, см. раздел 1.7).
     Часть 5 предоставляет больше информации по управлению памятью. */
  for( ;; );
}

Листинг 24. Реализация функции main() для примера 8.

На рисунке 14 показана последовательность, в которой выполняются задачи примера 8, в результате чего получается вывод, показанный на рисунке 15.

FreeRTOS-pict14-timing.png

Рис. 14. Последовательность выполнения задач при запуске примера 8.

FreeRTOS-pict15-example8-output.PNG

Рис. 15. Вывод, который производит при выполнении пример 8.

[1.9. Удаление задачи]

API функция vTaskDelete()

Задача может использовать API функцию vTaskDelete() для удаления самой себя или любой другой задачи.

Удаленные задачи более не существуют, и не могут снова войти в состояние Running.

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

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

void vTaskDelete( xTaskHandle pxTaskToDelete );

Листинг 25. Прототип API функции vTaskDelete().

Таблица 6. Параметры функции vTaskDelete().

Имя параметра Описание
pxTaskToDelete Хендл задачи, которая должна быть удалена (субъект задачи) — см. параметр pxCreatedTask функции API xTaskCreate() для более подробной информации по получению хендлов для задач. Задача может удалить саму себя путем передачи NULL вместо действительного хендла задачи.

Пример 9. Удаление задач

Этот очень простой пример делает следующее:

· Задача 1 создается функцией main() с приоритетом 1. Когда задача 1 запускается, она создает задачу 2 с приоритетом 2. Теперь задача 2 имеет наивысший приоритет, так что она начнет свое выполнение немедленно. Исходный код функции main() показан в листинге 26, а исходный код задачи 1 показан в листинге 27.

· Задача 2 ничего не делает, просто удаляет саму себя. Она могла бы удалить себя простой передачей NULL в качестве параметра функции vTaskDelete(), однако в целях полной демонстрации использует вместо NULL собственный хендл задачи. Исходный код задачи 2 показан в листинге 28.

· Когда задача 2 удалена, задача 1 снова получит наивысший приоритет среди всех имеющихся задач (так как она вообще осталась одна), и поэтому продолжит выполнение — в этот момент она делает вызов vTaskDelay() для блокировки себя (войдет в состояние Blocked) на короткий интервал времени.

· Когда задача 1 находится в состоянии Blocked, запустится Idle Task и освободит память, которая была ранее выделена для удаленной теперь задачи 2.

· Когда задача 1 покидает состояние Blocked, то она снова войдет в режим Ready с наивысшим приоритетом, и вытеснит (pre-empt) задачу Idle Task. Когда задача 1 войдет в состояние Running, она просто создает задачу 2 заново, и так далее — весь процесс повторяется снова и снова.

int main( void )
{
  /* Создание первой задачи 1 с приоритетом 1. Параметр задачи не используется
     и поэтому установлен в NULL. Хендл задачи также не используется и также
     установлен в NULL. */
  xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
  /* Задача создана с приоритетом 1 _________^. */

  /* Запуск шедулера, после чего задачи начнут свое выполнение. */
  vTaskStartScheduler();

  /* После запуска шедулера управление потоком main() никогда не достигнет
     этого места. */
  for( ;; );
}

Листинг 26. Реализация функции main() для примера 9.

void vTask1( void *pvParameters )
{
const portTickType xDelay100ms = 100 / portTICK_RATE_MS;

  for( ;; )
  {
     /* Печать имени этой задачи. */
     vPrintString( "Task1 is running\r\n" );

     /* Создание задачи 2 с более высоким приоритетом. Снова параметр задачи
        не используется и установлен в NULL, однако на этот раз нужен хендл
        задачи, и передается адрес переменной xTask2Handle в качестве
        последнего параметра. */
     xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
     /* Последний параметр - хендл задачи _________^^^^^^^^^^^^^ */

     /* У задачи 2 самый высокий приоритет, так что если управление потоком
        выполнения задачи 1 достигло этой точки, то задача 2 уже выполнилась
        и удалила саму себя. Далее производится задержка на 100 миллисекунд. */
     vTaskDelay( xDelay100ms );
  }
}

Листинг 27. Реализация задачи 1 для примера 9.

void vTask2( void *pvParameters )
{
  /* Задача ничего не делает, кроме того как удаляет саму себя. Для этой цели
     она вызывает vTaskDelete(), и могла бы передать в качестве параметра NULL,
     однако для полной демонстрации она передает вместо этого свой хендл. */
  vPrintString( "Task2 is running and about to delete itself\r\n" );
  vTaskDelete( xTask2Handle );
}

Листинг 28. Реализация задачи 2 для примера 9.

FreeRTOS-pict16-example9-output.PNG

Рис. 16. Вывод, который производит при выполнении пример 9.

FreeRTOS-pict17-timing.png

Рис. 17. Последовательность выполнения примера 9.

[1.10. Обзор алгоритмов шедулинга (планирования выполнения задач)]

Приоритетное планирование запуска задач с вытеснением, вытесняющая многозадачность (Prioritized Preemptive Scheduling)

Примеры в этой главе иллюстрируют, как и когда FreeRTOS выбирает, какая задача должна находиться в состоянии Running:

· Каждой задаче назначен приоритет
· Каждая задача может находиться в одном из некоторых возможных состояний.
· В любой момент времени только одна задача может находиться в состоянии Running.
· Шедулер всегда выберет задачу в состоянии Ready с наивысшим приоритетом — для перевода этой задачи в состояние Running.

Схема шедулинга такого типа называется «вытесняющая многозадачность с фиксированными приоритетами» (Fixed Priority Preemptive Scheduling). «Фиксированные приоритеты» — это потому, что каждой задаче назначен приоритет, который не может быть изменен самим ядром системы, только задачи могут менять приоритет (т. е. только код пользователя). «Вытесняющая» (preemptive) — потому что задача, вошедшая в режим Ready или имеющая измененный приоритет, всегда вытеснит другую задачу из состояния Running, если она имеет более низкий приоритет.

Задачи могут ждать в состоянии Blocked какого-то события, и переместятся обратно в состояние Ready, когда это событие произойдет. События времени срабатывают в определенное время, например когда истекает время блокировки (время нахождения в состоянии Blocked). Это обычно используется для реализации периодических действий или для отсчета таймаута. События синхронизации возникают, когда задача или обработчик прерывания посылает информацию в очередь или в один из нескольких типов семафоров. Это обычно используется для обработки асинхронной активности, как например поступление порции данных от периферийного устройства.

Рисунок 18 демонстрирует все эти типы поведения диаграммой выполнения гипотетического приложения.

FreeRTOS-pict18-timing.png

Рис. 18. Паттерн выполнения с подсвеченными точками вытеснения (pre-emption points).

Комментарии к рисунку 18:

1. Idle Task (задача ожидания)

Задача ожидания запускается с самым низким возможным приоритетом (приоритет 0), поэтому она вытесняется в любой момент времени задачей с более высоким приоритетом, которая входит в состояние Ready — например в моменты времени t3, t5 и t9.

2. Задача 3

Задача 3 является задачей, управляемой по событию, которая выполняется с относительно низким приоритетом, но выше приоритета Idle Task. Задача 3 проводит почти все время в ожидании (состояние Blocked) интересующего события, переходя из состояния Blocked в состояние Ready каждый раз при наступлении события. Все механизмы обмена данными между задачами во FreeRTOS (очереди, семафоры, и т. п.) могут использоваться как сигнализирующие события для разблокировки задач.

События происходят в моменты времени t3, t5, и также где-то между t9 и t12. События t3 и t5 обрабатываются немедленно, так как в это время задача 3 является задачей с наивысшим приоритетом, готовой к запуску. Событие, которое произойдет между t9 и t12, не будет обработано до времени t12, потому что до t12 все еще работают задачи 1 и 2 с более высоким приоритетом, чем задача 3. Только в момент времени t12 обе задачи — и 1, и 2 — находятся в состоянии Blocked, что делает задачу 3 задачей с наивысшим приоритетом, которая находится в состоянии Ready.

3. Задача 2

Задача 2 является периодической, она запускается с более высоким приоритетом, чем задача 3, но у неё приоритет ниже, чем у задачи 1. Интервал периода запуска такой, что задаче 2 нужно запуститься в моменты времени t1, t6 и t9.

В момент времени t6 задача 3 находится в состоянии Running, но задача 2 имеет более высокий приоритет, поэтому вытесняет задачу 3 и запускается немедленно. Задача 2 завершает свою работу и снова входит в режим Blocked в момент времени t7, и тогда задача 3 возвращается в состояние Running для завершения обработки. Задача 3 блокирует саму себя в момент времени t8.

4. Задача 1

Задача 1 также является задачей, запускаемой по событию. Она выполняется с самым высоким приоритетом, поэтому может вытеснять любую другую задачу в системе. Для задачи 1 показано событие только в момент времени t10, когда она вытесняет задачу 2. Задача 2 может завершить свою обработку только после того, как задача 1 снова войдет в режим Blocked (момент времени t11).

Выбор задачи на основе приоритета

Рисунок 18 показывает, как фундаментальное назначение приоритетов влияет на поведение приложения.

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

Техника RMS (Rate Monotonic Scheduling, шедулинг по монолитной скорости) использует общее назначение приоритета, и задает для каждой задачи уникальный приоритет в соответствии со скоростью периодического выполнения. Самый низкий приоритет назначается задаче, у которой самая низкая частота периодического выполнения. Назначение приоритетов таким способом показывает максимальную ‘управляемость’ шедулером всего приложения, однако вариации времени выполнения и тот факт, что не все задачи являются периодическими, делает очень сложным прямой расчет поведения приложения.

Кооперативная многозадачность (Co-operative Scheduling)

Эта книга в основном рассматривает вытесняющую многозадачность (preemptive scheduling). FreeRTOS может опционально использовать кооперативную многозадачность (co-operative scheduling).

Когда используется чистый кооперативный шедулер, то переключение контекста (смена выполняемой задачи) произойдет только либо если задача перейдет из режима Running в режим Blocked, либо если задача в состоянии Running явно сделает вызов taskYIELD(). Задачи не вытесняются, и задачи с одинаковым приоритетом не делят автоматически время процессора. Поэтому кооперативная многозадачность проще, но её использование может привести к получению менее отзывчивой системы.

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

[Следующая часть: FreeRTOS: практическое применение, часть 2 (управление очередями)]

[Ссылки]

1. USING THE FREERTOS REAL TIME KERNEL (Richard Barry) site:profdong.com.
2. Обзор FreeRTOS на русском языке — Андрей Курниц, статья в журнале «Компоненты и технологии» (2..10 номера 2011 года).
3. 150422FreeRTOS-API.zip — документация по API FreeRTOS 8.2.х на английском языке.

Время на прочтение
5 мин

Количество просмотров 192K


Здравствуйте. В короткой серии постов я постараюсь описать возможности, и подходы работы с одной из наиболее популярной и развивающейся РТОС для микроконтроллеров – FreeRTOS. Я предпологаю базовое знакомство читателя с теорией многозадачности, о которой можно почитать в одном из соседних постов на Хабре или ещё где-то.
Ссылки на остальные части:
FreeRTOS: межпроцессное взаимодействие.
FreeRTOS: мьютексы и критические секции.

Зачем все это? Или введение в многозадачные системы, от создателей FreeRTOS.

Традиционно существует 2 версии многозадачности:

  • «Мягкого» реального времени(soft real time)
  • «Жесткого» реального времени(hard real time)

К ОСРВ мягкого типа можно отнести наши с Вами компьютеры т.е. пользователь должен видеть, что, например, нажав кнопку с символом, он видит введенный символ, а если же он нажал кнопку, и спустя время не увидел реакции, то ОС будет считать задачу «не отвечающей»( по аналогии с Windows — «Программа не отвечает»), но ОС остается пригодной для использования. Таким образом, ОСРВ мягкого времени просто определяет предполагаемое время ответа, и если оно истекло, то ОС относит таск к не отвечающим.

К ОСРВ жесткого типа, как раз относят ОСРВ во встраиваемых устройствах. В чем-то они похоже на ОСРВ на дестопах(многопоточное выполнение на одном процессоре), но и имеют главное отличие — каждая задача должна выполняться за отведенный квант времени, не выполнение данного условия ведет к краху все системы.

А все таки зачем?

Если у Вас есть устройство с нетривиальной логикой синхронизации обмена данными между набором сенсоров, если Вам действительно нужно гарантировать время отклика, и наконец-то если Вы думаете, что система может разростись, но не знаете насколько, то РТОС Ваш выбор.

Не стоит применять РТОС, для применения РТОС т.е. не нужно применять РТОС в слишком тривиальных задачах(получить данные с 1 сенсора, и отправить дальше, обработать нажатие 1 кнопки и т.д) т.к. это приведет к ненужной избыточности, как полученного кода, так и решения самой задачи.

Работа с тасками(или задачами, процессами).

Для начала приведу несколько определений, для того чтобы внести ясность в дальнейшие рассуждения:

«Операционные системы реального времени (ОСРВ(RTOS)) предназначены для обеспечения интерфейса к ресурсам критических по времени систем реального времени. Основной задачей в таких системах является своевременность (timeliness) выполнения обработки данных».
«FreeRTOS — многозадачная операционная система реального времени (ОСРВ) для встраиваемых систем. Портирована на несколько микропроцессорных архитектур.
От хабраюзера andrewsh, по поводу лицензии: разрешено не публиковать текст приложения, которое использует FreeRTOS, несмотря на то, что OS линкуется с ним. Исходники самой же RTOS должны всегда прикладываться, изменения, внесённые в неё — тоже.».

FreeRTOS написана на Си с небольшим количеством ассемблерного кода(логика переключения контекста) и ее ядро представлено всего 3-мя C файлами. Более подробно о поддерживаемых платформах можно почитать на официальном сайте.

Перейдем к делу.
Любой таск представляет собой Си функцию со следующим прототипом:

void vTask( void *pvParametres );

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

Тело таска не должно содержать явных return; конструкций, и в случае если таск больше не нужен, его можно удалить с помощью вызова API функции. Следующий листинг, демонстрирует типичный скелет таска:

void vTask( void *pvParametres) {
    /* Данный фрагмент кода будет вызван один раз, перед запуском таска.
       Каждый созданный таск будет иметь свою копию someVar.
       Кроме объявления переменных, сюда можно поместить некоторый инициализационный код.    
    */
    int someVar;

    // Так как каждый таск - это по сути бесконечный цикл, то именно здесь начинается тело таска.
    for( ;; ) {
        // Тело таска
    }
    
    // Так как при нормальном поведении мы не должны выходить из тела таска, то в случае если это все таки произошло, мы удаляем таск.
    // Функция vTaskDelete принимает в качестве аргумента хэндл таска, который стоит удалить.
    // Вызов внутри тела таска с параметром NULL,удаляет текущий таск
    
    vTaskDelete( NULL );
}

Для создания таска, и добавления ее в планировщик используется специальная API функция со следующим прототипом:

portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,  
                           const signed portCHAR * const pcName,  
                           unsigned portSHORT usStackDepth,  
                           void *pvParameters,  
                           unsigned portBASE_TYPE uxPriority,  
                           xTaskHandle *pxCreatedTask  
                         );

pvTaskCode – так как таск – это просто Си функция, то первым параметром идет ее значение.

pcName – имя таска. По сути это нигде не используется, и полезно только при отладке с соответствующими плагинами для IDE.

usStackDepth – так как каждый таск – это мини подпрограмма, со своим стэком, то данный параметр отвечает за его глубину. При скачивании RTOS и разворачивания системы для своей платформы, вы получаете файл FreeRTOSConfig.h настройкой которого можно конфигурировать поведение самой ОС. В данном файле также объявлена константная величина configMINIMAL_STACK_SIZE, которую и стоит передавать в качестве usStackDepth с соответствующим множителем, если это необходимо.

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

uxPriority – каждый таск имеет свой собственный приоритет, от 0(min) до (configMAX_PRIORITIES – 1). Так как, по сути, нет верхнего предела для данного значения, то рекомендуется использовать как можно меньше значений, чтобы не было дополнительно расхода RAM на данную логику.

pxCreatedTask — хэндл созданного таска. При создании таска, опционально можно передать указатель на хэндл будующего таска, для последующего управления работой самого таска. Например, для удаления определенного таска.

Данная функция возвращает pdTRUE, в случае успешного создания таска, или errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY, в случае если размер стэка был указан слишком большим, т.е. недостаточно размера хипа для хранения стэка таска, и самого таска.

На следующем листинге я привел, короткий пример законченной программы, которая создает 2 таска, каждый из которых мигает светодиодом:

void vGreenBlinkTask( void *pvParametrs ) {
	for( ;; ) {
		P8OUT ^= BIT7;
		
		// Выполнить задержку в 700 FreeRTOS тиков. Величина одного тика задана в FreeRTOSConfig.h и как правило составляет 1мс.
		vTaskDelay( 700 );
	}
}

void vRedBlinkTask( void *pvParametrs ) {
	for( ;; ) {
		P8OUT ^= BIT6;
		
		// Выполнить задержку в 1000 FreeRTOS тиков. Величина одного тика задана в FreeRTOSConfig.h и как правило составляет 1мс.
		vTaskDelay( 1000 );
	}
}

void main(void) {
	// Инициализация микроконтроллера. Данный код будет у каждого свой.
	vInitSystem();

	// Создание тасков. Я не включил код проверки ошибок, но не стоит забывать об этом!
	xTaskCreate( &vGreenBlinkTask,
	             (signed char *)"GreenBlink",
	             configMINIMAL_STACK_SIZE, 
	             NULL,
	             1,
	             NULL );
	xTaskCreate( &vRedBlinkTask, 
	             (signed char *)"RedBlink",
	             configMINIMAL_STACK_SIZE,
	             NULL,
	             1,
	             NULL );

	// Запуск планировщика т.е начало работы тасков.
	vTaskStartScheduler();
	
	// Сюда стоит поместить код обработки ошибок, в случае если планировщик не заработал. 
	// Для примера я использую просто бесконечный цикл.
	for( ;; ) { }
}

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

При разработке электронных устройств до поры до времени (иногда довольно долго) удается обходиться без использования операционной системы. Но наступает момент, когда разрабатываемый девайс должен выполнять огромное количество различных функций или, например, должен обеспечивать возможность добавления новой задачи позднее. В этих случаях обойтись без RTOS (операционная система реального времени, ОСРВ) становится невозможно. Как раз про RTOS эта статья, а точнее про ее установку и использование для микроконтроллеров STM32.

Сразу скажу, что я остановил свой выбор на FreeRTOS – эта ОСРВ портирована на огромное количество архитектур, что является, безусловно, большим плюсом. Есть в сети очень хороший мануал на русском языке, там правда очень много букв, но зато все описано максимально подробно и понятно.

Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там — STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также статья на смежную тему из нового курса: STM32CubeMx. Быстрый старт с FreeRTOS для STM32.

Итак, в чем же кроется основная идея ОСРВ?

Операционная система реального времени позволяет организовать множество разнообразных задач и обеспечивает их поочередное выполнение и при этом учитывает приоритет каждой задачи. Также благодаря RTOS можно замутить запуск какой-либо задачи через определенные промежутки времени (причем обеспечивается высокая точность временных интервалов). Задачи могут обмениваться информацией друг с другом при помощи реализованной в RTOS очереди. И наконец, при необходимости возможно легким движением руки добавлять все новые и новые задачи. Конечно же, есть и минусы, связанные в основном с тем, что RTOS занимает довольно много памяти. Но эта проблема актуальна скорее для более старых моделей AVR и других контроллеров с небольшими объемами памяти. Так что нас это не будет сильно волновать.

Что можно сказать конкретно про FreeRTOS… FreeRTOS – операционная система с открытым исходным кодом. Можно абсолютно бесплатно и без проблем скачать ее последнюю версию (что кстати необходимо сделать, поскольку мы скоро перейдем к созданию проекта под FreeRTOS). Написана библиотека на C с наличием небольших ассемблерных вставок. Опять же, очень советую прочитать мануал на нее. Там и про различные типы многозадачности, про ОСРВ в целом, про организацию прерываний, про разделение процессорного времени между задачами. В общем, почитать стоит. Мы же рассмотрим практические аспекты установки FreeRTOS, естественно по ходу процесса постараюсь по максимуму объяснять, что и зачем делается. Итак, приступаем…

Для начала дружно скачаем FreeRTOS. У меня, например, FreeRTOS v.7.1.0, по-моему, это последняя версия (на момент написания статьи). Копируем папку с FreeRTOS куда-нибудь поближе к проекту. Именно копируем а не вырезаем/вставляем, поскольку сейчас мы будем удалять из нее то, что нам пока не нужно )

Скопировали, продолжаем…

Я создал в папке с проектом пару папок – Header_Files и Source_Files для заголовочных файлов и файлов с исходным кодом соответственно. Пусть пока для простоты все файлы, которые понадобятся для запуска RTOS лежат там.

Заходим в папку FreeRTOSV7.1.0\FreeRTOSV7.1.0\Demo и удаляем оттуда все, кроме папки CORTEX_STM32F103_Keil. Заходим в единственную оставшуюся папку и обращаем наше внимание на файл FreeRTOSConfig.h. Копируем его в папку header’ов. Давайте его сразу же и отредактируем. Открываем и копируем в этот файл следующие строки:

#define xPortSysTickHandler                                      SysTick_Handler
#define xPortPendSVHandler                                       PendSV_Handler
#define vPortSVCHandler                                          SVC_Handler

Давайте разбираться, что мы сделали. А всего лишь переопределили обработчики прерываний для того, чтобы прерывания из FreeRTOS попали в вектор прерываний.

Папка FreeRTOSV7.1.0\FreeRTOSV7.1.0\Source – копируем из нее все .c файлы в STMProjects\FreeRTOS\Source_Files. Аналогично, файлы из FreeRTOSV7.1.0\FreeRTOSV7.1.0\Source\include помещаем в STMProjects\FreeRTOS\Header_Files.

Теперь проследуем в FreeRTOSV7.1.0\FreeRTOSV7.1.0\Source\portable и там прямо манит папка под названием Keil. Заходим и видим «Nothing to see here». Первая мысль – «Как же так?…». Но не отчаиваемся, нам намекают посмотреть папку RVDS – заходим в нее, выбираем наше ядро (папка ARM_CM3) и забираем оттуда два файла к нам в проект.

Осталось зайти в папку FreeRTOSV7.1.0\Source\portable\MemMang и скопировать файл heap_2.c. Все файлы собрали, создаем новый проект в Keil и приступаем ко второй части.

Теперь нам нужно добавить все файлы в проект – это и CMSIS, и SPL, и FreeRTOS:

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

В настройках проекта прописываем все пути к файлам, присутствующим в проекте, а также не забываем про USE_STDPERIPH_DRIVER:

Настройка Keil.

И, наконец, создадим практический пример! Давайте сделаем традиционное мигание диодом, но уже с использованием ОСРВ. Для начала надо подключить файлы:

#include "task.h"
#include "queue.h"
#include "FreeRTOS.h"
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

Теперь надо настроить нужный нам вывод, на который мы повесим светодиод. Кстати, в мануале на FreeRTOS есть еще и правила, в соответствии с которыми рекомендуется оформлять код:

void vFreeRTOSInitAll()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	GPIO_StructInit(&port);
	port.GPIO_Mode = GPIO_Mode_Out_PP;
	port.GPIO_Pin = GPIO_Pin_0;
	port.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &port);
}

Тут все как обычно, включили тактирование, настроили вывод. Двигаемся дальше:

void vLedTask (void *pvParameters)
{
	while(1)
	{
		if (state == 0)
		{
			GPIO_SetBits(GPIOA,GPIO_Pin_0);
			state = 1;
		}
		else
		{
			GPIO_ResetBits(GPIOA,GPIO_Pin_0);
			state = 0;
		}
	}
	
	vTaskDelete(NULL); 
}

Это задача, которую мы будем выполнять. Тут реализовано, собственно, мигание диодом. В конце функции, за пределами цикла, принято вызывать функцию vTaskDelete(NULL), которая удалит задачу в случае выхода из бесконечного цикла.

Теперь осталось только реализовать функцию main():

int main()
{
	vFreeRTOSInitAll();
	xTaskCreate(vLedTask,(signed char*)"LedTask", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
	vTaskStartScheduler();
}

Сначала вызываем функцию для конфигурирования вывода PA0. А вот дальше интереснее – создаем задачу при помощи функции xTaskCreate(). Рассмотрим этот момент подробнее. Функция принимает аргументы:

  • pvTaskCode — указатель на функцию, реализующую задачу
  • pcName — нуль-терминальная (заканчивающаяся нулем) строка, определяющая имя функции
  • usStackDepth — глубина (размер) собственного стека создаваемой задачи, в данном случае мы используем минимально допустимый размер стека, определенный макросом configMINIMAL_STACK_SIZE
  • pvParameters— произвольный параметр, передаваемый задаче при ее создании
  • uxPriority — определяет приоритет создаваемой задачи, мы берем tskIDLE_PRIORITY + 1 = (приоритет задачи бездействие) + 1
  • pxCreatedTask — может использоваться для получения дескриптора (handle) создаваемой задачи, который помещается по адресу pxCreatedTask после успешного создания задачи.

Остается в функции main() вызвать планировщика, и все, готово!

Запускаем отладчик и смотрим, что из всего этого вышло. Открываем окошко логического анализатора и настраиваем его на мониторинг вывода РА0 (про это написано в предыдущих статьях по STM32, вот тут, например). Запускаем программу:

Работа программа с FreeRTOS.

Все работает четко по плану )

Подведем итог. Мы установили FreeRTOS, разобрались с ее работой и даже создали небольшую программку с применением ОСРВ. В следующей статье тоже будем писать под FreeRTOS, но это будет что-нибудь посложнее и поинтереснее. Также вот ссылка на еще один пример организации работы программы с операционной системой.

P. S. Вот, кстати, хорошее описание FreeRTOS на русском — ссылка.


Бытует мнение, что RTOS это некий хардкор для избранных. Что там все сложно, замудрено и новичкам туда соваться бестолку. Отчасти тут есть доля истины, такие системы крайне сложны в отладке, но и то лишь тогда, когда вы забиваете контроллер под завязку и работаете на пределе оперативной памяти и быстродействия. Тогда да, словить какой-нибудь dead lock или пробой стека можно на раз. И попробуй найти где это случилось в этой асинхронной системе. Но простые задачи на RTOS реализуются еще проще и с меньшим количеством мозга.
 

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

▌FreeRTOS?
Почему именно она? Она популярна, она Free и она портирована на огромное количество архитектур, под нее существуют плагины для Keil и IAR и всякие примочки для PC. При этом она довольно легкая и функциональная.
 

Я не буду вам сейчас тут расписывать все эти прототипы функций, порядок записи, технические тонкости и прочее. Это все есть в технической документации и в замечательном цикле статей Андрей Курница, что был в журнале Компоненты и Технологии в 2011 году. PDF статьи вы найдете в конце.
 

Я лишь на пальцах и псевдокоде быстро распишу те инструменты которыми владеет FreeRTOS, чтобы когда вы будете читать более подробную документацию за деревьями не потеряли лес :)
 

Ну и все сказанное тут справедливо и для большинства других RTOS. Т.к. механизмы в целом все одни и те же и никто ничего нового еще не придумал.

 

▌Системный тик
Один из таймеров микроконтроллера, самый ненужный, настраивают на генерацию системных тиков. Один тик делается, обычно, раз в 1мс, но можно и чаще или реже. В зависимости от того какая реакция и дискретность системы нам нужна.


 

Каждый тик это вызов прерывания таймера, в котором вызывается диспетчер, чьими усилиями проворачиваются шестеренки ОС. Это время еще называют квантом, говоря, что задаче выделяется квант времени.
 

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

Ее код будет выглядеть примерно так:
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void KeyScan(void)
{
	while(1)
	{
	 if (Button1 == Pressed) 
 		{
		do_action_1();
		}
 
 	if (Button2 == Pressed) 
 		{
		do_action_2();
		}
	}
}

Если в классическом стиле программирования бесконечные циклы были зло которое могло затупить всю программу, то в случае RTOS (она должна быть вытесняющей) это нормально. Так и должно быть. Можно даже быдлокодить и делать тупые задержки на циклах. Например так:
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
void LedBlink(void)
{
	while(1)
	{
	 LED_ON();		// Зажечь диод
 
	for (i=0;i<1000000;i++)	// Выдержка в 1000000 тактов.
		{
		_NOP();
		}
 
	 LED_OFF();		// Погасить диод
 
	for (i=0;i<1000000;i++)	// Выдержка в 1000000 тактов. 
		{
		_NOP();
		}
	}
}

Вот примерно так может выглядеть алгоритм этой программы с точки зрения программиста, не углубляясь в дебри ОС. Как видно, связей между задачами нет. Им и не нужно, они работают каждая сама по себе.

 

И все это будет работать. Диодик будет моргать, клавиатура сканироваться. Да, неоптимально, да тупо в лоб. Но работать будет. Как? За счет диспетчера. Который по прерыванию таймера будет прерывать на каждом тике каждую задачу и отдавать следующей. Т.е. задачи будут работать как бы сами по себе, но кусочками по очереди.
 


 

Сам же диспетчер озаботится тем, чтобы у каждой задачи все сохранялось и запоминалось: текущее положение, стек, переменные, регистры и с точки зрения задачи ничего не происходило. От самой задачи же требуется только соответствовать ряду правил, например, не лезть без спросу куда не просят. Т.к. не известно что там будет. А в целом она может быть весьма самодостаточной.
 

Изначально задача не инициализирована. Т.е. задачи как таковой нет, есть лишь некий кусок кода, который лежит во флеше в скомпиленном виде и мы знаем его адрес и знаем, что это наша задача. В этом случае она занимает только место на флеше. И все. Есть она, нет ее — не важно, остальная программа этого не заметит.
 

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

  • READY Задача запущена и готова принять на себя управление. Ждет только момента когда на нее обратит внимание диспетчер. Как только дойдет ее очередь так сразу же задача перейдет в режим RUN.
  • RUN Т.е. диспетчер переключил управление на нее, процессор прогоняет непосредственно ее код через себя в данный момент. В этот момент задача живет, потребляет процессорное время и делает полезную работу ради которой она была записана.
  • WAIT Задача в спячке. Т.к. ждет некого события, например, пока таймер натикает, или пока что-нибудь в системе не случится, на что эта задача должна среагировать. При этом диспетчер не переключается на нее, процессорное время не тратится. Как только ожидаемое событие произойдет, то RTOS назначит этой задаче состояние READY.
  • SUSPEND Выключено. Т.е. задача не выгружена из памяти, данные ее все сохранены, но она неактивна. Ни на какие события не реагирует и сама из этого состояния не выйдет. Вывести ее из этого состояния можно только API командой ОС, вручную.

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

У задачи есть такой важный параметр как приоритет. Он задается при создании и его можно на лету вручную менять через API функции RTOS . Приоритет определяет в каком порядке будут работать задачи.
 

Т.е. если есть две задачи в статусе Ready, но у одной приоритет выше другой. Задача с низким приоритетом в таком случае не получит управление до тех пор, пока высокоприоритетная задача не свалится в WAIT. Диспетчер всегда будет выбирать ту READY задачу у которой приоритет выше.
 

А если READY задач нет, то будет вращать IDLE цикл. В котором происходит обслуживание памяти, зачистка неиспользованной оперативки, удаление ошметков от удаленных задач и прочей служебной фигней. Ну и туда же (на IDLE) можно повесить свою callback функцию, в которой, например, контроллер будет отправляться в режим энергосбережения.
 

API функции управления задачами, кратко. Аргументы посмотрите в технической документации:
 

  • xTaskCreate — создает новую задачу, выделяя под нее память и натравливая на нее диспетчер.
  • vTaskDelete — удаляет задачу. Память потом освобождает IDLE задача.
  • vTaskDelay(N) — эта функция вызывает диспетчер, который переводит задачу в WAIT на N системных тиков. Можно на ней лепить всякие простые задержки, вроде опроса кнопок.
  • vTaskDelayUntil(N) — функция аналогичная предыдущей, но считает время N не от момента ее срабатывания, а от момента прошлого пробуждения задачи.
  • uxTaskPriorityGet — возвращает приоритет задачи. Т.е. можно посмотреть приоритет текущей или любой другой задачи заголовок (handle) которой мы знаем.
  • uxTaskPrioritySet — устанавливает приоритет задачи. Т.е. можно приоритет менять.
  • vTaskSuspend — глушит задачу, что она перестает отвечать на события. Перестает работать, но не выгружается из памяти, а зависает в текущем состоянии.
  • vTaskResume — возврат задачи из SUSPEND состояния. Эту функцию нельзя выполнять из обработчика прерывания.
  • vTaskResumeFromISR — аналогичная команда, но ее как раз можно выполнять из обработчика прерывания, но нельзя запускать вне него. Там еще есть ряд особенностей, о которых я расскажу ниже отдельно, когда буду описывать все *FromISR функции оптом.

Утилиты задач. Они не используются для управления задачами, но позволяют выудить из диспетчера некоторые сведения.
 

  • xTaskGetCurrentTaskHandle — узнать Handle текущей задачи. Зная заголовок можно можно менять ее приоритет, запускать, удалять и так далее.
  • xTaskGetTickCount — выдает количество тиков с момента запуска планировщика. Это этакий глобальный таймер, отсчитывающий время с начала времен.
  • xTaskGetSchedulerState — выдает состояние диспетчера. Запущен, работает, выключен и так далее.
  • uxTaskGetNumberOfTasks — показывает количество загруженных задач. Например если надо определить хватит ли памяти. Или для отладки.
  • vTaskList — отладочная утилита. В итоговую программу совать ее не следует. При запуске делает большой такой отчет в котором записывает досье на все запущенные задачи. Там и заголовки, имена, состояние, сколько есть памяти, какая глубина стека.
  • vTaskStartTrace — запуск отладочной трассировки. Данные тоже пишутся в большой такой лог. Который можно по UART скинуть на комп, расшифровать специальной утилиткой и посмотреть что происходило в недрах вашей программы.
  • ulTaskEndTrace — остановка трассировки.

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

▌Очереди. Обмен данными между задачами
Окей, задачи это такие программы сами в себе. Крутятся себе в бесконечном цикле. Но как между ними гонять данные? Ведь считанную кнопку надо кому-то передать, чтобы другая задача провернула с ней некий экшн.
 

Первое что приходит в голову это замутить глобальную переменную и совать туда все, а в другой задаче читать. Плохая идея. У нас же задачи асинхронные, они вращаются сами по себе. Диспетчер прерывает их как ему вздумается. А если вы, например, пишете четырех байтную переменную. Три байта записали… тут бац! Планировщик прибежал, управление отобрал и отдал задаче которая эту переменную должна считать. А она и прочитает и ей невдомек, что там всего три байта из 4 записаны, ей никто не обьяснил.
Это называется нарушение атомарности. Нет, конечно можно запрещать прерывания на все такие движухи. Или диспетчер останавливать. Но это же сплошь и рядом, постоянные запреты прерывания это плохо. Поэтому обмен данными между задачами идет тоже через планировщик.
 

Между задачами прокладываются очереди.
 


 

Одни задачи кладут данные в очередь, а другие оттуда читают.
 

А диспетчер следит за тем, чтобы из очереди нельзя было прочитать до того как там все нормально запишется. Также он следит за тем, чтобы в очередь нельзя было записать если она переполнена. В случае если очередь пуста/переполнена, то та задача которая хочет считать/записать в очередь сваливается в WAIT и диспетчер ее разбудит когда очередь будет готова отдать/принять данные.
 

Попутно будет и решен вопрос приоритетов. Ведь при пробуждении проснутся все те задачи, что ждут очереди и вперед пойдут более приоритетные. В очередь можно класть либо в начало, либо в конец.
 


 

Очередь перед использованием надо создать, при этом мы указываем количество и тип элементов в ней, под которые резервируется память. И данные туда копируются. А если она вдруг стала не нужна, то ее можно удалить, освободив память.
 

Очереди в нашем упрощенном примере в псевдокоде работать будут так (псевдокод):
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 
//Задача KeyScan опрашивает кнопки и шлет данные через очередь другой задаче.
// Если очередь будет переполнена, то на функции QueueSend задача упадет в Wait до 
// освобождения очереди. 
void KeyScan(void)
{
	while(1)
	{
	 if (Button1 == Pressed) 
 		{
		QueueSend(Button2Led,1);	// Шлем в очередь Button2Led номер кнопки "1".
		}
 
 	if (Button2 == Pressed) 
 		{
		QueueSend(Button2Led,2);	// Шлем в очередь Button2Led номер кнопки "2".
		}
 
	TaskDelay(10);			// Задержка средствами диспетчера. 
	}
}
 
 
// Задача LedBlink получает данные от задачи KeyScan
void LedBlink(void)
{
	while(1)
	{
	Button = QueueReceive(Button2Led);	// Тут задача упадет в Wait пока данные не появятся в очереди. 
 
	switch(Button)			// А как только проснется в Button будет загружены данные.
		{
		case 1: LED_1_ON(); 	// Зажигаем соответствующий светодиод. 
		case 2: LED_1_ON(); 
		}
	}
}

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

  • xQueueCreate — создает очередь. При этом выделяется память. Если памяти не хватит, то очередь создана не будет и будет возвращена ошибка.
  • vQueueDelete — удаляет очередь, освобождая память. Память зачищается в IDLE процессе.
  • xQueueReset — обнуление очереди.
  • uxQueueMessagesWaiting — показывает сколько у нас в очереди элементов.
  • xQueueSend/xQueueSendToBack — два имени для удобства. А так они одинаковые. Кладет данные в конец очереди. В прерывании использовать нельзя.
  • xQueueSendToFront — кладет данные в начало очереди. В прерывании использовать нельзя.
  • xQueueReceive — берет данные из очереди, освобождая ячейку. В прерывании использовать нельзя.
  • xQueuePeek — просто считывает данные из очереди, но ячейку не освобождает. Как наблюдатель. Может использоваться, например, для отладки. Чтобы слать содержимое очереди в UART, показывая последовательность данных. В прерывании использовать нельзя.
  • xQueueSendToBackFromISR/xQueueSendFromISR — то же самое, что и простая очередь, но для чтения из прерываний.
  • xQueueSendToFrontFromISR — аналогично, все то же самое, но для прерываний. Ну и есть разница. О них ниже.
  • xQueueReceiveFromISR — аналогично, версия для использования в обработчиках прерываний.

▌Семафоры
Иногда надо не передать данные между задачами, а просто дать отмашку. Мол тут у нас нажата кнопка, ты знаешь что делать, вперед! Для этого служат семафоры. С точки зрения внутреннего устройства семафор это та же самая очередь. Только содержимое ячейки никого не волнует, главное что она не пуста.
 

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

Т.е. если переделать нашу задачу с кнопками то можно показать это так:
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void KeyScan(void)
{
	while(1)
	{
	 if (Button1 == Pressed) 
 		{
		SemaphoreGive(RunTask1);	// Машем семафором RunTask1  задаче 1
		}
 
 	if (Button2 == Pressed) 
 		{
		SemaphoreGive(RunTask2);	// Машем семафором RunTask2  задаче 2
		}
 
	TaskDelay(10);			// Задержка средствами диспетчера. 
	}
}
 
// Задача 1
void Task1(void)
{
	while(1)
	{
	SemaphoreTake(RunTask1);		// Программа тут свалится в WAIT до тех пор пока не появится семафор
	Action1();			// А как семафор появится, отомрет и выполнит Action1
	}	
}
 
// Задача 2
void Task2(void)
{
	while(1)
	{
	SemaphoreTake(RunTask2);		// Программа тут свалится в WAIT до тех пор пока не появится семафор
	Action2();			// А как семафор появится, отомрет и выполнит Action2
	}	
}

Понятно, что у такого подхода есть один недостаток — непонятно сколько раз поставили семафор. Если мы его сразу обработать не успели, то можем прозевать одно из событий. Скажем было два нажатия кнопки, а мы обработали как одно.

Для решения этой проблемы есть счетный семафор. Разница лишь в том, что у счетного семафора состояние это не единичный флажок, а некая переменная. Задача которая его выдает эту переменную увеличивает, а которая считывает — уменьшает. Досчитает задача до нуля тогда и заснет в WAIT.
 

▌Mutex
Mutual exclusion — система взаимного исключения. Механизм обеспечивающий уникальный доступ многих задач к единственному ресурсу.
 

Простой пример:
Сортир это уникальный ресурс. Если два человека вломятся в одноместный сортир случится конфуз. Ключ от сортира, лежащий у вахтерши, это мутекс. Прибежал, взял ключ, пошел в сортир. Пока ключ у тебя никто сортиром воспользоваться не сможет. Сходил — верни ключ на место. Кто ключ взял тот его и возвращает вахтеру. Нет, разумеется никто не запретит прийти и выбить дверь ногой, но это же неприлично, а RTOS только для приличных людей. Быдло его портит, случаются падения и глюки. Так что двигай к вахтеру и жди ключ. Ну и, разумеется, зажимать ключ дольше чем это реально требуется большое свинство.
 

Второй пример:
Есть две задачи. Одна шлет в UART «ABCDEF» Другая шлет «123456». Без разделения их может на выходе получиться шняга вида A1BCD123E45F6, что совсем не похоже на нужный нам результат.
 


 

Мутексом можно защитить что угодно. Интерфейс, область памяти, переменную. Не важно. Причем защита это декларативная. Т.е. нет никаких скрытых программных или аппаратных блоков на обращение к этому ресурсу. Мы сами должны все обращение к таким ресурсам делать исключительно через опрос и захват мутекса.
 

Если доступ к UART, например, защищен мутексом, то никакой мутекс не запретит писать напрямую в порты периферии и тем самым устроить конфликт.
 

Но мы же себе не враги, поэтому если что-то защищается через мутекс, то все задачи должны перед обращением к ресурсу проверять этот флажок и пользоваться только если удалось мутекс захватить.
 

Если мутекс захватить не удается, то задача падает в WAIT и диспетчер разбудит ее когда флажок будет доступен к взятию. Тут правда надо учитывать, что есть и задачи с другим приоритетом, которые могут сами претендовать на этот флажок.
 

Мутекс бывает и рекурсивным. Т.е. когда одна задача может его взять дважды, например в результате вложенной фукнции. В этом случае для освобождения его она должна его столько же раз вернуть обратно.
 

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

В сложных схемах с кучей зависимостей это может привести к цепной самоблокировке системы. В результате круговой зависимости. Подробней о таких вилах лучше читать у Таннебаума. Там много разных примеров наглядных.
 

За семафоры и мутексы отвечают следующие API фукнции:
 

  • xSemaphoreCreateBinary — создать простой бинарный семафор.
  • xSemaphoreCreateCounting — создать счетный семафор
  • xSemaphoreCreateMutex — создать мутекс
  • xSemaphoreCreateRecursiveMutex — создать рекурсивный мутекс
  • vSemaphoreDelete — удалить семафор/мутекс.
  • xSemaphoreGetMutexHolder — узнать какая сволочь держит мутекс.
  • xSemaphoreTake — Взять семафор/мутекс
  • xSemaphoreTakeFromISR — взять семафор/мутекс из прерывания
  • xSemaphoreTakeRecursive — взять семафор/мутекс рекурсивный.
  • xSemaphoreGive — выдать семафор/мутекс
  • xSemaphoreGiveRecursive — выдать рекурсивный мутекс
  • xSemaphoreGiveFromISR — выдать семафор/мутекс из прерывания.

▌Системный таймер
Это программный таймер. Он позволяет запустить какую либо функцию по таймеру. Но, в отличии от задачи, она не вертится в бесконечном цикле, а запускается один раз или периодически. Но отработала и вышла. Функция вызываемая таймером не должна быть зацикленной. Дискретность времени таймера — системный тик. Таймер после отработки выгружается из памяти. Его не нужно принудительно удалять, только если он не периодический.
 

Вот основные таймерные команды
 

  • xTimerCreate — создать таймер. В этой API функции мы связываем таймер с указателем на его функцию.
  • xTimerDelete — удалить таймер
  • xTimerStart — запустить таймер
  • xTimerStop — остановить таймер
  • xTimerChangePeriod — изменить период временной выдержки
  • xTimerReset — сбросить таймер

Ну и те же функции, но отдельно для прерываний:
 

  • xTimerResetFromISR
  • xTimerChangePeriodFromISR
  • xTimerStartFromISR
  • xTimerStopFromISR

▌Диспетчер и Многозадачность
Над всеми задачами, очередями, мутексами и семафорами темным властелином стоит диспетчер. Он в тени, мы его можем только запустить или остановить. Он висит обычно на каком-нибудь таймере, в ARM Cortex-M3 его вешают на SysTick таймер. Таймер генерирует системные тики, скажем раз в 1мс. И каждый такой тик вызывается диспетчер который тасует задачи и обслуживает все функции ядра. А пока не будет вызван диспетчер ничего в системе не меняется. Диспетчер это суть и душа всей RTOS.
 

Задача не обязательно прерывается диспетчером по тику таймера. Она может и самостоятельно передать управление. Например, на время ожидания. Т.е. если наш тупой код с задержками на for (i=0;i<1000000;i++) переписать. Заменив задержку тупым for циклом, на задержку средствами OS. Такую как
 

То задача не будет уже впустую крутить миллион итераций цикла, а данной API функцией попросит диспетчера разбудить ее черезе 1000 системных тиков и свалится в WAIT, вызвав диспетчер, который передаст управление другой READY задаче (если такая есть). А через 1000 тиков диспетчер переведет ее снова в READY и продолжит со следующей строчки.
 

Многозадачность в FreeRTOS бывает разная. Выше я описывал вытесняющую многозадачность. Это когда диспетчер каждый системный тик, никого не спрашивая, останавливает задачу, и передает управление другой. Обеспечивая тем самым псевдопараллельное выполнение.
 

Но в зависимости от конфигурации ядра FreeRTOS многозадачность может быть и кооперативной.
 

Если обратиться к диаграмме состояний задачи, то в кооперативной многозадачности нет вот этой красной связи. Т.е. диспетчер не может отобрать у задачи управление.

 

Т.е. в этом случае диспетчер уже не вызывается каждый системный тик. А вызывается ТОЛЬКО вручную программистом. Т.е. теми сами командам переводящими систему в WAIT. Это черевато тем, что высокоприоритетная задача не может перебить низкоприоритетную до тех пор, пока та сама не отдаст управление. Что накладывает ограничения на стиль написания. Нужно будет стараться не тупить и как можно быстрей отдавать управление диспетчеру. Тупой наглый цикл тут не прокатит, зато грамотное использование кооперативной многозадачности позволяет экономить память. Т.к. OS переключается в конкретные, заранее известные, моменты, а значит сохранять надо несколько меньше данных.
 

API ядра

  • taskYIELD — отдать управление диспетчеру принудительно, не дожидаясь тика.
  • taskENTER_CRITICAL — начало критической секции. В критической секции не вызывается диспетчер. Это как бы запрет прерываний, но только для диспетчера.
  • taskEXIT_CRITICAL — конец критической секции. Сами критические секции могут быть вложенными. Т.е. насколько мы в нее углубились столько же раз надо из нее выйти.
  • taskDISABLE_INTERRUPTS — запрет прерываний.
  • taskENABLE_INTERRUPTS — разрешение прерываний. Тоже может быть вложенным.
  • vTaskStartScheduler — запуск диспетчера. Собственно с этой команды все и начинает вертеться.
  • vTaskEndScheduler — остановка диспетчера.
  • vTaskSuspendAll — заглушить все задачи
  • xTaskResumeAll — восстановить все заглушенные задачи

▌Таймауты, обработка ошибок, и что же не так с прерываниями?
Почти все API функции что нибудь да возвращают. Обычно они возвращают PASS или ERROR на предполагаемое действие. Т.е. считав после функции значение мы можем понять выполнилась ли команда или нет. Ошибка может быть по разным причинам, например, памяти не хватило для выделения чего бы то ни было.
 

Еще у тех функций, что ждут чего либо. Ну там семафоры, очереди, мутексы есть такой параметр как таймаут. Т.е. сколько мы будем ждать наше событие. А вдруг оно вообще никогда не придет, ну клавиатуру оторвали вандалы. Или провод оборвался. Что нам теперь, насмерть зависать чтоль? А кто ментов вызовет? Кто создателю пожалуется?
 

Вот для этого есть таймаут. Если его задать, то по истечении его задача проснется и получив от функции ошибку будет решать что с ней делать дальше. Выглядит это, например, так:
 

1
2
3
4
5
6
7
8
9
10
11
// Получить сообщение из очереди Queue.  Ждем сообщение не более 10 тиков
if(xQueueReceive(Queue,&(pxRxedMessage),( TickType_t)10 ) )
	{
	// pcRxedMessage теперь указывает на полученное сообщение
	// Обрабатываем его. 
	}
else
	{
	// Error! сообщение не пришло за 10 тиков
	// Решаем эту проблему. 
	}

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

Поэтому существуют специальный функции которые [****]FromISR. Таймаута там нет, там просто есть ответ считано или нет значение. А еще есть один важный параметр pxHigherPriorityTaskWoken.
 

Этот параметр нам говорит есть ли у нас READY задача более высокого приоритета, чем та которая была прервана текущим прерыванием.
 

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

Но вот если такая задача есть… То мы можем ничего не делать. И тогда прерывание отдаст управление прерванной задаче, та дотикает до конца кванта времени (системного тика), а дальше диспетчер сам решит кому там дать управление.
 


 

Либо мы можем прям из обработчика прерывания вызвать диспетчер командой taskYIELD и он сразу же выполнит высокоприоритетную задачу, а потом вернет управление той, которую мы прервали.
 


 

Это позволит обработать быстрей важные вещи.
 

Пример такого подхода:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Обработчик прерывания */
static void __interrupt __far vExampleInterruptHandler( void )
{
static unsigned long ulReceivedNumber;
static const char *pcStrings[] = {“String 0,“String 1,“String 2,“String 3};
 
/* Создадим переменную куда будет положен результат API-функции xQueueReceiveFromISR(), 
он станет pdTRUE, если операция с очередью разблокирует более высокоприоритетную задачу.*/
static portBASE_TYPE xHigherPriorityTaskWoken;
 
/*Перед вызовом xQueueReceiveFromISR() должен принудительно устанавливаться в pdFALSE */
xHigherPriorityTaskWoken = pdFALSE;
 
/* Считывать из очереди числа, пока та не станет пустой. */
while( xQueueReceiveFromISR( xIntegerQueue,&ulReceivedNumber,&xHigherPriorityTaskWoken ) != errQUEUE_EMPTY )
	{
	ulReceivedNumber &= 0x03;
	xQueueSendToBackFromISR( xStringQueue,&pcStrings[ ulReceivedNumber ],&xHigherPriorityTaskWoken );
	}
 
/* Проверить, не разблокировалась ли более высокоприоритетная задача при записи в очередь. 
Если да, то выполнить принудительное переключение контекста. */
if( xHigherPriorityTaskWoken == pdTRUE ) taskYIELD();
}

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

Суть в чем. Есть у нас прерывание диспетчера. И в этом прерывании он что-то делает со своими структурами (очередями, задачами и прочей системной фигней). Но диспетчер это не самая важная вещь на свете, поэтому его часто вешают на самое зачуханное прерывание с самым низким приоритетом. И его все могут перебивать.

 
Но есть нюанс. Допустим диспетчер что-то делает с очередью, смотрит кого там можно разблокировать по ее состоянию, пишет туда что-то свое, а тут опа прерывание и в нем некая [****]FromISR записала в ту самую очередь. Что у нас будет? У нас будет лажа. Т.к. на выходе из прерывания диспетчер похерит все что предыдущее прерывание туда писало — он то запись до конца не провел. Т.е. налицо классическое нарушение атомарности.
Чтобы этого не было, диспетчер на время критических записей запрещает прерывания. Чтобы ему никто не мог помешать делать свое дело. Но запрещает он не все прерывания, а только определенную группу. Скажем все прерывания с приоритетом (меньше число, старше приоритет) 15 по 11, а 1 по 10 нет. В результате на 1 по 10 мы можем вешать что то ну очень сильно важное и никакой диспетчер это не перебьет. Но пользоваться API RTOS в этих (1-10) прерываниях ни в коем случае уже нельзя — они могут диспетчер скукожить. Для настройки этих групп есть конфиг специальный. Для STM32 он выглядит так:

1
2
3
4
5
6
/* This is the raw value as per the Cortex-M3 NVIC.  Values can be 255 (lowest) to 0 (1?) (highest). */
 
#define configKERNEL_INTERRUPT_PRIORITY              255
#define configMAX_SYSCALL_INTERRUPT_PRIORITY     191 /* equivalent to 0xb0, or priority 11. */
 
  /* This is the value being used as per the ST library which permits 16 priority values, 0 to 15.  This must correspond to the configKERNEL_INTERRUPT_PRIORITY setting.  Here 15 corresponds to the lowest NVIC value of 255. */

Мы указываем приоритет ядра KERNEL = 255. И задаем планку максимального приоритета для прерываний которые имеют право юзать [****]FromISR API функции = 191. В статье о NVIC я писал, что у STM32 в байте с приоритетом играет роль только старшая тетрада, т.е. у нас есть 16 уровней приоритетов от старшего к младшему: 0х00, 0х10, 0х20, 0х30…0xF0
 

Т.е. 255 это уровень 0xF0 — самый младший, а 191 уровень 0xB0 и, таким образом, все прерывания в которых мы можем использовать API фукнции должны быть сконфигурированы с приоритетом от 0xF0 до 0xB0, не старше. Иначе будет трудноловимвый глюк. Прерывания же не использующие API могут быть с каким угодно приоритетом от самого низкого до самого старшего.
 

▌Сопрограммы aka Co-Routines
Но даже этих механизмов разработчикам FreeRTOS было мало и они добавили в нее сопрограммы. Это, по сути дела, еще одна маленькая виртуальная RTOS с кооперативной многозадачностью, работающая внутри системы на правах обычной задачи.
 

Как виртуальная машина на компе. Т.е. с точки зрения текущей операционки это обычная программа. А внутри нее свои программы гоняют :) Тут похоже, только в миниатюре. Сопрограммы вращаются в одном адресном пространстве, не имеют раздельных стеков и данных. У них свой собственный сопрограммный диспетчер, они имеют свой собственный простенький API с полудесятком самых необходимых вещей. Вроде очередей и задержек.
 

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

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

  • Официальная документация FreeRTOS
  • Цикл статей по FreeRTOS с более подробным описанием большинства API функций

Следующей статьей я дам демопрограммку в которой распишу создание программы на базе 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 будет одинаков в обоих случаях.

Далее хуки.

Proudly powered by Pelican, which takes great advantage of Python.

The theme is by Smashing Magazine, thanks!

Понравилась статья? Поделить с друзьями:
  • Как снять проданный авто с учета через госуслуги инструкция
  • Руководство гибдд по волгоградской области официальный сайт
  • Руководство по ремонту инжекторов автомобилей
  • Нафтифин инструкция по применению капли ушные капли
  • Паллет ультра осветлитель инструкция по применению