FreeRTOS. Операционная система реального времени для микроконтроллеров. Часть 6. Создание программ под управлением RTOS


Автор: Андрей Курниц (kurnits@stim.by). Журнал КиТ
Шестая часть статьи посвящена взаимо­действию прерываний с остальной частью программы и поможет читателям ответить на следующие вопросы:

1) Какие API-функции и макросы можно ис­пользовать внутри обработчиков преры­ваний?

2)  Как реализовать отложенную обработку прерываний?

3)  Как   создавать и использовать двоичные и счетные семафоры?

4)  Как использовать очереди для передачи информации в обработчик прерывания и из него?

5)  Каковы особенности обработки вложен­ных прерываний во FreeRTOS?

События и прерывания

Встраиваемые микроконтроллерные си­стемы функционируют, отвечая действиями на события внешнего мира. Например, полу­чение Ethernet-пакета (событие) требует обра­ботки в задаче, которая реализует TCP/IP-стек (действие). Обычно встраиваемые системы обслуживают события, которые приходят от множества источников, причем каждое со­бытие имеег свое требование по времени ре­акции системы и расходам времени на его об­работку. При разработке встраиваемой микроконтроллерной системы необходимо подобрать свою стратегию реализации обслу­живания событий внешнего мира. При этом перед разработчиком возникает ряд вопросов:

1) Каким образом события будут регистриро­ваться? Обычно применяют прерывания, однако возможен и опрос состояния вы­водов микроконтроллера.

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

3)Как обработчики прерываний связаны с остальным кодом и как организовать программу, чтобы обеспечить наибы­стрейшую обработку асинхронных собы­тий внешнего мира?

FreeRTOS не предъявляет никаких требо­ваний к организации обработки событий, однако предоставляет удобные возможности для такой организации.

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

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

Отложенная обработка прерываний

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

Двоичные семафоры

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

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

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

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

1) wait() или Р() — означает захват семафора, если он свободен, и ожидание, если занят. В примере выше функцию wait() реализует задач а - обработчик пpepывания.

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

Легко заметить, что операция выдачи двоичного семафора напоминает операцию помещения элемента в очередь, а операция захвата семафора — чтения элемента из оче­реди. Если установить размер очереди рав­ным одному элементу, то очередь превра­щается в двоичный семафор. Наличие эле­мента в очереди означает, что одна (а может, и несколько) задача произвела(и) выдачу семафора, и теперь другая задача может его захватить. Пустая же очередь означает ситуа­цию, когда семафор уже был захвачен, и за­дача, которая «хочет» его захватить, вынуж­дена ожидать (находясь в блокированном со­стоянии), пока другая задача или обработчик прерывания произведут выдачу семафора.

В именах API-функций FreeRTOS для ра­боты с семафорами используются термины Take — эквивалентен функции wait(), то есть захват двоичного семафора, и Give — экви­валентен функции signal(), то есть означает выдачу семафора, но никогда не отдает его обратно. Такой сценарий еще раз подчеркивает сходство ра­боты двоичного семафора с очередью. Стоит отметить, что одна из частых причин ошибок в программе, связанных с семафорами, заклю­чается в том, что в других сценариях задача после захвата семафора должна его отдать.

Во FreeRTOS механизм семафоров основан на механизме очередей. По большому счету API-функции для работы с семафорами пред­ставляют собой макросы — «обертки» других API-функций для работы с очередями. Здесь и далее для простоты будем называть их API- функциями для работы с семафорами.

Все API-функции работы с семафора­ми сосредоточены в заголовочном файле /Source/Include/semphr.h, поэтому следует убедиться, что этот файл находится в списке включенных (#include) в проект.

Доступ ко всем семафорам во FreeRTOS (а не только к двоичным) осуществляется с помощью дескриптора (идентификато­ра) — переменной типа xSemaphoreHandle.

Создание двоичного семафора

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

void vSemaphoreCreateBinary( xSemaphoreHandle xScmaphore );

Единственным аргументом является де­скриптор семафора, в него будет возвращен дескриптор в случае успешного создания семафора. Если семафор не создан по при­чине отсутствия памяти, вернется значение NULL. Гак как vSemaphoreCreateBinaryO представляет собой макрос, то аргумент xSemaphore следует передавать напрямую, то есть нельзя использовать указатель на де­скриптор и операцию переадресации.

Захват семафора

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

portBASE_TYPE xSemaphoreTake( xSemaphoreHandle хSemaphore, portTickType xTicksToWait);

Назначение параметров и возвращаемое значение:

•    xSemaphore— дескриптор семафора. Должен быть получен с помощью API функции создания семафора.

•    xTicksToWait — максимальное количество квантов времени, в течение которого за­дача может пребывать в блокированном состоянии, если семафор невозможно захватить (семафор недоступен). Для представления времени в миллисекундах следует использовать макроопределение portTICK_RATE_MS. Задание xTicksToWait равным 0 приведет к тому, что задача не перейдет в блокированное состояние, если семафор недоступен, а продолжит свое выполнение сразу же. Установка xTicksToWait равным константе portMAX_DELAY приведет к тому, что вы­хода из блокированного состояния по ис­течении времени тайм-аута не произойдет. Задача будет сколь угодно долго «ожидать» возможности захватить семафор, пока та­кая возможность не появится. Для этого макроопределение #INCLUDE_vTaskSuspend в файле FreeRTOSConfig.h должно быть равно «1».

Возвращаемое значение - возможны два варианта:

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

-    pdFALSE — означает, что семафор недо­ступен (никто его не отдал). Если опре­делено время тайм-аута (параметр xTicksToWait не равен 0 или portMAX_ DELAY), то возврат значения pdFALSE говорит о том, что время тайм-аута ис­текло, а семафор так и не стал доступен.

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

Все типы семафоров во FreeRTOS, исклю­чая рекурсивные, могут быть выданы из тела обработчика прерывания при помощи API- функции xSemaphoreGiveFromlSR().

API-функция xSemaphoreGiveFromlSRf) представляет собой специальную версию API-функции xSemaphoreGive()  которая предназначена для вызова из тела обработчи­ка прерывания.

Прототип API-функции xSemaphoreGiveFromlSR():

portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle xSemaphore,  portBASE_TYPE *pxHigherPriorityTaskWoken );

Назначение параметров и возвращаемое значение:

1.       xSemaphore — дескриптор семафора, ко­торый должен быть в явном виде создан до первого использования.

2.      pxHigherPriorityTaskWoken — значение xSemaphoreGiveFromlSR() и зависимости от того, разблокирована ли более высоко­приоритетная задача в результате выдачи семафора. подробнее об этом будет сказано далее.

3.       Возвращаемое значение — возможны два варианта:

-         pdPASSxSemaphoreGiveFromISR() был успешным, семафор отдан.

-         pdFAIL — означает, что семафор в мо­мент вызова xSemaphonGiveFromlSR()

другой задачей или прерыванием. (датчика прерывания была разблокирована более высокоприоритетная задача, чем та, что была прервана обработчиком прерывания, то API-функция xSemaphoreGiveFromlSR() установит *pxHigherPriorityTaskWoken рав­ным pdTRUE,  в противном случае значение *pxHigherPriorityTaskWoken останется без изменений

Значение *pxHigherPriorityTaskWoken необходимо отслеживать, чтобы  вручную выполнить переключение контекста задачи в конце обработчика прерывания, если в результате выдачи семафора была разблоки­рована более высокоприоритетная задача. Если этого не сделать, то после выполнения обра­ботчика прерывания выполнение продолжит та задача, выполнение которой были прервано этим прерыванием (рис. ). Ничего «страшно­го» в этом случае не произойдет: текущая за­дача будет выполняться до истечения текущего кванта времени, после чего планировщик вы­полнит переключение контекста (которое он выполняет каждый системный квант), и управ­ление получит более высокоприоритетная за­дача (рис. а). Единственное, что пострадает, это  время реакции системы на прерывание, ко­торое может составлять до одного системного кванта: величина dT на рис.

Далее в учебной программе  будет приведен пример использования значения * pxHigherPriorityTaskWoken для принуди­тельного переключения контекста.

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

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


#include
#include
#include
#include
#include
#include "task.h"
#include "semphr.h"
#include "portasm.h"
/* Двоичный семафор - глобальная переменная */
xSemaphoreHandle xBinarvSemaphore;
/* Периодическая задача */
static void vPeriodicTask (void *pvParameters) {
for (;;) {
*/Эта задача используется только с целью генерации прерывания каждые 500 мс */
vTaskDelay(500 / portT1CK_RATE_MS); /* (Сгенерировать прерывание. Вывести сообщение до этого и после. */ puts("Periodic task - About to generate an interrupt");
 _asm(int 0x82); /*генерировать прерывание MS-DOS*/
puts("Periodic task - Interrupt generated.\r\n\r\n\r\n");
}
}
/* Обработчик прерывания */
static void_interrupt_far_vExampleInterruptHandler ( void )
{
static portBASE_TYPE xHigherPriorityTaskWoken;
xHigherPnorityTaskWoken = pdfALSE; /* Отдать семафор задаче-обработчику */
xSemaphoreGiveFromlSR(xBinarySemaphore, &cxHigherPriorityTaskWoken);
if( xHigherPriorityTaskWoken == pdTRUE )
{
/* Это разблокирует задачу-обработчик. При этом приоритег задачи-обработчика выше приоритета выполняющейся в данный момент периодической задачи. Поэтому переключаем контекст принудительно - так мы добьемся того, что после выполнения обработчика прерывания управление получит задача обработчик.*/
/* Макрос, выполняющий переключение контекста. На других платформах имя макроса может быть другое! */
 portSWITCH_CONTEXT();
}
}
/* Задача обработчик */
static void vHandlerTask(void *pvParameters)
{
/* Как и большинство задач реализована как бесконечный цикл */
for (;;) {
/* Реализовано ожидание события с помощью двоичного семафора. Семафор после создания становится доступен (так, как будто его кто-то отдал). Поэтому сразу после запуска планировщика задача захватит его. Второй раз сделать это ей не удастся, и она будет ожидать, находясь в блокированном состоянии, пока семафор не отдаст обработчик прерывания. Время ожидания ааЪно равным бесконечности, поэтому нет необходимости проверять возвращаешь функцией xSemaphoreTake() лначеиие. */
 xSemaphoreTake( xBinarv Semaphore, portMAX_DELAY);
/* Если программа "дошла" до этого места, значит семафор был успешно захвачен. (Обработка события, связанного с семафором. В нашем случае - индикация на дисплей.*/
puts( "Handler task Processing event.\r\n");
}
}
/* Точка входа. С функции main() начнется выполнение программы. */
int main(void) {
/* Перед использованием семафор необходимо создать.*/
vSemaphorCeateBinary(xBinarvSemaphore); /* Связать прерывание MS-DOS с обработчиком прерывания vExamplelnterruptHandler(). */
_dos_setvect(Ox82, vExamplelnterruptHandler) /* Если семафор успению создан */
if (xBinarvSemaphore != NULL) { /* Создать задачу обработчик, которая будет синхронизирована с прерыванием. Приоритет задачи обработчика выше, чем у периодической ладами.*/
 xTaskCreate( vHandlerTask, "Handler". 1000, NULL, 3, NULL); /* Создать периодическую задачу, которая будет генерировать прерывание с некоторым интервалом. Ее приоритет ниже, чем у задачи обработчика. */ xTaskCreate(vPeriodicTask, "Periodic", 1000, NULL 1, NULL); /* Запуск планировщика. */
vTaskStartScheduler();
}
/* При нормальном выполнении программа до этого места "не дойдет"*/
for (;;);
}

В демонстрационных целях использовано не аппаратное, а программное прерывание MS-DOS, которое «вручную» вызывается из служебной периодической задачи каждые 500 мс. Заметьте, что сообщение на дисплей выводится как до генерации прерывания, так и после него, что позволяет проследить по­следовательность выполнения задач.

Следует обратить внимание на использо­вание параметра хНigherPriorityTaskWoken в API-функции xSemaphoreGiveFromlSR(). До вызова функции ему присваивается значе­ние pdFALSE, а после вызова — проверяется на равенство pdTRUE. Таким образом отсле­живается необходимость принудительного переключения контекста.

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

Для принудительного переключения кон­текста служит API макрос portSWITCH_ CONTEXT(). Однако для других платформ имя макроса будет иным, например, для ми­кроконтроллеров AVR это будет taskYIELD(), для ARM7 — portYIELD_FROM_ISR (). Узнать точное имя макроса можно из демонстраци­онного проекта для конкретной платформы.

Переключение между задачами в учебной программе № 1 приведено на рис.

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

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

portSWITCH_CONTEXT();

то можно наблюдать описанный ранее эф­фект ( рис. ).

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

Подведя итоги, можно представить такую последовательность действий при отложен­ной обработке прерываний с помощью дво­ичного семафора:

•      Происходит событие внешнего мира, вследствие него — прерывание микро­контроллера.

•      Выполняется обработчик прерывания, который отдает семафор и разблокирует таким образом задачу — обработчик пре­рывания.

•      Задача-обработчи к начинает выполняться, как только завершит выполнение обработ­чик прерывания. первое, что она делает,— захватывает семафор.

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

Счетные семафоры

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

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

В отличие от двоичных семафоров со­стояние счетного семафора определяется не значениями отдан/захвачен, а представ­ляет собой целое неотрицательное число — значение счетного семафора. И если двоичный семафор — это, по сути, очередь дли­ной в 1 элемент, то счетный семафор можно представить очередью в несколько элемен­тов. Причем текущее значение семафора представляет собой длину очереди, то есть количество элементов, которые в данный момент находятся в очереди. Значение эле­ментов, хранящихся в очереди» когда она ис­пользуется как счетный (или двоичный) се­мафор, не важно, а важно само наличие или отсутствие элемента.

Существует два основных применения счетных семафоров:

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

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

Работа со счетными семафорами

Создание счетного семафора

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

xSemaphoreHandle xSemaphoreCreateCounting ( unsigned portBASE_TYPE uxMaxCount, unsigned portBASE_TYPE uxInitialCount );

Назначение параметров и возвращаемое значение:

1. uxMaxCount— задает максимально воз­можное значение семафора. Если проводить аналогию с очередями, то он эквивалентен размеру очереди. Определяет максимальное количество событий, которые может обработать семафор, или общее количество до­ступных ресурсов, если семафор используется для управления очередями.
2. uxInitialCount— задает значение сема­фора, которое он принимает сразу после создания. Если семафор используется хтя подсчета событий, следует установить uxInitialCount равным 0, что будет озна­чать, что ни одного события еще не про­изошло. Если семафор используется для управления доступом к ресурсам, то сле­дует установить uxInitialCount равным максимальному значению — параметру uxMaxCount. Это будет означать, что все ресурсы свободны.

Возвращаемое значение — равно NULL, если семафор не создан по причине отсут­ствия требуемого объема свободной памя­ти. Ненулевое значение означает успешное создание счетного семафора. Это значе­ние необходимо сохранить в переменной типа xSemaphoreHandle для обращения к семафору в дальнейшем. API-функции выдачи (инкремента, уве­личения на единицу) и захвата (декремен­та, уменьшения на единицу) счетного сема­фора ничем не отличаются от таковых для двоичных семафоров: xSemaphoreTake() — захват семафора; xSemaphoreGive(),xSemaphoreGiveFromISR() — выдача семафо­ра, соответственно, из задачи и из обработ­чика прерывания.

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

/* Обработчик прерывания */
static void_interrupt_far vExamplelnterruptHandler (void )
{
static portBASE_TYPE xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE; /* Отдать семафор задаче-обработчику несколько раз. Таким образом
симулируется быстро следующая группа событий, с которыми связано прерывание. Первая выдача разблокирует
задачу-обработчик. Cледующие будут "запомнены" счетным семафором и обработаны позже. "Потери" событий не происходит.*/
xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken);
xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken);
xSemaphoreGiveFromlSR( xBinarySemaphore, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* Макрос, выполняющий переключение контекста. На других платформах имя макроса может быть другое!*/
portSWITCH_CONTEXT();
}
}

API-функцию создания двоичного сема­фора в главной функции main():

Перед использованием семафор необходимо создать.

vSemaphoreCreateBinary( xBinarySemaphore);

следует заменить функцией создания счетно­го семафора:

/* Перед использованием счетный семафор необходимо создать.

Семафор сможет обработать максимум 10 событий. Начальное значение - 0 */

xBinarySemaphore = xSemaphoreCreateCounting( 10,0);

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

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

Использование очередей в обработчиках прерываний

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

Ранее мы говорили об API- функциях для работы с очередями: xQueueSendToFront() ,xQueueSendToBack() и xQueueReceive(). Использование их вну­три тела обработчика прерывания приведет к краху программы. Для этого существу­ют версии этих функций, предназначен­ные для вызова из обработчиков пре­рываний: xQueueSendToFrontFromlSR(), xQueueSendToBackFromlSR () и xQueueReceiveFromISR(), причем вы­зов их из тела задачи запрещен. API- функция xQueueSendFromlSR() яв­ляется полным эквивалентом функ­ции xQueueSendToBackFromISR().

Функции xQueueSendToFrontFromISR(), xQueueSendToBackFromlSR() служат для запи­си данных в очередь и отличаются лишь тем, что первая помещает элемент в начало очере­ди, а вторая — в конец. В остальном их поведе­ние идентично.

Рассмотрим их прототипы:

portBASE_TYPE xQueueSend ToFrontFromlSRl xQueueHandlу xQueue, void * pvItemToQueue portBASE_TYPE *pxHigherPriorityTaskWoken );

portBASE_TYPE xQueueSendToBackFromlSR( xQueueHandle xQueue, void * pvItemToQueue port BASE_TYPE *pxHigherPriorityTaskWoken );

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

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

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

3.  pxHigherPriorityTaskWoken — значение *pxHigherPriorityTaskWoken устанавливается равным pdTRUE, если существует задача, которая «хочет» прочитать данные из очереди, и приоритет у нее выше, чем у задачи, выполнение которой прервало прерывание. Если таковой задачи нет, то значение *pxHigherPriorityTaskWoken оста­ется неизменным. Проанализировав зна­чение *pxHigherPriorityTaskWoken после выполнения xQueueSendToFrontFromlSR() или xQueueSendToBackFromISR() можно сделать вывод о необходимости принуди­тельного переключения контекста в конце обработчика прерывания. В этом случае управление сразу перейдет разблокирован­ной высокоприоритетной задаче.

4.  Возвращаемое значение — может прини­мать 2 значения:

-   pdPASS — означает, что данные успешно записаны в очередь.

-   errQUEUE_FULL — означает, что данные не записаны в очередь, так как очередь заполнена.

API-функция xQueueReceiveFromlSR() служит для чтения данных с начала очереди. Вызываться она должна только из обработ­чиков прерываний. Ее прототип:

portBASE_TYPE xQueueReceiveFromlSR( xQueueHandle pxQueue, void *pvBuffer, portBASE_TYPE  *pxlastWoken)

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

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

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

3.  pxTaskWoken — значение *pxTaskWoken устанавливается равным pdTRUE, если су­ществует задача, которая «хочет»» записать данные в очередь, и приоритет у нее выше, чем у задачи, выполнение которой прервало прерывание. Если таковой задачи нет, то зна­чение * pxTaskWoken остается неизменным. Проанализировав значение * pxTaskWoken после выполнения xQueueReceiveFromlSR(), можно сделать вывод о необходимости принудительного переключения контекста в конце обработчика прерывания. В этом случае управление сразу перейдет разбло кированной высокоприоритетной задаче.

1.   Возвращаемое значение— может прини­мать 2 значения:

-    pdTRUE — означает, что данные успеш но прочитаны из очереди.

-    pdFALSE — означает, что данные не про читаны, так как очередь пуста.

Следует обратить внимание, что в отличие от версий API-функций для работы с очередя­ми, предназначенными для вызова из тела за­дачи, описанные выше API-функции не име­ют параметра portTickType xTicksToWait, который задает время ожидания задачи в бло­кированном состоянии. Что и понятно, так как обработчик прерывания — это не задача, и он не может переходить в блокированное состояние. Поэтому если чтение/запись из/в очередь невозможно выполнить внутри об­работчика прерывания, то соответствующая API-функция вернет управление сразу же.

Эффективное использование очередей

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

Гораздо эффективнее использовать один из следующих подходов:

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

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

Рассмотрим учебную программу 2, в ко­торой продемонстрировано применение API-функций xQueueSendToBackFromlSR() и xQueueReceiveFrotnlSR() внутри обработ­чика прерываний. В программе реализована задача — генератор чисел, которая отвечает за генерацию последовательности целых чи­сел. Целые числа по 5 штук помешаются в оче­редь №1, посте чего происходит программное прерывание (для простоты оно генерируется из тела задачи — генератора чисел). Внутри обработчика прерывания происходит чте­ние числа из очереди № I с помощью API функции xQueueReceiveFromlSR(). Далее это число преобразуется в указатель на строку, ко­торый помешается в очередь №2 с помощью API-функции xQueueSendToBackFrotnlSR(). Задача-принтер считывает указатели из очере­ди № 2 и выводит соответствующие им строки на экран (рис. ). Текст учебной программы :

#include
#include
#include
#include
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "portasm.h"
/* Дескрипторы очередей - глобальные переменные */
xQueueHandle xlntegerQueue;
xQueueHandle xStringQueue;
/* Периодическая задача — генератор чисел */
static void vlntegerGenerator( void *pvParameter) {
portTickType xIastExecutionTime;
unsigned portLONG ulValueToSend = 0;
int i;
/* Переменная xlastExecutionTime нуждается в инициализации текущим значением счетчика квантов. Это единственный случай, когда ее значение задается явно. В дальнейшем ее значение будет автоматически модифицироваться API функцией vTaskDelayUntil().*/
xLastExecutionTime = xTaskGetTickCount();
for (;;) {
/* Это периодическая задача. Период выполнения - 200 мс. */
 vTaskDelayUntil(&xlastExecutionTime, 200 / portTICK_RATE_MS); /* Отправить в очередь №1 5 чисел от 0 до 4. Числа будут считаны из очереди в обработчике прерывания. Обработчик прерывания всегда опустошает очередь, но «ТОМУ запись 3 элементов будет всегда возможна - в переходе в блокированное состояние нет иеобходимости */
for (i = О; i <	5;++i) {
xQueueSendToBack( xlntegerQueue, &ulValueToSend, 0);
ulValueToSend++;
}
/* Принудительно вызвать прерывание. Отобразить сообщение до его вызова и после. */
 puts ( "Generator task About to generate an interrupt.");
__asm (int 0xR2); /* Эта инструкция сгенерирует прерывание.*/
 puts( "Generator task - Interrupt generated.\r\n");
}
}
/* Обработчик прерывания */
static void interrupt_far_vKxamplelnterruptHandler ( void )
{
static portBASE_TYPE xHigherPriorityTaskWoken;
static unsigned long ulReceivedNumber;
/* Массив строк определен как static, значит, память хтя его размещения выделяется как
для глобальной переменной (он хранится не в стеке). */
static const char *pcStrings [] {
"String 0",
"String 1",
"String 2",
"String 3"
};
/* Аргумент API функции xQueueReceiveFromlSR (), который устанавливается в pdTRUE, если операция с очередью разблокирует более высокоприоритетную задачу. Перед вызовом xQueueReceiveFromlSR() должен принудительно устанавливаться в pdFALSE */
xHigherPrioritvTaskWoken = pdFALSE; /* Cчитывать из очереди числа, пока та не станет пустой. */
 while (xQueueReceiveFromlSR(),xlntegerQueue,
 &ulReceivedNumber,
 8cxHigherPriorityTaskWoken ) != errQEEUE_EMPTY )
{
/* Обнулить в числе все биты, кроме последних двух. Таким образом, получети* чисю будет принимать значения от 0 до 3. Использовать полученное число как индекс в массиве строк. Получить таким образом указатель на строку, который передать в очередь №2 */
ulReceivedNumber & = 0x03;
xQueueSendToBackFromISR( xStringQueue. &pcStrings[ulReceivedNumber], cxHigherPriorityTaskWoken );
}
/* Проверить, ие разблокировалась ли более вькокоприоритетная задача при записи в очередь. Если да. то выполнить принудительное переключение контекста. */
if (cxHigherPriorityTaskWoken == pdTRUE )
{
/* Макрос, выполняющий переключение контекста. На других платформах имя макроса может быть другое! */ poctSWITCН_CONTEXT();
}
}
/* Задача-принтер. */
static void vStringPrinter(void *pvParameters);
char *pcString; /* Ьесконечный цикл */
for(;;)
{ Прочитать очередной указатель на строку из очереди 2. Находится в блокированном состоянии сколь угодно долго, пока очередь 2 пуста. 7 xQueueReceive(xStringQueue, &tpcString, portMAX_DELAY);
/* Вывести строку, на которую ссылается указатель на дисплей. */
 puts( pcString);
}
/* Точка входа. С функции mainf) начнется выполнение программы. */
 int main(void) { /* Как и другие объекты ядра, очереди необходимо создать до первого их использования. Очередь xlntegerQueue будет хранить переменные типа unsigned long. Очередь xStringQueue будет хранить переменные типа char* - указатели на нуль-терминальные строки. Обе очереди создаются размером 10 элементов. Реальная программа должна проверять значения xlntegerQueue, xStringQueue, чтобы убедиться, что очереди успешно созданы. */
xlntegerQueue = xQueueCreate( 10, sizeof(unsigned long));
xStringQueue = xQueueCreate( 10, sizeof(char *)); /* Связать прерывание MS-DOS с обработчиком прерывания vExampleInterruptHandler(). */
 _dos_setvect(Ox82, vExamplelnterruptHandler); /* Создать задачу — генератор чисел с приоритетом 1. */ xTaskCreate(vlntegerGenerator, "IntGen", 1000, NULL, 1, NULL); /* Создать задачу-принтер с приоритетом 2.*/
xTaskCreate( vStringPrinter, "String", 1000, NULL, 2, NULL); /* Запуск планировщика. */
vTaskStartScheduler();
/* При нормальном выполнении программа до этого места
"не дойдет" */
for (;;);
}

Заметьте, что для эффективного распреде­ления ресурсов памяти данных (как и реко­мендовалось в ) очередь No 2 хра­нит не сами строки, а лишь указатели на стро­ки, которые содержатся в отдельном массиве. Такое решение вполне допустимо, гак как со­держимое строк в программе не изменяется.

По результатам выполнения  вид­но, что в результате возникновения прерыва­ния была разблокирована высокоприоритет­ная задача-принтер, после чего управление снова возвращается низкоприоритетной за­даче— генератору чисел (рис. ).

Задача-бездействие выполняется большую часть времени. Каждые 200 мс она вытесняет­ся задачей — генератором чисел (1). Задача — генератор чисел записывает в очередь № 1 пять целых чисел, после чего принудитель­но вызывает прерывание (2). Обработчик прерывания считывает числа из очереди № 1 и записывает в очередь № 2 указатели за соответствующие строки. Запись в оче­редь № 2 разблокирует высокоприоритетную задачу-принтер (3). Задача-принтер считыва­ет указатели на строки из очереди № 2, пока они там есть, и выводит соответствующие строки на экран. Как только очередь № 2 опу­стошится, задача-принтер переходит в бло­кированное состояние (4). Управление снова получает низкоприоритетная задача — ге­нератор чисел, которая также блокируется на время -200 мс, так что система снова пере­ходит в состояние бездействия (5).

Вложенность прерываний

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

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

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

Большинство портов FreeRTOS допу­скает вложение прерываний. Эти порты требуют задания одного или двух конфи­гурационных макроопределений в файле FreeRTOSConfig. h:

1.  configKERNELINTERRUPT_PRIORITY

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

2.   configMAX_SYSCALL_INTERRUPT__ PRIORITY—задает наибольший приоритет прерывания, из обработчика которого мож­но вызывать API-функции FreeRTOS (чтобы прерывания могли быть вложенными). Получить модель вложенности прерыва­ний без каких-либо ограничений можно задав значение configMAX_SYSCALL_INTERRUPT__ PRIORITY выше, чем configKERNEL_ INTERRUPT_PRIORITY.

Рассмотрим пример. Пусть некий микро­контроллер имеет 7 возможных приоритетов прерываний. Значение приоритета 7 соответ­ствует самому высокоприоритетному пре­рыванию, 1 — самому низкоприоритетному. Зададим значение configMAX_SYSCALL_ INTERRUPT_PRIORITY = 3, а значение configKERNEL_INTERRUPT_PRIORITY = 1 .

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

На прерывания с приоритетом 4 и выше не влияют критические секции, так что ниче­го, что делает ядро в данный момент, не поме­шает выполнению обработчика такого пре­рывания. Обычно те прерывания, которые имеют самые строгие временные требования (например, управление током в обмотках двигателя), должны иметь приоритет выше, чем configMAX_SYSCALL_INTERRUPT_ PRIORITY, чтобы гарантировать, что ядро не внесет дрожание (jitter) во время реакции на прерывание.

И наконец, прерывания, которые не вы­зывают никаких API функций, могут иметь любой из возможных приоритетов.

Критическая секция в FreeRTOS — это уча­сток кода, во время выполнения которого за­прещены прерывания процессора и, соответственно, не происходит переключение кон­текста каждый квант времени [7]. Подробнее о критических секциях — в следующей пу­бликации.

Следует отметить, что в популярном се­мействе микроконтроллеров ARM Cortex МЗ (как и в некоторых других) меньшие значения приоритетов прерываний соответствуют ло­гически большим приоритетам. Если вы хоти­те назначить прерыванию более высокий при­оритет, вы назначаете ему приоритет с более низким номером. Одна из возможных причин краха программы в таких случаях — назначе­ние прерыванию номера приоритета меньше го, чем configMAX_S YSCALL_INTTERRUPT_ PRIORITY, и вызов из него API функции.

Пример корректной настройки файла FreeRTOSConfig.h для микроконтроллеров ARM Cortex МЗ:

#define configKERNEL_INTERRUPT_PRIORITY      255

#define configMAX_SYSCAL_INTERRUPT_PRIORITY  191

Выводы

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

•     память;

•     периферийные устройства;

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

При этом результат обращения к ресурсу в обеих задачах окажется ошибочным, ис­каженным.

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

•     мьютексы и двоичные семафоры;

•     счетные семафоры;

•     критические секции;

•     задачи-сторожа (gatekeeper tasks).

Leave a Reply

You must be logged in to post a comment.