FreeRTOS. Операционная система реального времени для микроконтроллеров. Часть 8. Сопрограммы

Введение

Автор: Андрей Курниц (kurnits@stim.by). Журнал КиТ
Автор этой статьи продолжает знакомить читателя с созданием программ, работающих под управлением FreeRTOS — операционной системы для микроконтроллеров. На этот раз речь пойдет об альтернативном способе реализации многозадачной среды, когда в программе вместо задач ис­пользуются сопрограммы. Мы оценим достоинства и недостатки использования сопрограмм.

Что представляет собой сопрограмма?

В предыдущих публикациях  мы говори­ли о FreeRTOS как о многозадачной операци­онной системе, и в центре нашего внимания находилась задача (task) — базовая единица программы, работающей под управлением FreeRTOS. Речь шла о том, что программа, работающая под управлением FreeRTOS, разбивается на совокупность задач. Задача представляет собой отдельный поток команд процессора и реализуется в виде функции языка Си. Каждая задача отвечает за неболь­шую часть функциональности всей програм­мы. Каждая задача выполняется независимо от остальных, но взаимодействует с осталь­ными задачами через механизмы межзадач­ного взаимодействия.

Начиная с версии V4.0.0 во FreeRTOS по­явилась поддержка сопрограмм (coroutines). Сопрограмма сходна с задачей, она также пред­ставляет собой независимый поток команд процессора, и ее можно использовать как базо­вую единицу программы. То есть программа, работающая под управлением FreeRTOS, мо­жет состоять из совокупности сопрограмм.

Когда следует использовать сопрограммы?

Главное преимущество сопрограмм перед задачами — это то, что использование со­программ позволяет достичь значительной экономии оперативной памяти по сравне­нию с использованием задач.

Каждой задаче для корректной работы ядро выделяет участок памяти, в которой размещаются стек задачи и структура управ­ления задачей (Task Control Block). Размер этого участка памяти за счет размещения в нем стека оказывается значительным. Так как объем оперативной памяти в микрокон­троллерах ограничен, то его может оказать­ся недостаточно для размещения всех задач.

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

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

Особенности сопрограмм:

  1. Использование стека. Все сопрограммы в программе используют один и тот же стек, это позволяет добиться значительной экономии оперативной памяти по сравне­нию с использованием задач, но налагает ряд ограничений при программировании сопрограмм.
  2. Планирование и приоритеты. Сопрограммы в отличие от задач выполняются в режиме кооперативной многозадачности с приори­тетами. Кооперативная многозадачность в отношении сопрограмм автоматически устраняет проблему реентерабельности функций, но негативно сказывается на вре­мени реакции микроконтроллерной систе­мы на внешние события.
  3. Сочетание с задачами. Сопрограммы мо­гут выполняться одновременно с задачами, которые обслуживаются планировщиком с вытесняющей многозадачностью. При этом задачи выполняются в первую очередь, и только если нет готовых к выполнению за­дач, процессор занят выполнением сопро­грамм. Важно, что во FreeRTOS не существу­ет встроенного механизма взаимодействия между задачами и сопрограммами.
  4. Примитивность. По сравнению с задача­ми сопрограммы не допускают целый ряд операций.

-   Операции с семафорами и мыотексами не представлены для сопрограмм.

-   Набор операций с очередями ограничен по сравнению с набором операций для задач.

-    Сопрограмму после создания нельзя уни­чтожить или изменить ее приоритет.

5. Ограничения в использовании:

-    Внутри сопрограмм нельзя использовать локальные переменные.

-    Существуют строгие требования к ме­сту вызова API-функций внутри сопро­грамм.

Экономия оперативной памяти при использовании сопрограмм

Оценим объем оперативной памяти, ко­торый можно сэкономить, применяя сопро­граммы вместо задач.

Пусть в качестве платформы выбран ми­кроконтроллер семейства AVR. Настройки ядра FreeRTOS идентичны настройкам де­монстрационного проекта, который входит в дистрибутив FreeRTOS. Рассмотрим два случая. В первом случае вся функциональ­ность программы реализована десятью зада­чами, во втором —десятью сопрограммами.

Оперативная память, потребляемая одной задачей, складывается из памяти стека и па­мяти, занимаемой блоком управления зада­чей. Для условий, приведенных выше, раз­мер блока управления задачей составляет 33 байт, а рекомендованный минимальный размер стека — 85 байт, Таким образом, име­ем 33+85 =118 байт на каждую задачу. Для создания 10 задач потребуется 1180 байт.

Оперативная память, потребляемая одной сопрограммой, складывается только из памя­ти, занимаемой блоком управления сопро­граммой. Размер блока управления сопро­граммой для данных условий равен 26 байт. Как упоминалось выше, стек для всех сопро­грамм общий, примем его равным рекомендо­ванному, то есть 85 байт. Для создания 10 со­программ потребуется 10x26+85 = 345 байт.

Таким образом, используя сопрограммы, удалось достичь экономии оперативной па­мяти 1180-345 = 835 байт, что составляет приблизительно 71%.

Состояния сопрограммы

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

    1. Выполнение (Running). Говорят, что сопро­грамма выполняется, когда в данный момент времени процессор занят непосредственно ее выполнением. В любой момент вре­мени только одна сопрограмма в системе мо­жет находиться в состоянии выполнения.
    2. Готовность к выполнению (Ready). Говорят, что сопрограмма готова к выполнению, если она не блокирована, однако в данный момент процессор занят выполнением другой сопрограммы или какой-то задачи. Сопрограмма может находиться в состоя­нии готовности к выполнению по одной из следующих причин:

-    Другая сопрограмма в данный момент находится в состоянии выполнения.
-    Одна из задач находится в состоянии выполнения, если в программе одно­временно используются и сопрограммы, и задачи.

3.   Блокированное состояние (Blocked). Сопрограмма блокирована, когда ожида­ет наступления некоторого события. Как и в случае с задачами, событие может быть связано с отсчетом заданного временного интервала — временное событие, а мо­жет быть связано с ожиданием внешнего по отношению к сопрограмме события. Например, если сопрограмма вызовет API-функцию crDELAY(), то она перейдет в блокированное состояние и пробудет в нем на протяжении заданного интервала времени. Блокированные сопрограммы не получают процессорного времени. Графически состояния сопрограммы и пе­реходы между ними представлены на рис. 1. В отличие от задач у сопрограмм нет при­остановленного (suspended) состояния, од­нако оно может быть добавлено в будущих версиях FreeRTOS.

Выполнение сопрограмм и их приоритеты

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

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

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

Сопрограммы выполняются в режиме ко­оперативной многозадачности. Это означает, что одна сопрограмма сменяет другую лишь тогда, когда выполняющаяся в данный мо­мент сопрограмма сама передает управление другой сопрограмме посредством вызова API-функции. Причем если в момент переда­чи управления в состоянии готовности к вы­полнению находятся несколько сопрограмм, то управление получит самая высокоприори­тетная среди них.

Итак, сопрограмма прерывает свое выпол­нение только при выполнении одного из сле­дующих условий:

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

Для корректного выполнения со­программ необходимо о р га. низ о - вать в программе периодический вы­зов API-функции vCoRoutineSchedule(). Рекомендованное место для вызова API- функции vCoRoutineSchedule() — тело за­дачи Бездействие, подробнее об этом бу­дет написано ниже. После первого вызова vCoRoutineSchedule() управление получает сопрограмма с наивысшим приоритетом.

Приоритет сопрограммы задается це­лым числом, которое может прини­мать значения от 0 до (configMAX_CO_ ROUTINE_PRIORITIES— 1). Большее  значение соответствует более высокому приоритету. Макроопределение configMAX_ CO_ROUTINE_PRIORITIES задает общее число приоритетов сопрограмм в програм­ме и определено в конфигурационном фай­ле FreeRTOSConfig.h. Изменяя значение configMAX_CO_ROUTINE_PRIORITIES, можно определить любое число возможных приоритетов сопрограмм, однако следует стремиться уменьшить число приоритетов до минимально достаточного для экономии оперативной памяти, потребляемой ядром.

Реализация сопрограммы

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

void vACoRoutineFunctionfxCoRoutineHandle xHandle, unsigned portBASE_TYPE uxIndex)
 
{
 
crSTART ( xHandle );
 
for(;;)
 
{
 
/* Код, реализующий функциональность сопрограммы, размещается здесь.*/
 
{
 
crEND();
 
}

Аргументы функции, реализующей сопро­грамму:

  1. xHandle— дескриптор сопрограммы. Автоматически передается в функцию, ре­ализующую сопрограмму, и в дальнейшем используется при вызове API-функций для работы с сопрограммами.
  2. uxlndex — произвольный целочисленный параметр, который передается в сопро­грамму при ее создании.

Указатель на функцию, реализующую со­программу, определен в виде макроопределе­нияcrCOROUTINE_CODE.

К функциям, реализующим сопрограммы, предъявляются следующие требования:

  1. Функция должна начинаться с вызова API- функции crSTART().
  2. Функция должна завершаться вызовом API-функции crEND().
  3.  Как и в случае с задачей, функция никогда не должна заканчивать свое выполнение, весь полезный код сопрограммы должен быть заключен внутри бесконечного цикла.
  4. Сопрограммы выполняются в режиме ко­оперативной многозадачности. Поэтому если в программе используется несколько сопрограмм, то для того, чтобы процессор­ное время получали все сопрограммы в про­грамме, бесконечный цикл должен содержать вызовы блокирующих API-функций.

Создание сопрограммы

Для создания сопрограммы следует до за­пуска планировщика вызвать API-функ­цию xCoRoutineCreate(), прототип которой приведен ниже:

portBASE_TYPE crCORoutineCreate(crCOROUTINE_CODE pxCoRoutineCode,

                                                                            unsigned portBASE_TYPE uxPriority,

                                                                            unsigned portBASE_TYPE uxlndex );

Аргументы и возвращаемое значение:

1.pxCoRoutineCode — указатель на функ­цию, реализующую сопрограмму (факти­чески — идентификатор функции в про­грамме).

  1. uxPriority— приоритет создаваемой со­программы. Если задано значение боль­ше, чем (соnfigMAX_CO_ROUTINE_ PRIORITIES— 1), то сопрограмма получит приоритет? равный (configMAX_ СО ROUTINE_PRIORITIES - 1).

3. uxlndex — целочисленный параметр, который передается сопрограмме при ее создании. Позволяет создавать несколько экземпляров одной сопрограммы.

4. Возвращаемое значение. Равно pdPASS, если сопрограмма успешно создана и до­бавлена к списку готовых к выполне­нию, в противном случае — код ошибки, определенный в файле ProjDefs.h (обычно errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY().

API-функция vCoRoutineSchedule()

Выполнение сопрограмм должно быть организовано при помощи циклического вызова API-функции vCoRoutineSchedule(). Ее прототип:

void vGoRoutineSchedule ( void );

Вызов vCoRoutitieSchedule()  рекомендуется располагать в задаче Бездействие:

void vApplicationldleHook( void)
 
{
 
vCoRoutineSchedule( void);
 
}

Если задача Бездействие не выполняет ни­каких других функций, то более эффектив­ной будет следующая ее реализация:

void vApplicationIdleHook(void)
 
{
 
for(;;) {
         vCoRoutineSchedule( void );
        }
}

Даже если в программе не используется ни одной задачи, задача Бездействие автома­тически создается при запуске планировщика.

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

В принципе вызов API-функции vCoRoutineSchedule() возможен в любой задаче, а не только в задаче Бездействие. Обязательным требованием является то, чтобы задача, из которой вызывает­ся vCoRoutineSchedule(), имела самый низкий приоритет. Иначе если существуют задачи с более низким приоритетом, то они не будут получать процессорное время.

Важно, что стек, общий для всех сопро­грамм, является стеком той задачи, которая вы­зывает API-функцию vCoRoutineSchedule(). Если вызов vCoRoutineSchedule() располага­ется в теле задачи Бездействие, то все сопро­граммы используют стек задачи Бездействие. Размер стека задачи Бездействие задается ма­кроопределением configMINIMAL_STACK SIZE в файле FreeRTOSConfig.h.

Настройки FreeRTOS для использования сопрограмм

Для того чтобы организовать многозадач­ную среду на основе сопрограмм, прежде все­го необходимо соответствующим образом настроить ядро FreeRTOS:

#indude "croutine.h"

  1. В исходный текст программы должен быть включен заголовочный файл croutine.h, со­держащий определения API-функций для работы с сопрограммами:
  2. Конфигурационный файл FreeRTOSConfig.h должен содержать следующие макроопреде­ления, установленные в cconfigUSE_IDLE_HOOK a configUSE_CO_ROUTINES.
  3. Следует также определить количе­ство приоритетов сопрограмм. Файл

FreeRTOSConfig.h должен содержать ма­кроопределение вида:

#define configMAX_CO_ROUTINE_RIORITIES ()

Учебная программа № 1

Рассмотрим учебную программу № 1, в ко­торой создаются 2 сопрограммы и реализова­но их совместное выполнение. Каждая сопро­грамма сигнализирует о своем выполнении, после чего реализуется временная задержка с помощью пустого цикла, далее происходит принудительное переключение на другую со­программу. Приоритет сопрограмм установ­лен одинаковым.

#inlude "FreeRTOS.h"
#include "task.h"
#include "croutine.h"
#include
#indude
/* Функция, реализующая Сопрограмму 1. Параметр, передаваемый в сопрограмму при ее создании,
 не используется. Сопрограмма сигнализирует о своем выполнении, после чего блокируется на 500 мс. */
void vCoRoutinel (xCoRoutineHandle xHandie, unsigned portBASE_ TYPE uxlndex) {
/* Все переменные должны быть объявлены как static.*/
static long i;
/* Сопрограмма должна начинаться с вызова crSTART().
Дескриптор сопрограммы xHandie получен автоматически в виде аргумента функции,
реализующей сопрограмму vCoRoutine1().*/
crSTART (xHandle);
/* Сопрограмма должна содержать бесконечный цикл. */
 for(;;)
  {
   /* Сигнализировать о выполнении */
   puts("Co-routine #1 runs!");
   /* Пауза, реализованная с помощью пустого цикла */
   for (i = 0; i < 5000000; i++);
   /* Выполнить принудительное переключение на другую со¬программу */
   crDELAY( xHandle, 0 );
  }
/* Сопрограмма должна завершаться вызовом crEND().*/
crEND();
}
/* Функция, реализующая Сопрограмму 2. Сопрограмма 2 выполняет те же действия, что и Сопрограмма 1*/
void vCoRoutine2( xCoRoutineHandie xHandie, unsigned portBASE_ TYPE uxlndex) {
static long i;
crSTART (xHandle);
for(;;) {
        /* Сигнализировать о выполнении */
        puts("Co-routine #2 runs!");
        /* Пауза, реализованная с помощью пустого цикла */
        for (i = 0; i < 5000000; i++);
/* Выполнить принудительное переключение на другую со¬программу */
crDELAY( xHandle, 0);
}
crEND();
}
/* Точка входа. С функции main() начинается выполнение про¬граммы. */
void main(void) {
/* До запуска планировщика создать Сопрограмму 1 и Сопрограмму 2. Приоритеты сопрограмм одинаковы и равны . Параметр, передаваемый при создании, не используется и равен 0. */
xCoRoutineCreate(vCoRoutine1, 1, 0);
xCoRоutineCreate(vCoRoutine2, 1, 0);
/ В программе не создается ни одной задачи. Однако задачи можно добавить, создавая их до запуска планировщика */
/* Запуск планировщика. Сопрограммы начнут выполняться. */
vTaskStartScheduler();
}
/* Функция, реализующая задачу Бездействие, должна присутствовать в программе и содержать вызов vCoRoutineSchedule() */
void vApplicationldleHook(void) { /* Так как задача Бездействие не выполняет других действии, то вызов vCoRoutineSchedule() размещен внутри бесконечного цикла.*/
 for(;;) {
         vCoRoutineSchedule();
         }
}

Результаты работы учебной программы № 1 приведены на рис. 2. На рис. 2 видно, что сообщения на дисплей выводят обе сопро­граммы, следовательно, каждая из них по­лучает процессорное время. На рис. 3 пред­ставлено разделение процессорного вре­мени между сопрограммами.

Сопрограммы выполняются в режиме кооперативной многозадачности, поэтому текутщая сопрограмма выполняется до тех пор, пока не произойдет явное переключе­ние на другую сопрограмму. На протяжении времени 0...tl будет выполняться только Сопрограмма 1, а именно будет выполняться продолжительный по времени пустой цикл (рис. 3). Как только пустой цикл Сопрограммы 1 будет завершен, в момент времени tl прои­зойдет явное переключение на другую сопро­грамму. В результате чего управление получит Сопрограмма 2 на такой же продолжительный промежуток времени — t1..t2.

Следует обратить внимание на обязатель­ный вызов API-функции crDELAY(xHandle, 0), благодаря которому происходит принуди­тельное переключение на другую сопрограм­му и, таким образом, реализуется принцип кооперативной многозадачности.

Продемонстрировать важность «ручного» переключения на другую сопрограмму можно, если исключить из функции Сопрограммы 1 вызов API - функции crDELAY(). В таком слу­чае результаты работы программы (рис. 4) будут свидетельствовать о том, что процес­сорное время получает только Сопрограмма 1. Причиной этому является тот факт, что Сопрограмма 1 не выполняет принудительно­го переключения на другую сопрограмму, что является необходимым условием корректной работы кооперативной многозадачной среды.

Ограничения при использовании сопрограмм

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

Использование локальных переменных

Особенность сопрограмм в том, что когда сопрограмма переходит в блокированное со­стояние, стек сопрограммы не сохраняется. То есть если переменная находилась в стеке в момент, когда сопрограмма перешла в бло­кированное состояние, то по выходу из него значение переменной, вероятно, будет дру­гим. Эта особенность объясняется тем фак­том, что все сопрограммы в программе ис­пользуют один и тот же стек.

Чтобы избежать потери значения пере­менных, не следует размещать их в Стеке, то есть нельзя использовать локальные пере­менные в сопрограммах. Все переменные, используемые в сопрограмме, должны быть глобальными либо объявлены статическими (ключевое слово static). Рассмотрим пример функции, реализующей сопрограмму:

// Глобальная переменная:
unsigned int uGlobalVar
// Функция, реализующая сопрограмму
void vACoRoutineFunction( xCoRoutineHandle xHandle. unsigned portBASH_TYPE uxlndex)
{
// Статическая переменная:
static unsigned int uStaticVar;
// Локальная переменная — В СТЕКЕ!!!
unsigned int uLocalVar = 10L;
crSTART(xHandle);
for(;;) {
uGlobalVar = 1;
uStaticVar = 10;
uLocalVar = 100;
// Вызов блокирующей API-функции crDELAY(xHandle, 10);
// После вызова блокирующей API-функции // значение глобальной и статической переменной
// uGlobalVar и uStaticVar гарантированно сохранится.
// Значение же локальной переменной uLocalVar И может оказаться не равным 100!!!
}
crEND();
)

Вызов блокирующих API-функций

Еще одним последствием использования общего для всех сопрограмм стека является то, что вызов блокирующих API-функций допускается только непосредственно из тела сопрограммы, но не допускается из функций, которые вызываются из тела сопрограммы. Рассмотрим пример:

// Функция, реализующая сопрограмму
void vACoRoutineFunction(xCoRoutineHandle xHandle, unsigned portBASE_TYPE uxlndex)
{
crSTART( xHandle);
for( ;; )
{
// Непосредственно в сопрограмме II блокирующие API-функции вызывать можно.
crDELAY(xHandle, 10 );
// Однако внутри функции vACalledFunction{) их НЕЛЬЗЯ вызывать!!!
}
}

Внутренняя реализация сопрограмм не до­пускает вызова блокирующих API-функций внутри выражения switch. Рассмотрим пример:

// Функция, реализующая сопрограмму
void vACoRoutineFunction( xCoRoutineHandle xHandie, unsignedportBASE_TYPE uxlndex)
{
crSTART( xHandle );
for(;;) {
// Непосредственно в сопрограмме
// блокирующие API-функции вызывать можно.
crDELAY( xHandle, 10);
switch (aVariable)
{
case 1: //Здесь нельзя вызывать блокирующие API-функции.
break;
default: // Здесь тоже нельзя.
}
}
crEND();
}

API-функции, предназначенные для вызова из сопрограмм

Текущая версия FreeRTOS v7.0.1 поддер­живает следующие API-функции, предназна­ченные для вызова из сопрограмм:

-  crDELAY();

- crQUEUE_SEND();  

- crQUEUE_RECEIVE();

Кроме этого, существуют еще API-функции crQUEUE_SEND_FRОМ ISR() и crQUEUE_ RECEIVE_FROM_lSR(), предназначенные для вызова из обработчиков прерываний и вы­полняющие операции с очередью, которая ис­пользуется только в сопрограммах.

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

Стоит подчеркнуть, что API-функции, предназначенные для вызова из сопрограмм, разрешено вызывать только непосредствен­но из тела сопрограммы. Вызов их из других функций запрещен.

Префикс всех вышеперечисленных API- функций указывает на заголовочный файл croutine.h, в котором эти API-функции объ­явлены.

Реализация задержек в сопрограммах

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

void crDELAY( xCoRoutineHandle xHandie, portTickType xTicksToDelay};

Аргументы API-функции crDELAYQ:

1. xHandle— дескриптор вызывающей со­программы. Автоматически передается в функцию, реализующую сопрограмму, в виде первого ее аргумента.

2. xTicksToDelay — количество квантов вре­мени, в течение которых сопрограмма будет заблокирована. Если xTicksToDelay равен 0, то вместо блокировки сопрограм­мы происходит переключение на другую готовую к выполнению сопрограмму. Отдельно следует обратить внимание на вы­зов crDELAY(), когда аргумент xTicksToDelay равен 0. В этом случае вызывающая crDELAY(xHandle, 0) сопрограмма переходит в состояние готовности к выполнению, а в со­стояние выполнения переходит другая сопро­грамма, приоритет которой выше или равен приоритету вызывающей сопрограммы.

Посредством вызова crDELAY (xHandle, 0) происходит принудительное переключение на другую сопрограмму, что было продемон­стрировано в учебной программе № 1.

Следует отметить, что применительно к сопрограммам не существует аналога API- функции vTaskDelayUntil(), которая пред­назначена для вызова из задач и позволя­ет организовать циклическое выполнение какого-либо действия со строго заданным пери­одом. Также отсутствует аналог API-функции xTaskGetTickCount(), которая позволяет полу­чить текущее значение счетчика квантов.

Использование очередей в сопрограммах

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

FreeRTOS допускает использование очере­дей и в сопрограммах, но в этом случае су­ществует одно серьезное ограничение: одну и ту же очередь нельзя использовать для пере­дачи сообщений от очереди к сопрограмме и наоборот. Допускается лишь передача сооб­щений между сопрограммами и обработчика­ми прерываний. Когда очередь создана, ее сле­дует использовать только в задачах или только в сопрограммах. Эта особенность существен­но ограничивает возможности совместного использования задач и сопрограмм.

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

1) uxQueueMessagesWaiting() — получение количества элементов в очереди.

2) xQueueSendToFront() — запись элемента в начало очереди.

3) xQueuePeek() — чтение элемента из очере­ди без удаления его из очереди.

4) xQueueSendToFrontFromlSR() — запись элемента в начало очереди из обработчика прерывания.

Запись элемента в очередь

Для записи элемента в очередь из тела сопро­граммы служит API-функция crQUEUE__SENDO- Ее прототип:

crQUEUE_SEND(

                                 xCoRoutineHandle xHandie,

                                 xQueueHandle pxQueue,

                                 void *pvItemToQueue,

                                 portTickType xTicksToWait,

                                portBASE_TYPE  *pxResult )

Аргументы API-функции crQUEUE_SEND():

1.xHandle — дескриптор вызывающей сопрограммы. Автоматически передается в функцию, реализующую сопрограмму, в виде первого ее аргумента.

2. pxQueue — дескриптор очереди, в которую будет записан элемент. Дескриптор очере­ди может быть получен при ее создании API-функцией xQueueCreate().

3. pvItemToQueue — указатель на элемент, который будет записан в очередь. Размер элемента зафиксирован при создании оче­реди. Именно это количество байт будет скопировано с адреса, на который ссыла­ется указатель pvItemToQueue.

4. xTicksToWait — максимальное количество квантов времени, в течение которого со­программа может пребывать в блокиро­ванном состоянии, если очередь полна и записать новый элемент нет возмож­ности. Для представления времени в мил­лисекундах следует использовать макро­определение portTICK_RATE_MS. Задание xTicksToWait равным 0 приведет к тому, что сопрограмма не перейдет в бло­кированное состояние, если очередь полна, и управление будет возвращено сразу же.

5. pxResult — указатель на переменную типа portBASE_TYPE, в которую будет поме­щен результат выполнения API-функции crQUEUE_SEND(). Может принимать сле­дующие значения:

-    pdPASS— означает, что данные успеш­но записаны в очередь. Если определено время тайм-аута (параметр xTicksToWait не равен 0), то возврат значения pdPASS говорит о том, что свободное место в очереди появилось до истечения вре­мени тайм-аута и элемент был помещен в очередь.

-    Код ошибки errQUEUE_FULL, опреде­ленный в файле ProjDefs.h.

Следует отметить, что при записи элемента в очередь из тела сопрограммы нет возмож­ности задать время тайм-аута равным бес­конечности, такая возможность есть, только если задача записывает элемент в очередь. Установка аргумента xTicksToWait равным константе portMAX_DELAY приведет к пере­ходу сопрограммы в блокированное состо­яние на конечное время, равное portMAX_ DELAY квантов времени. Это связано с тем, что сопрограмма не может находиться в при­остановленном (suspended) состоянии.

Чтение элемента из очереди

Для чтения элемента из очереди служит API-функция crQUEUE_RECEIVE(), кото­рую можно вызывать только из тела сопрограммы. Прототип API-функции crQUEUE_ RECEIVE():

void crQUEUE_RECEIVE(

                                                     xCoRoutineHandle xHandle,

                                                     xQueueHandle pxQueue,

                                                     void  *pvKuffer,

                                                     portTickType xTicksToWait,

                                                     portBASE_TYPE *pxResult

                                                   )

Аргументы API-функции crQUEUE_ RECEIVE():

1. xHandle — дескриптор вызывающей со­программы. Автоматически передается в функцию, реализующую сопрограмму, в виде первого ее аргумента.

2. pxQueue — дескриптор очереди, из кото­рой будет прочитан элемент. Дескриптор очереди может быть получен при ее созда­нии API-функцией xQueueCreate().

3. pvBuffer— указатель на область памя­ти, в которую будет скопирован элемент из очереди. Участок памяти, на которую ссылается указатель, должен быть не мень­ше размера одного элемента очереди.

4. xTicksToWait— максимальное количе­ство квантов времени, в течение которого сопрограмма может пребывать в блоки­рованном состоянии, если очередь пуста и считать элемент из очереди нет возмож­ности. Для представления времени в мил­лисекундах следует использовать макро­определение portTICK_RATE_MS . Задание xTicksToWait равным 0 приведет к тому, что сопрограмма не перейдет в бло­кированное состояние, если очередь пуста, и управление будет возвращено сразу же.

5. pxResult — указатель на переменную типа portBASE_TYPE, в которую будет поме­щен результат выполнения API-функции crQUEUE_RECEIVE(). Может принимать следующие значения:

-    pdPASS — означает, что данные успешно прочитаны из очереди. Если определено время тайм-аута (параметр xTicksToWait не равен 0), то возврат значения pdPASS говорит о том, что новый элемент в очереди появился до истечения вре­мени тайм-аута.

-    Код ошибки errQUEUE_FULL, опреде­ленный в файле ProjDefs.h.

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

Запись/чтение в очередь из обработчика прерывания

Для организации обмена между обработ­чиками прерываний и сопрограммами пред­назначены API-функции crQUEUE_SEND_ FROM_ISR() и crQUEUE_RECEIVE_FROM_ISR(), вызывать которые можно только из обработчиков прерываний. Причем оче­редь можно использовать только в сопро­граммах (но не в задачах).

Запись элемента в очередь (которая исполь­зуется только в сопрограммах) из обработчи­ка прерывания осуществляется с помощью API-функции crQUEUE_SEND_FROM_ISR(). Ее прототип:

portBASE_TYPE crQUEUE_SEND_FROM_ISR(

xQueueHandle pxQueue,

void *pvItemToQueue,

portBASE_TYPE xCoRoutinePreviouslyWoken )

Ее аргументы и возвращаемое значение:

1. pxQueue — дескриптор очереди, в которую будет записан элемент. Дескриптор очере­ди может быть получен при ее создании API-функцией xQueueCreate()

2. pvItemToQueue — указатель на элемент, который будет записан в очередь. Размер элемента зафиксирован при создании оче­реди. Именно это количество байт будет скопировано с адреса, на который ссылает­ся указатель pvItemToQueue.

3. xCoRoutinePreviouslyWoken — этот аргу­мент необходимо устанавливать в pdFALSE, если вызов API-функции crQUEUE_SEND_ FROM_ISR() является первым в обработчи­ке прерывания. Если же в обработчике пре­рывания происходит несколько вызовов crQUEUE_SEND_FROM_ISR() (несколь­ко элементов помещается в очередь), то аргумент xCoRoutinePreviouslyWoken следует устанавливать в значение, кото­рое было возвращено предыдущим вы­зовом crQUEUE__SEND_FROM_ISR(). Этот аргумент введея для того, чтобы в случае, когда несколько сопрограмм ожи­дают появления данных в очереди, только одна из них выходила из блокированного состояния.

4. Возвращаемое значение. Равно pdTRUE, если в результате записи элемента в оче­редь разблокировалась одна из сопрограмм. В этом случае необходимо выполнить пере­ключение на другую сопрограмму после выполнения обработчика прерывания.

Чтение элемента из очереди (которая ис­пользуется только в сопрограммах) из обра­ботчика прерывания осуществляется с по­мощью API-функции crQUEUE RECEIVE_ FROM_ISR(). Ее прототип:

portBASE_TYPE crQUEE_RECIEIVE_FROM_ISR(

                                                                                                     xQueueHandle pxQueue,

                                                                                                     void *pvBuffer,

                                                                                                     portBASE_TYPE * pxCoRoutineWoken

                                                                                                    )

 Аргументы и возвращаемое значение:

1. pxQueue — дескриптор очереди, в которую будет записан элемент. Дескриптор очере­ди может быть получен при ее создании API-функцией xQueueCreate().

2. pvItemToQueue — указатель на область па­мяти, в которую будет скопирован элемент из очереди. Объем памяти, на которую ссылается указатель, должен быть не мень­ше размера одного элемента очереди.

3. pxCoRoutineWoken — указатель на пере­менную, которая в результате вызова crQUEUE_RECEIVE_FROM_ISR () примет значение pdTRUE, если одна или несколько сопрограмм ожидали возможности поме­стить элемент в очередь и теперь разбло­кировались. Если таковых сопрограмм нет, то значение *pxCoRoutine Woken останется без изменений.

4. Возвращаемое значение:

-    pdTRUE, если элемент был успешно про­читан из очереди;

-    pdFALSE — в противном случае.

Учебная программа № 2

Рассмотрим учебную программу № 2, в ко­торой продемонстрирован обмен инфор­мацией между сопрограммами и обработ­чиками прерываний. В программе имеются 2 обработчика прерывания и 2 сопрограммы, которые обмениваются друг с другом сооб­щениями, помещая их в очередь (в програм­ме созданы 3 очереди). В демонстрационных целях в качестве прерываний используются программные прерывания MS-DOS, а слу­жебная сопрограмма выполняет вызов этих прерываний.

В графическом виде обмен информацией в учебной программе № 2 показан на рис. 5

 

Текст учебной программы №2:

#include
#include
#include
#include
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "portasm.h"
#include "croutine.h"
/* Дескрипторы очередей — глобальные переменные */
xQueueHandle xQueuel;
xQueueHandle xQueue2;
xQueueHandle xQueue3;
/* Служебная сопрограмма. Вызывает программные прерывания.
	Приоритет = 1.*/
void vIntCoRoutine( xCoRoutineHandle xHandie, unsigned portBASE_TYPE uxlndex )
crSTART( xHandle);
for(;;) {
/* Эта инструкция сгенерирует прерывание № 1. */
__asm {int 0х83}
/* Заблокировать сопрограмму на 500 мс */
crDELAY (xHandle, 500);
/* Эта инструкция сгенерирует прерывание № 2*/
asm {int 0x82}
/* Заблокировать сопрограмму на 500 мс */
crDELAY (xHandle, 500);
}
crEND();
}
/*------------------------------------*/
/*Функция, реализующая Сопрограмму № 1 и Сопрограмму № 2,
то есть будет создано два экземпляра этой сопрограммы. */
void vTransferCoRoutine( xCoRoutineHandle xHandle, unsigned portBASE_TYPE uxlndex) {
static long i;
portBASE_TYPE xResult;
crSTART(xHandle);
for(;;) {
/* Если выполняется Сопрограмма № 1 */
if (uxlndex == 1) { /* Получить сообщение из Очереди № 1 от Прерывания № 1.  Если очередь пуста —
заблокироваться на время portMAX_DELAY квантов */
crQUEUE_RECEIVE( xHandle,xQueue1, (void *)&i, portMAX_DELAY, &xResult);
if {xResult == pdTRUE) {
puts(" CoRoutine 1 has received a message from Interrupt 1.");
}
/* Передать это же сообщение в Очередь № 2 Сопрограмме No 2 */
crQUEUE_SEND( xHandle, xQueue2, (void *)&i,portMAX_DELAY, &xResult);
if (xResult == pdTRUE) { puts("CoRoutine 1 has sent a message to CoRoutine 2.");
}
}
/* Если выполняется Сопрограмма № 2 */
else if (uxlndex == 2) {
/* Получить сообщение из Очереди № 2 от Сопрограммы № 1.
Если очередь пуста — заблокироваться на время portMAX_DELAY квантов. */
crQUEUE_RECEIVE( xHandle, xQueue2, (void *)&i, portMAX_DELAY, &xResult);
if (xResult == pdTRUE) { puts("CoRoutine 2 has received a message from CoRoutine 1.");
{
/* Передать это же сообщение в обработчик прерывания №2 через Очередь № 3. */
crQUEUE_SEND( xHandle, xQueue3, (void *)&i, portMAX_DELAY, &xResult);
if (xResult == pdTRUE) { puts("CoRoutine 2 has sent a message to Interrupt 1.");
}
}
i}
crEND();
}
/*----------------------------*/
/* Обработчик Прерывания 1 */
static void__interrupt__far vSendInterruptHandler( void )
{
static unsigned long ulNumberToSend;
if (crQUEUE_SEND_FROM_ISR( xQueue1, &ulNumberToSend, pdFALSE)== pdPASS) {
puts ("Interrupt 1 has sent a message!");
}
}
/*-----------------------------*/
/* Обработчик Прерывания 2*/
static void__interrupt__far__vReceivelnterruptHandler(void)
{
static portBASE_TYPE pxCoRoutineWoken;
static unsigned long ulReceivedNumber;
/* Аргумент API-функции crQUEUE_RECEIVE_FROM_ISR(),
который устанавливается в pdTRUE,
если операция с очередью разблоадрует более
высокоприоритетную сопрограмму.
Перед вызовом crQUEUE_RECEIVE_FROM_ISR()
следует установить в pdFALSF.. */
pxCoRoutineWoken = pdFALSE;
if (crQUEUE_RECEIVE_FROM_ISR( xQueue3,
                            &ulReceivedNumber,
                            &pxCoRoutineWoken ) == pdPASS) {
 puts("lnterrupt 2 has received a message!\n");
}
/* Проверить, нуждается ли в разблокировке более
	высокоприоритетная сопрограмма,
	чем та, что была прервана прерыванием. */
if( pxCoRoutineWoken == pdTRUE ) {
/* В текущей версии FreeRTOS нет средств для корректного
	переключения на другую сопрограмму из тела обработчика
	прерывания! */
}
}
/*-----------------------------------------*/
/* Точка входа. С функции main() начнется выполнение про¬граммы. */
int main (void) {
/* Создать 3 очереди для хранения элементов типа unsigned long.
Длина каждой очереди — 3 элемента. */
xQueuel = xQueueCreate(3, sizeof (unsigned long));
xQueue2 = xQueueCreate(3, sizeof (unsigned long));
xQueue3 = xQueueCreate(3, sizeof (unsigned long));
/* Создать служебную сопрограмму. Приоритет = 1.*/
xCoRoutineCreate(vlntCoRoutine, 1, 0);
/* Создать сопрограммы № 1 и № 2 как экземпляры одной сопрограммы.
	Экземпляры различаются целочисленным параметром,
	который передается сопрограмме при ее создании. ,
	Приоритет обеих сопрограмм = 2. */
xCoRoutineCreate(vTransferCoRoutine, 2,1);
xCoRoutineCreate(vTransferCoRoutine, 2,2);
/* Связать прерывания MS-DOS с соответствующими об¬работчиками прерываний. */
_dos_setvect( 0x82,vReceiveInterruptHandIer);
_dos_set vect(0x83,vSendInterruptHand1er);
/* Запуск планировщика. */
 vTaskStartScheduler();
/* При нормальном выполнении программа до этого места "не дойдет" */
 for (;;);
/* Функция, реализующая задачу Бездействие,
должна присутствовать в программе и содержать вызов
vCoRoutineSchedule() */
void vApplicationldleHook(void) {
/* Так как задача Бездействие не выполняет других действий, то вызов vCoRoutineSchedule() размещен внутри бесконечного цикла.*/
for(;;){
vCoRoutineSchedule();
}
}

По результатам работы учебной програм­мы (рис. 6) можно проследить, как сообщение генерируется сначала в Прерывании № 1, за­тем передается в Сопрограмму № 1 и далее в Сопрограмму № 2, которая в свою очередь отсылает сообщение Прерыванию № 2.

Время реакции системы на события

Продемонстрируем недостаток кооператив­ной многозадачности по сравнению с вытес­няющей с точки зрения времени реакции си­стемы на прерывания. Для этого заменим реа­лизацию служебной сопрограммы в учебной программе № 2 vIntCoRoutine()  на следующую:

/* Служебная сопрограмма. Вызывает программные прерывания.  Приоритет = 1 */
void vIntCoRoutine( xCoRoutineHandle xHandle, unsigned portBASE_TYPE uxlndex ) {
/* Все переменные должны быть объявлены как static. */
  static long i;
  crSTART( xHandle);
  for(;;) {
/* Эта инструкция сгенерирует Прерывание № 1.*/
   asm {int 0x83}
/* Грубая реализация задержки на какое-то время.*/
/* Служебная сопрограмма при этом не блокируется! */
 for (i = 0; i < 5000000; i++);
/* Эта инструкция сгенерирует Прерывание № 2. */
 asm {int 0x82}
/* Грубая реализация задержки на какое-то время.
Служебная сопрограмма при этом не блокируется! */
 for (i = 0; i < 5000000; i++);
}
crEND();
}

В этом случае низкоприоритетная служеб­ная сопрограмма не вызывает блокирующих

API-функций. Результат выполнения модифицированной учебной программы № 2 приведен на рис. 7.

На рис. 7 видно, что теперь выполняется только низкоприоритет­ная служебная сопрограмма. Высокоприоритетная Сопрограмма № 1 не получает процессорного времени, даже несмотря на то, что она вышла из блокированного состояния, когда Прерывание № I поме­стило сообщение в Очередь № 1.

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

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

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

С точки зрения времени реакции системы на внешние события кооперативная многозадачность не позволяет гарантировать задан­ное время реакции, что является одним из основных недостатков кооперативной многозадачности.

Выводы

Подводя итог, можно выделить следующие тезисы относительно со­программ во FreeRTOS:

  • Выполняются в режиме кооперативной многозадачности.
  • Значительно экономят оперативную память.
  • Автоматически устраняют проблему реентерабельности функций.
  • Не гарантируют заданного времени реакции системы на прерывание.
    • При написании сопрограмм следует придерживаться строгих огра ничений.
    • Бедный набор API-функций для работы с сопрограммами. Таким образом, использование сопрограмм может быть оправда­но лишь в том случае, если преследуется цель написания програм­мы, работающей под управлением FreeRTOS, на микроконтроллере, который не имеет достаточного объема оперативной памяти для реализации программы с использованием задач.

Leave a Reply

You must be logged in to post a comment.