FreeRTOS. Операционная система реального времени для микроконтроллеров. Часть 3. Задачи и события.

Автор: Андрей Курниц (kurnits@stim.by). Журнал КиТ
Продолжать подробное рас­смотрение и демонстрацию возможностей FreeRTOS на платформе реального МК не всегда удобно. Гораздo удобнее использовать в ка­честве платформы любой х86 совместимый настольный компьютер, используя соответствующий порт FreeRTOS. Все последующие при­меры будут приведены для порта для х86 совместимых процессоров, работающих в реальном режиме. Мы используем бесплатный пакет Open Watcom, включающий Си-компилятор и среду разработки, об особенностях установки которого будет сказано ниже. Получаемые в результате компиляции и сборки исполнимые (ехе) файлы могут быть выполнены из интерпретатора команд Windows (cmd.exe). В ка­честве альтернативы можно использовать бесплатный эмулятор ОС DOS под названием DOSBox, который позволит выполнять примеры не только из-под Windows, но и из-под UNIX-подобных (FreeBSD, Fedora, Gentoo Linux) и некоторых других ОС.

Загрузить последнюю версию пакета OpenWatcom можно с офици­ального сайта. На момент написания статьи это версия 1.9. Файл для скачивания: open-watcom-c-witi32-L9.exe. Во время инсталляции пакета следует включить в установку 16-разрядный компилятор для DOS и до­бавить DOS в список целевых ОС (рис. 1 и 2).

После установки пакета Open Watcom нужно выполнить переза­грузку рабочей станции. Далее можно проверить работу компиля­тора, открыв демонстрационный проект, входящий в дистрибутив FreeRTOS. Проект располагается в C:/FreeRTOSV6.LO/Demo/PC/( в слу­чае установки FreeRTOS на диск С:/). Далее следует открыть файл про­екта Open Watcom, который называется rtosdemo.wpj, и выполнить сборку проекта, выбрав пункт меню Targets -> Маке. Сборка должна пройти без ошибок (рис. 3).

При этом в директории демонстрационного проекта появится ис­полнимый файл rtosdemo.exe, запустив который можно наблюдать результаты работы демонстрационного проекта в окне интерпретато­ра команд Windows (рис. 4).

В демонстрационный проект включена демонстрация всех воз­можностей FreeRTOS. Для наших целей, чтобы продолжить изучение задач, не вникая в остальные возможности FreeRTOS, необходимо исключить из проекта все исходные и заголовочные файлы, кроме файлов ядра FreeRTOS и файла mairt.c (рис. 5).

Кроме этого, необходимо произвести настройку ядра, отредактировав заголовочный файл FreeRTOSConfig.h:

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
#include <conio.h>
#include <i86.h>
#define configUSE_PREEMPTION                       1
#define configUSE_IDLE_HOOK                        0
#define configUSE_TICK_HOOK                        0
#define configTICK_RATE_HZ                (( portTickType) 1000 )
#define configMINIMAL_STACK_SIZE (( unsigned short) 256 )  /* This can be made smaller if required. */
#define соnfigTOTAL_HEAP_ SIZE             (size_t) ( 32 * 1024 ))
#define configMAX_TASK_NAME_LEN                   16
#define configUSE_TRACE_FACILITY                   1
#define configUSE_16_BIT_TICKS                     1
#define configIDLE_SHOULD_YIELD                    1
#define configUSE_CO_ROUTINES                      0
#define configUSE_MUTEXES                          1
#define configUSE_COUNTING_SEMAPHORES              1
#define configUSE_ALTERNATIVE_API                  1
#define configUSE_RECURSIVE_MUTEXES                1
#define configCHECK_FOR_STACK_OVERFLOW             0
#define configUSE_APPLICATION_TASK_TAG             1
#define configQUEUE_REGISTRY_SIZE                  0
#define configMAX_PRIORITIES	    (( unsigned portBASE_TYPE) 10 )
#define configMAX_CO_ROUTINE_PRIORITIES            2
/*Set the following definitions to 1 to include the API function, or zero to exclude the API function. */
#define INCLUDE_vTaskPrioritySet	           1
#define INCLUDE_uxTaskPriorityGet                  1
#define INCLUDE_vTaskDelete	                   1
#define INCLUDE_vTaskCleanUpResources	           1
#define INCLUDE_vTaskSuspend	                   1
#define INCLUDE_vTaskDelayUntil	                   1
#define INCLUDE_vTaskDelav	                   1
#define INCLUDE_uxTaskGetStackHighWaterMark        0 / Do not use this option on the PC port. */
#endif  /* FREERTOSCONFIG.H */

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

На этом подготовительный этап можно считать завершенным. Как говорилось в [1], при создании задачи с помощью API-функции xTaskCreate() есть возможность передать в функцию, реализующую задачу, произвольный параметр.

Разработаем учебную программу № 1, которая будет создавать два экземпляра одной задачи. Чтобы каждый экземпляр задачи выполнял уникальное действие, передадим в качестве параметра строку симво­лов и значение периода, которое будет сигнализировать о том, что задача выполнена. Для этого следует отредактировать файл main.c:

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "FreeRTOS.h"
#include "task.h"
/* Структура, содержащая передаваемую в задачу информацию */
typedef struct TaskParam_t {
 char	string[32]; /* строка */
long	period; /* период */
} TaskParam;
/* Объявление двух структур TaskParam 7 TaskParam хТР1,хТР2;*/
/* Функция, реализующая задачу */
void vTask( void *pvParameters) {
volatile long ul;
volatile TaskParam *pxTaskParam;
/*Прео6разованне типа void* к типу TaskParam*/
pxTaskParam = (TaskParam *) pvParameters;
for(;;)
{
/* Вывести на экран строку, переданную в качестве параметра при создании задачи */
 printf(string); /*здесь будет вывод на экран*/
}
_delay_ms(pxTaskParam-&gt;period); /*задержка, чтобы увидеть*/
vTaskDelete( NULL);
}
/* Точка входа. С функции main( ) начнется выполнение программы. */
short main( void ) (
/*Заполнение нолей структуры, передаваемой Задаче 1 */
 strcpy(xTPl.string, "Task I is running");
 xTP 1. period = 10000000L;
 /*Заполнение нолей структуры, передаваемой Задаче 2 */
 strcpy(xTP2.string, "Task 2 is running");
 xTP2.period = 30000000L;
/*Создание Задачи 1. Передача ей в качестве параметра указателя на структуру хТР1 */
 xTaskCreate( vTask, /* Функция, реализующая задачу */
            (signed char *) "Task 1",
            configMINIMAL_STACK_SIZE,
            (void*)&amp;xTPl,1,/* Передача параметра */,
            NULL);
/* Создание Задачи 2. Передача ей указателя на структуру хТР2 */
xTaskCreate( vTask,
           ( signed char *) "Task 2",
           configMINIMAL_STACK_SIZE,
           (void*)&amp;xTP2,1, NULL);
/* Запуск планировщика */
 vTaskStartScheduler();
return 1;
}

Задача 2 выводит сообщение о своей работе в три раза реже, чем Задача 1. Это объясняется тем, что в Задачу 2 было передано значение периода в 3 раза большее, чем в Задачу 1. Таким образом, передача различных параметров в задачи при их создании позволила добиться различной функциональности отдельных экземпляров одной задачи.

Приоритеты задач

При создании задачи ей назначается приоритет. Приоритет за­дается с помощью параметра uxPriority функции xTaskCreate(). Максимальное количество возможных приоритетов определяется макроопределением configMAX_PRIORITIES в заголовочном файле FreeRTOSConfig.h. В целях экономии ОЗУ необходимо задавать наи­меньшее, но достаточное значение configMAX_PRIORITIES. Нулевое значение приоритета соответствует наиболее низкому приоритету, значение (configMAX_PRIORITIES-l) — наиболее высокому (в ОС семейства Windows наоборот — приоритет 0 наивысший).

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

Этот пример показывает необходимость пользоваться приорите­тами осмотрительно, так как никакого алгоритма старения в плани­ровщике не предусмотрено (как в ОС общего назначения). Поэтомувозможна ситуация «зависания» задачи с низким приоритетом, кото­рая никогда не выполнится. Программисту необходимо тщательно проектировать прикладные программы и благоразумно задавать уровни приоритетов, чтобы избежать такой ситуации. Далее будет показано, как избежать «зависания» низкоприоритетных задач, используя механизм событий для управления ходом их выполнения.

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

Подсистема времени FreeRTOS

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

configTICK_RATE_HZ определяет частоту отсчета системных квантов в герцах, например значение configTICK_RATE_HZ, равное 100 (Гц), определяет продолжительность системного кванта, равную 10 мс. Следует отметить, что в большинстве демонстрационных про­ектов продолжительность системного кванта устанавливается равной 1 мс (configTICK_RATE_HZ = 1000).

Все API-функции, связанные с измерением временных интерва­лов, в качестве единицы измерения времени используют системный квант. Используя макроопределение portTICK_RATE_MS, можно получить продолжительность системного кванта в миллисекундах. Но для задания длительности кванта нужно использовать макро­определение configTICK_RATE_HZ.

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

События как способ управления выполнением задач

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

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

Гораздо эффективнее управлять выполнением задач с помощью событий. Управляемая событием задача выполняется только после того, как некоторое событие произошло. Если событие не произошло и задача ожидает его наступления, то она НЕ находится в состоя­нии ГОТОВНОСТИ к выполнению, а следовательно, не может быть выполнена планировщиком. Планировщик распределяет процес­сорное время только между задачами, ГО ТОВЫМИ к выполнению. Таким образом, если высокоприоритетная задача ожидает наступле­ния некоторого события, то есть не находится в состоянии готов­ности к выполнению, то планировщик отдаст управление готовой к выполнению более низкоприоритетной задаче.

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

Блокированное состояние задачи

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

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

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

Во FreeRTOS есть возможность заблокировать задачу, заставив ее ожидать события синхронизации, но определить при этом тайм-аут ожидания. То есть выход задачи из блокированного состояния возмо­жен как при наступлении события синхронизации, так и но проше­ствии времени тайм-аута, если событие синхронизации так и не про­изошло. Например, задача ожидает появления данных из очереди. Тайм-аут при этом установлен равным 10 мс. В этом случае выход задачи из блокированного состояния возможен при выполнении двух условий:

  • Данные в очередь поступили.
  • Данные не поступили, но вышло время тайм-аута, равное 10 мс.
  • Реализация задержек с помощью API-функции vTaskDelay(). Вернемся к рассмотрению учебной программы № 1. Задачи в этой программе выполняли полезное действие (в нашем случае — мигание светодиодом), после чего ожидали определенный про­межуток времени, то есть выполняли задержку на какое-то время. Реализация задержки в виде пустого цикла не эффективна. Один из основных недостатков мы продемонстрировали, когда задачам был назначен разный приоритет. А именно, когда высокоприоритет­ная задача все время остается в состоянии готовности к выполнению (не переходит ни в блокированное, ни в приостановленное состоя­ние), она поглощает все процессорное время, и низкоприоритетные задачи никогда не выполняются.Для корректной реализации задержек средствами FreeR TOS следует применять API-функцию vTaskDelay(), которая переводит задачу, вызывающую эту функцию, в блокированное состояние на заданное количество квантов времени. Ее прототип:    void vTaskDelay( portTickType xTicksToDelay );    Единственным аргументом является xTicksToDelay, который непо­средственно задает количество квантов времени задержкиНапример, пусть задача вызвала функ­цию xTicksToDelay(100) в момент времени, когда счетчик квантов был равен 5000. Задача сразу же блокируется, планировщик отдаст управление другой задаче, а вызывающая за­дача вернется в состояние готовности к вы­полнению, только когда счетчик квантов до­стигнет значения 5000. В течение времени, пока счетчик квантов будет увеличиваться от 5000 до 5100, планировщик будет выпол­нять другие задачи, в том числе задачи с бо­лее низким приоритетом.Следует отметить, что программисту нет необходимости отслеживать переполнение счетчика квантов времени. API-функции, связанные с отсчетом времени (в том числе и vTaskDelay(), берут эту обязанность на себя.

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

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

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

    Если стоит цель обеспечить циклическое выполнение с точно за­данным периодом dt3, то необходимо знать время выполнения тела задачи dtl, чтобы скорректировать величину задержки dtl. Это созда­ет дополнительные сложности.

    Для таких целей предназначена API-функция vTaskDelayUntil(). Программист в качестве ее параметра задает период dT, который от­считывается с момента t1 — момента выхода задачи из блокирован­ного состояния . Прототип функции vTaskDelayUntil()

    void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimelncrement);

    Функции vTaskDelayUntil() передаются следующие аргументы:

    1. pxPreviousWakeTime — указатель на переменную, в которой хра­нится значение счетчика квантов в момент последнего выхода за­дачи из блокированного состояния (момент времени Г] на рис. ). Этот момент используется как отправная точка для отсчета вре­мени, на которое задача переходит в блокированное состояние. Переменная, на которую ссылается указатель pxPreviousWakeTime, автоматически обновляется функцией vTaskDelayUntil(), поэтому при типичном использовании эта переменная не должна моди­фицироваться в теле задачи. Исключение составляет начальная инициализация, как показано в примере ниже.
    2. xTimelncrement— непосредственно задает период выполнения задачи. Задается в квантах; для задания в миллисекундах может использоваться макроопределение portTICK_RATE_MS.

    Типичное применение API-функции vTaskDelayUntil() в теле функции, реализующей задачу:

     

/* Функция задачи, которая будет циклически выполняться с жестко заданным периодом в 50 мс */
void vTaskFunction( void *pvParameters)
{
/* Переменная, которая будет хранить значение счетчика квантов в момент выхода задачи нз
блокированного состояния */
portTickType xLastWakeTime;
/* Переменная xLastWakeTime нуждается в инициализации текущим значением счетчика квантов.
Это единственный случай, когда ее значение задается явно.
В дальнейшем ее значение будет автоматически модифицироваться API-функцией vTaskDelayUntfl(). */
 xLastWakeTime = xTaskGetTickCount(); /* Бесконечный цикл */
 for(;;)
{
/* Какая-либо полезная работа */
/* Период выполнения этой задачи составит 50 мс.
Разделив это значение на кол-во миллисекунд в 1 кванте portTlCK_RATE_.MS,
 получим кол-во квантов периода, что и является аргументом vTaskDelayUntilO.
Переменная xLastWakeTime автоматически модифицируется внутри vTaskDelayUntilO,
поэтому нет необходимости делать это в явном виде. */
 vTaskDelayUntil( &amp;xlastWakeTime, ( 50 / portTICK_RATE_MS ) );
}
}

Задача Бездействие

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

В этом случае МК будет выполнять задачу Бездействие (Idle task). Задача Бездействие создается автоматически при запуске плани­ровщика API-функцией vTaskStartScheduler(). Задача Бездействие постоянно находится в состоянии готовности к выполнению. Ее приоритет задается макроопределением tskIDLE_PRIORITY как самый низкий в программе (обычно 0). Это гарантирует, что задача Бездействие не будет выполняться, пока в программе есть хотя бы одна задача в состоянии готовности к выполнению. Как только поя­вится любая готовая к выполнению задача, задача Бездействие будет вытеснена ею.

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

Добавление своей функциональности в функцию задачи Бездейст­вие окажется полезным в следующих случаях:

  1. Для реализации низкоприоритетных фоновых задач.
  2. Для измерения резерва МК по производительности. Во время вы­полнения задачи Бездействие процессор не занят полезной рабо­той, то есть простаивает. Отношение времени простоя процессора ко всему времени выполнения программы даст представление о ре­зерве процессора по производительности, то есть о возможности добавить дополнительные задачи в программу.
  3. Для снижения энергопотребления микроконтроллерного устрой­ства. Во многих МК есть возможность перехода в режим пони­женного энергопотребления для экономии электроэнергии. Это актуально, например, в случае проектирования устройства с ба­тарейным питанием. Выход из режима энергосбережения во многих МК возможен по прерыванию от таймера. Если настроить МК так, чтобы вход в режим пониженного энергопотребления происходил в теле функции задачи Бездействие, а выход — по прерыванию от того же таймера, что используется ядром FreeRTOS для форми­рования квантов времени, то это позволит значительно понизить энергопотребление устройства во время простоя процессора. Есть некоторые ограничения на реализацию функции задачи

Бездействие:

1. Задачу Бездействие нельзя пытаться перевести в блокированное или приостановленное состояние.

2. Если программа допускает использо­вание API-функции уничтожения за­дачи vTaskDelete(), то функция задачи Бездействие должна завершать свое вы­полнение в течение разумного периода времени. Это требование объясняется тем, что функция задачи Бездействие от­ветственна за освобождение ресурсов ядра после уничтожения задачи. Таким образом, временная задержка в теле функции задачи Бездействие приведет к такой же задержке в очистке ресурсов, связанных с уничто­женной задачей, и ресурсы ядра не будут освобождены вовремя. Чтобы задать свою функцию задачи Бездействие, необходимо в файле настрой­ки ядра FreeRTOSConifig.h задать макро­определение configUSE_IDLE_HOOK рав­ным 1. В одном из файлов исходного кода должна быть определена функция задачи Бездействие, которая имеет следующий про­тотип:

void vApplicationIdlelIook( void );

Значение configUSE_IDLE_HOOK, рав­ное 0, используется, когда не нужно добав­лять дополнительную функциональность.

Выводы

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

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

Leave a Reply

You must be logged in to post a comment.