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

Основы работы ОСРВ

Автор: Андрей Курниц (kurnits@stim.by). Журнал КиТ
Прежде чем говорить об особенностях FreeRTOS, следует остановиться на основных принципах работы любой ОСРВ и пояснить значение терминов, которые будут приме­няться в дальнейшем. Эта часть статьи будет особенно полезна читателям, которые не зна­комы с принципами, заложенными в ОСРВ.

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

Каждая выполняющаяся программа пред­ставляет собой задачу (Task). Если ОС позволя­ет одновременно выполнять множество задач, она является мультизадачной (Multitasking).

Большинство процессоров могут выпол­нять только одну задачу в один момент вре­мени. Однако при помощи быстрого пере­ключения между задачами достигается эф­фект параллельного выполнения всех задач. На рис. 1 показано истинно параллельное выполнение трех задач. В реальном же про­цессоре при работе ОСРВ выполнение задач носит периодический характер: каждая за­дача выполняется определенное время, после чего процессор «переключается» на следую­щую задачу:

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

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

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

Кроме того, что выполнение задачи может быть приостановлено планировщиком при­нудительно, задача может сама приостано­вить свое выполнение. Это происходит в двух случаях. Первый — это когда задача «хочет» задержать свое выполнение на определенный промежуток времени (в таком случае она пере­ходит в состояние сна (sleep)). Второй — когда задача ожидает освобождения какого-либо аппаратного ресурса (например, последова­тельного порта) или наступления какого-то события (event), в этом случае говорят, что за­дача блокирована (block). Блокированная или «спящая» задача не нуждается в процессорном времени до наступления соответствующего события или истечения определенного интер­вала времени. Функции измерения интерва­лов времени и обслуживания событий берет на себя ядро ОСРВ

Пример перехода задачи в блокированное состояние :

Задача 1 исполняется на протяжении определенного времени (1). В момент вре­мени (2) планировщик приостанавливает задачу 1 и возобновляет выполнение зада­чи 2 (момент времени (3)). Во время свое­го выполнения (4) задача 2 захватывает определенный аппаратный ресурс для свое­го единоличного использования. В момент времени (5) планировщик приостанавливает задачу 2 и восстанавливает задачу 3 (6). Задача 3 пытается получить доступ к тому же самому аппаратному ресурсу, который занят задачей 2. В результате чего задача 3 блокиру­ется — момент времени (7). Через некоторое время управление снова получает задача 2, которая завершает работу с аппаратным ре­сурсом и освобождает его (9). Когда управле­ние получает задача 3, она обнаруживает, что аппаратный ресурс свободен, захватывает его и выполняется до того момента, пока не бу­дет приостановлена планировщиком (10).

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

Задача «не знает», когда ядро ОСРВ прио­становит ее выполнение или, наоборот, воз­обновит.

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

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

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

Подводя итог, можно выделить три основ­ные функции ядра любой ОСРВ:

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

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

Соглашения о типах данных и именах идентификаторов

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

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

Для переменных префикс определяет тип переменной согласно таблице 1.

Например, ulMemCheck — переменная типа unsigned long, pxCreatedTask — пере­менная типа «указатель на структуру».

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

Следом за префиксом функции следу­ет имя модуля (файла с исходным кодом), в котором она определена. Например, vTaskStartScheduler() — функция, возвра­щающая тип void, которая определена в фай­ле task.c, uxQueueMessagesWaiting() — воз­вращает некий беззнаковый целочисленный тип, определена в файле queue, с.

Префикс переменной Ее тип
с char
s short
l long
f float
d double
v void
е Перечисляемый тип (enum)
x Структуры (struct) и др. типы
p Указатель (дополнительно к вышеперечисленным)
u Беззнаковый (дополнительно к вышеперечисленным)

Встроенные типы данных (short, char и т.д.) не используются в исходном коде ядра. Вместо этого используется набор специальных типов, которые определены индивидуально для каж­дого порта в файле portmacro.h и начинаются с префикса port. Список специальных типов FreeRTOS приведен в таблице

Специальные типы FreeRTOS

Специальный тип FreeRTOS

Соответствующий встроенный тип

portCHAR

char

portSHORT

short

portlONG

long

portTickType

Тип счетчика системных квантов

portBASE_TYPE

Наиболее употребительный тип во FreeRTOS

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

Подробнее следует остановиться на типах portTickType и portBASE_TYPE:

1. portTickType может быть целым беззна­ковым 16- или 32-битным. Он определяет тип системной переменной, которая ис­пользуется для подсчета количества си­стемных квантов, прошедших с момента старта планировщика. Таким образом, portTickType задает максимальный вре­менной интервал, который может быть отсчитан средствами FreeRTOS. В случае 16-битного portTickType максимальный интервал составляет 65536 квантов, в слу­чае 32-битного — 4 294 967 296 квантов. Использование 16-битного счетчика кван­тов оправдано на 8- и 16-битных платфор­мах, так как позволяет значительно повы­сить их быстродействие.

2. portBASE_TYPE определяет тип, актив­но используемый в коде ядра FreeRTOS. Операции с типом portBASE должны вы­полняться как можно более эффективно на данном МК, поэтому разрядность типа portBASE_TYPE устанавливается идентич­ной разрядности целевого МК. Например, для 8-битных МК это будет char, для 16-битных — short.

Идентификаторы макроопределений так­же начинаются с префикса, который опреде­ляет, в каком файле этот макрос находится:

Префикс Где определен Пример макроопределения
port portable.h portMAX_DELAY
tsk,task task.h taskENTER_CRITICAL()
pd projdefs.h pdTRUE
config FreeRTOSConfig.h configUSE_PREEMPTION
err projdefs.h errQUEUE_FULL

Задачи

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

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

Подробно рассмотрим состояния зада­чи в FreeRTOS. Говорят, что задача выпол­няется (running), если в данный момент времени процессор занят ее выполнением. Состояние готовности (ready) характеризу­ет задачу, готовую к выполнению, но не вы­полняющуюся, так как в данный момент вре­мени процессор занят выполнением другой задачи. Готовые к выполнению задачи (с оди­наковым приоритетом) но очереди переходят в состояние выполнения и пребывают в нем в течение одного системного кванта, после чего возвращаются в состояние готовности.

Задача находится в блокированном состоя­нии, если она ожидает наступления временно­го или внешнего события (event). Например, вызвав API-функцию vTaskDelay(), задача переведет себя в блокированное состояние до тех пор, пока не пройдет временной период задержки (delay): это будет временное событие. Задача блокирована, если она ожидает собы­тия, связанного с другими объектами ядра — очередями и семафорами: это будет внешнее (по отношению к задаче) событие.

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

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

В любой программе реального вре­мени есть как менее, так и более ответствен­ные задачи. Под «ответственностью» задачи здесь понимается время реакции программы на внешнее событие, которое обрабатывает­ся задачей. Например, ко времени реакции на срабатывание датчика в производственной установке предъявляются куда более строгие требования, чем ко времени реакции на на­жатие клавиши на клавиатуре. Для обеспече­ния преимущества на выполнение более от­ветственных задач во FreeRTOS применяется механизм приоритетов задач (Task priorities).

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

Каждой задаче назначается приоритет от 0 до {configMAX_PRIORITIES — 1). Меньшее значение приоритета соответствует меньшему приоритету. Наиболее низкий приоритет у за­дачи «бездействие», значение которого опреде­лено в tskIDLE_PRIORITY как 0. Изменяя зна­чение configMAX_PRIORITlES, можно опре­делить любое число возможных приоритетов, однако уменьшение configMAX_PRIORITIES позволяет уменьшить объем ОЗУ, потребляе­мый ядром.

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

void ATaskFunction ( void *pvParameters )
{
/* Переменные могут быть объявлены здесь, как и в обычной функции.
Каждый экземпляр этой задачи будет иметь свою собственную копню переменной iVariableExample.
Если объявить переменную со спецификатором static, то будет создана только одна переменная
 iVariableExample,доступная из всех экземпляров задачи */
int iVariableExample = 0;
/* Тело задачи реализовано как бесконечный цикл */
for(;;)
{
/* Код, реализующий функциональность задачи */
}
/* Если все-таки произойдет выход из бесконечного цикла, то задача должна быть уничтожена ДО конца функции.
 Параметр NULL обозначает, что уничтожается задача, вызывающая API-функцию vTaskDeleteO */
vTaskDelete( NULL);
}

Задачи создаются API-функцией xTaskCreate(), а уничтожаются xTaskDelete(). Функция xTaskCreate() является одной из наиболее сложных API-функций. Ее про­тотип:

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

xTaskCreate() в случае успешного созда­ния задачи возвращает pdTRUE. Если же объема памяти кучи недостаточно для раз­мещения служебных структур данных и стека задачи, то xTaskCreate() возвращает errCOULD_NOT_ALLOCATE_REQUIRED_ MEMORY. Функции xTaskCreate() передают­ся следующие аргументы:

  1. pvTaskCode — указатель на функцию, реа­лизующую задачу (фактически — иденти­фикатор функции в программе).
  2. pcName — нуль-терминальная (заканчива­ющаяся нулем) строка, определяющая имя функции. Ядром не используется, а служит лишь для наглядности при отладке.
  3. usStackDepth — глубина (размер) собствен­ного стека создаваемой задачи. Размер зада­ется в словах, хранящихся в стеке, а не в бай­тах. Например, если стек хранит 32-битные слова, а значение usStackDepth задано рав­ным 100, то для размещения стека задачи будет выделено 4x100 = 400 байт. Размер стека в байтах не должен превышать мак­симального значения для типа size_t. Размер стека, необходимый для корректной работы задачи, которая ничего не делает (содержит только пустой бесконечный цикл, как за­дача ATaskFunction выше), задается макро­сом соnfigMINIMAL_STACKJSIZE. Не ре­комендуется создавать задачи с меньшим размером стека. Если же задача потребляет большие объемы стека, то необходимо за­дать большее значение usStackDepth. Нет простого способа определить размер сте­ка, необходимого задаче. Хотя возможен точный расчет, большинство програм­мистов находят золотую середину между требованиями выделения достаточного размера стека и эффективного расхода па­мяти. Существуют встроенные механизмы экспериментальной оценки объема ис­пользуемого стека, например API-функция uxTaskGetStackHighWaterMark(). О воз­можностях контроля переполнения стека будет рассказано позже.
  4. pvParameters— произвольный параметр, передаваемый задаче при ее создании. Задается в виде указателя на void, в теле за­дачи может быть преобразован в указатель на любой другой тип. Передача параметра оказывается полезной возможностью при создании нескольких экземпляров одной задачи.
  5. uxPriority — определяет приоритет соз­даваемой задачи. Нуль соответствует са­мому низкому приоритету, (cotifigMAX_ PRIORITIES — 1)— наивысшему. Значение аргумента uxPriority большее, чем {configMAX_PRIORITIES— 1), при­ведет к назначению задаче приоритета (configMAX_PRlORITIES — 1).
  6. pxCreatedTask — может использоваться для получения дескриптора (handle) создавае­мой задачи, который помещается по адресу pxCreatedTask после успешного создания задачи. Дескриптор можно использовать в дальнейшем для различных операций над задачей, например изменения приори­тета задачи или ее уничтожения. Если в по­лучении дескриптора нет необходимости, то pxCreatedTask должен быть установлен в NULL.

По сложившейся традиции первая про­грамма в учебнике по любому языку про­граммирования для компьютеров выводит на экран монитора фразу "Hello, world!", но в случае МК мигают светодиодом :)

Что ж, пришло время написать первую программу под управлением FreeRTOS. Программа будет содержать две задачи. Задача 1 будет переключать логический уро­вень на одном выводе МК, задача 2 — на дру­гом. Частота переключения для разных вы­водов будет разной.

В качестве аппаратной платформы бу­дет использоваться МК AVR ATmega128L, установленный на мезонинный модуль WIZ200WEB фирмы WIZnet (рис. 6) [7]. Как отправная точка будет взят демонстрацион­ный проект, компилятор — WinAVR, версия 2010.01.10.

Прежде всего необходимо загрузить и установить компилятор WinAVR [8]. Далее с официального сайта загрузить дистри­бутив FreeRTOS и распаковать в удобное ме­сто (в статье это С:/).

Демонстрационный проект распола­гается в C:/FreeRTOSV6.1.0/Demo/AVR_ ATMega323_WinAVR/ и предназначен для выполнения на МК ATmega323. Файл makefile, находящийся в директории про­екта, содержит все настройки и правила компиляции и, в том числе, определяет, для какого МК компилируется проект. Для того чтобы целевой платформой стал МК ATmegal28, необходимо в файле makefile отыскать строку:

MCU = atmega323

и заменить ее на

MCU = atmegal28

Подготовительный этап закончен. Теперь можно переходить к редактированию фай­ла main.c. Его содержимое должно принять вид:

#include
#include
#ifdef GCC_MEGA_AVR /* EEPROM routines used only with the WinAVR compiler. */
#include
#endif
/* Необходимые файлы ядра */
#include "FreeRTOS.h"
#include "task.h"
#include "croutine.h"
/* Функция задачи 1*/
void vTask1( void *pvParameters)
{
/* Квалификатор volatile запрещает оптимизацию  переменной ul */
 volatile unsigned long ul;
/* Как и большинство задач, эта задача содержит  бесконечный цикл */
 for(;;) {
/* Инвертировать бит 0 порта PORTF */
 PORTF^ = (1 << PF0); /* Задержка на некоторый период Т1*/
for( ul = 0; ul < 4000L; ul++ )
{
/* Это очень примитивная реализация задержки, в дальнейших примерах будут использоваться  API-функции */
}
}
/* Уничтожить задачу, если произошел выход из бесконечного цикла (в данной реализации выход заведомо не произойдет*/
 vTaskDelete( NULL);
}
 
/* Функция задачи 2*/
void vTask2( void *pvParameters)
{
/* Квалификатор volatile запрещает оптимизацию  переменной ul */
 volatile unsigned long ul;
/* Как и большинство задач, эта задача содержит  бесконечный цикл */
 for(;;) {
/* Инвертировать бит 1 порта PORTF */
 PORTF^ = (1 << PF1); /* Задержка на некоторый период Т1*/
for( ul = 0; ul < 8000L; ul++ )
{
/* Это очень примитивная реализация задержки, в дальнейших примерах будут использоваться  API-функции */
}
}
/* Уничтожить задачу, если произошел выход из бесконечного цикла (в данной реализации выход заведомо не произойдет*/
 vTaskDelete( NULL);
}
 
short main( void)
/* Биты 0, 1 порта PORTF будут работать как ВЫХОДЫ */
DDRF|= (1 << PF0) | (1 << PF1);
/* Создать задачу 1, заметьте, что реальная программа должна  проверять возвращаемое значение, чтобы убедиться,  что задача создана успешно*/
 xTaskCreate(vTask 1, /* Указатель на функцию,  реализующую задачу */
 (signed char *) "Task 1", /*Текстовое имя задачи. Только для наглядности*/
 configMINIMAL_STACK_SIZE, /* Размер стека- минимально необходимый*/
 NULL, /* Параметр, передаваемый задаче, - не используется */
 1,	/*Приоритет = 1 */
 NULL); /* Получение дескр!штора задачи - не используется */ 
 
 /* Создать задачу 2 */
xTaskCreate( vTask2, ( signed char * ) "vTask2", configMINIMAL_STACK_SIZE, NULL, 1, NULL );
/* Запустить планировщик. Задачи начнут выполняться. */
 vTaskStartScheduler();
return 0;
}

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

Пока выполняется задача 1, она увеличи­вает свой счетчик ul. Когда планировщик переводит задачу 1 в состояние готовности, переменная ul сохраняется в собственном стеке задачи 1 и не увеличивается, пока вы­полняется задача 2. Как только переменная ul достигает значения 4000, она обнуляется (момент времени tl), а логический уровень на выводе PF0 инвертируется, однако это мо­жет произойти только в течение кванта вре­мени выполнения задачи 1. Аналогично ведет себя задача 2, но ее счетчик обнуляется но до­стижении значения 8000. Таким образом, эта простейшая программа генерирует меандр с «плавающим» полупериодом, а разброс продолжительности полупериода достигает одного системного кванта, то есть 1 мс.

Выводы:

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

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

Leave a Reply

You must be logged in to post a comment.