FreeRTOS: практическое применение, часть 1 (управление задачами) Печать
Добавил(а) microsin   

[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.х на английском языке.