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



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

Необходимость использования очередей

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

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

Продемонстрировать этот эффект позво­ляет учебная программа, в которой объ­явлена глобальная переменная iVal и две за­дачи: задача, которая модифицирует общую переменную,—vModifyTask(), и задача, кото­рая проверяет значение этой переменной,— vCheckTask(). Модификация производится так, чтобы итоговое значение глобальной переменной после окончания вычислений не изменялось. В случае если значение общей переменной отличается от первоначального, задача vCheckTask() выдает соответствующее сообщение на экран.

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/*Глобальная переменная, доступ к которым будет осуществляться из нескольких задач */
long IVal = 100;
/* Функция, реализующая задачи, которая модифицирует глобальную переменную */
void vModiifyTask(void *pvParameters) {
/*Бесконечный цикл */
for (;;) {
/* Модифицировать переменную IVal так, чтобы tе значение не изменилось */
IVal += 10; IVal -= 10;
}
}
/* Функция, реализующая задачу, которая проверяет значение переменной */
void vCheckTask(void *рvParameters) {
/*Бесконечный цикл */
for (;;) {
if (IVal != 100) {
puts("Variable IVal is not 100!");
}
vTaskDelay(lOO);
}
/* Точка входа. С функции mainO начнется выполнение  программы. */
 int main(void) {
/*Создать задачи с равным приоритетом */
xTaskCreate(vModifyTask,"Modifу", 1000, NULL, 1, NULL);
xTaskCreate( vCheckTask, "Check", 1000, NULL, 1, NULL);
/* Запуск планировщика. Задачи начнут выполняться. */
 vTaskStartScheduler();
 for {;;);
}

Результаты работы показывают, что значение глобальной переменной часто оказывается не равным ожидаемому (100).

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

Характеристики очередей

Хранение информации в очереди

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

Элементы очереди в контексте обмена ин­формацией между задачами будем называть сообщениями.

Запись элемента в очередь приводит к со­зданию побайтовой копии элемента в очереди. Побайтовое копирование происходит и при чтении элемента из очереди.

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

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

Очередь функционирует по принципу «первым вошел — первым вышел» (First In First Out, FIFO), то есть элемент, который раньше остальных был помещен в очередь (в конец очереди), будет и прочитан раньше остальных (рис.). Обычно элементы запи­сываются в конец («хвост») очереди и считываются с начала («головы») очереди.

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

На рис. а Задача 1 записывает число «15» в конец очереди. Так как теперь очередь со­держит I элемент, то он является одновре­менно и началом, и концом очереди.

На рис. в Задача 1 записывает еще один элемент («69») в конец очереди. Теперь оче­редь содержит 2 элемента, причем элемент «15» находится в начале очереди, а элемент «69» — в конце.

На рис. г Задача 2 считывает элемент, на­ходящийся в начале очереди, то есть элемент «15». Таким образом, выполняется принцип «первым вошел — первым вышел», так как элемент«15» первым записан в очередь и, со­ответственно, первым из нее считан. Теперь очередь снова содержит один элемент («69») в начале очереди, который и будет считан при следующем чтении из очереди Задачей 2 (рис. 2д).Следует отметить, что на рис. 2 показано использование API-функций для работы с очередями в упрощенном виде. Корректное их применение будет описано ниже.

Также имеется возможность помешать элементы в начало очереди, тогда очередь превращается в стек, работающий по прин­ципу «последним вошел — первым вышел» (LastInFirstOut, LIFO).

Доступ из множества задач

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

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

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

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

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

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

Блокировка при записи в очередь

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

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

Работа с очередями

Создание очереди

Очередь должна быть явно создана перед первым ее использованием. API-функция xQueueCreate() служит для создания оче­реди, она возвращает переменную типа xQueueHandle в случае успешного создания очереди:

xQueueHandle xQueueCreate ( unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxIemSize);

Еe параметры и возвращаемое значение:

  • uxQueueLength — определяет размер оче­реди, то есть максимальное количество элементов, которые может хранить оче­редь.
  • uxItemSize — задает размер одного эле­мента очереди в байтах, его легко получить с помощью оператора sizeof().
  • Возвращаемое значение — дескриптор оче­реди. Равен NULL если очередь не создана по причине отсутствия достаточного объема памяти в куче FreeRTOS. Ненулевое значе­ние свидетельствует об успешном создании очереди, в этом случае оно должно быть со­хранено в переменной типа xQueueHandle для дальнейшего обращения к очереди. При создании очереди ядро FreeRTOS выде­ляет блок памяти из кучи для ее размещения. Этот блок памяти используется как для хра­нения элементов очереди, так и для хранения служебной структуры управления очередью, которая представлена структурой xQUEUE.

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

sizeof(xQUEUE);

При этом следует учесть, что структура xQUEUE используется ядром в собствен­ных целях и доступа к этой структуре из тек­ста прикладных исходных файлов {main.с в том числе) изначально нет. Чтобы полу­чить доступ к структуре xQUEUE, необходи­мо включить в исходный файл строку:

«include "..\\queue.c"

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

Для записи элемента в конец очереди используется API-функция xQueueSendToBack() для записи элемента в начало очереди — xQueueSendToFront(). Так как запись в конец очереди применяет­ся гораздо чаще, чем в начало, то вызов API- функции xQueueSend() эквивалентен вызо­ву xQueueSendToBack(). Прототипы у всех трех API-функций одинаковы:

portBASE_TYPE xQueueSendXXXX (xQueueHandle xQueue, const void * pvIiemToQueue, portTickType xTicksToWait);

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

  • xQueue — дескриптор очереди, в которую будет записан элемент. Дескриптор очере­ди может быть получен при ее создании API-функцией xQueueCreate().
  • pvItemToQueue — указатель на элемент, который будет записан в очередь. Размер элемента зафиксирован при создании оче­реди, так что для побайтового копирова­ния элемента достаточно иметь указатель на него.
  • xTicksToWait — максимальное количество квантов времени, в течение которого задача может пребывать в блокированном состоя­нии, если очередь полна и записать новый элемент невозможно. Для представления времени в миллисекундах следует исполь­зовать макроопределение portTICK_RATE_ MS. Задание xTicksToWait равным «О» приведет к тому, что задача не перейдет в блокированное состояние, если очередь полна, а продолжит свое вы­полнение. Установка xTicksToWait рав­ным константе portMAX_DELAY приведет к тому, что выхода из блокированного со­стояния по истечении тайм-аута не будет. Задача будет сколь угодно долго «ожидать» возможности записать элемент в очередь, пока такая возможность не появится. При этом макроопределение INCLUDE_ vTaskSuspend в файле FreeRTOSConfig.h должно быть равно «1».
  • Возвращаемое значение — может возвра­щать 2 значения:

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

- errQUEUE_FULL — означает, что дан­ные не записаны в очередь, так как очередь заполнена. Если определена продолжительность тайм-аута (пара­метр xTicksToWait не равен «О» или portMAX_DELAY), то возврат значения errQUEUE_FULL говорит о том, что тайм-аут завершен и свободное место в очереди так и не появилось.

Следует отметить, что API-функции xQueueSendToBack() и xQueueSendToFront() нельзя вызывать из тела обработ­чика прерывания. Для этой цели служат специальные версии этих API-функций — xQueueSendToBackFromlSR ()и xQueueSendToFrontFromISR() соответственно. Более подробно об использовании API-функций FreeRTOS в теле обработчика прерывания бу­дет рассказано в дальнейших публикациях.

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

Чтение элемента из очереди может быть произведено двумя способами:

  • Элемент считывается из очереди (создается его побайтовая копия в другую перемен­ную), после чего он удаляется из очереди. Именно такой способ считывания проде­монстрирован на рис. 2.
  • Создается побайтовая копия элемента, при этом элемент из очереди не удаляется. Для считывания элемента с удалением его из очереди используется API-функция xQueueReceive(). Ее прототип:

portBASE_TYPE xQueueReceive(xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait);

Для считывания элемента из очереди без его удаления используется API-функция xQueuePeek(). Не прототип:

portBASE_TYPE xQueuePeek(xQueueHandle xQueue,  const void* pvBuffer,  portTickType xTicksToWait);

Назначение параметров и возвращаемое значение для API-функций xQueue Receive () и xQueuePeek() одинаковы:

  • xQueue — дескриптор очереди, из которой будет прочитан элемент. Дескриптор оче­реди может быть получен при ее создании API- фу н кцией xQueueCreate().
  • pvBuffer— указатель на область памя­ти, в которую будет скопирован элемент из очереди. Объем памяти, на которую ссылается указатель, должен быть не мень­ше размера одного элемента очереди.
  • xTicksToWait— максимальное количе­ство квантов времени, в течение которого задача может пребывать в блокирован­ном состоянии, если очередь не содержит ни одного элемента. Для представления вре­мени в миллисекундах следует использо­вать макроопределение portTICK_RATE_MS. Задание xTicksToWait равным «О» приведет к тому, что задача не перейдет в блокированное состояние, а продолжит свое выполнение, если очередь в данный момент пуста. Установка xTicksToWait рав­ным константе portMAX_DELAY приведет к тому, что выхода из блокированного со­стояния по истечении тайм-аута не будет. Задача будет сколь угодно долго «ожидать» появления элемента в очереди. При этом макроопределение INCLUDE_vTaskSuspend в файле FreeRTOSConfig.h должно быть равно«1».
  • Возвращаемое значение — может возвра­щать 2 значения:

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

- errQUEUE_EMPTY— означает, что элемент не прочитан из очереди, так как очередь пуста.

Если определена продолжительность тайм-аута (пара­метр xTicksToWait не равен «О» или portMAX_DELAY), то возврат значения errQUEUE_FULL говорит о том, что тайм-аут завершен и никакая другая за­дача или прерывание не записали эле­мент в очередь. Как и в случае с записью элемента в очередь, API функции xQueueReceive() и xQueuePeek() нельзя вызывать из тела об­работчика прерывания. Для этих целей слу­жит API-функция xQueueReceiveFromlSR(), которая будет описана в следующих публи­кациях.

Состояние очереди

Получить текущее количество записанных элементов в очереди можно с помощью API функции uxQueueMessagesWaiting():

unsigned portBASE_TYPE  uxQueueMessagesWaiting(xQueueHandle xQueue);

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

  • xQueue — дескриптор очереди, состояние которой необходимо получить. Дескриптор очереди может быть получен при ее созда­нии API-функцией xQueueCreate().
  • Возвращаемое значение — количество эле ментов, которые хранит очередь в момент вызова uxQueueMessagesWaiting(). Если очередь пуста, то возвращаемым значени­ем будет «О».

Как и в случаях с чтением и запи­сью элемента в очередь, API-функцию uxQueueMessagesWaiting() нельзя вызывать из тела обработчика прерывания. Для этих целей служит API-функция uxQueueMessagesWaitingFromlSR ().

Удаление очереди

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

void vQueueOeletel( xQueueHandle xQueue);

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

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

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/* Объявить переменную-дескриптор очереди. Эта переменная будет использоваться
	для работы с очередью из тела всех трех задач. */
xQueueHandle xQueue;
 
/* Функция, реализующая задачи-передатчики */
 void vSenderTask(void *pvParameters)  { /* Переменная, которая будет хранить значение, передаваемое в очередь */
 long IValueToSend;
/* Переменная, которая будет хранить результат выполнения xQueueSendToBack(); */
portBASE_TYPE xStatus;
/* Будет создано несколько экземпляров задачи. В качестве
	параметра задачи выступает число, которое задача будет
	записывать в очередь */
 IValueToSend = (long) pvParameters; /* Бесконечный цикл */
for (;;){
/* Записать число в конец очереди.
1 -й параметр — дескриптор очереди, в которую будет производиться запись,
 очередь создана до запуска  планировщика, и ее дескриптор сохранен в глобальной переменной xQueue.
2-й параметр — указатель на переменную, которая будет записана в очередь, в данном случае — IValueToSend.
3-й параметр — продолжительность тайм-аута.
4-й В данном случае задана равной 0, что соответствует отсутствию времени ожидания, если очередь полна.
 Однако из-за того, что задача-приемник сообщений имеет	более высокий приоритет, чем задач и-передатчики,
в очереди не может находиться более одного элемента.Таким образом, запись нового элемента будет всегда
возможна. */
xStatus = xQueucSendToBack( xQueue. &amp;lValueToSend,0);
if (xStatus != pdPASS) { /* Если попытка записи не была успешной —  индицировать ошибку. */
 puts("Could not send to the queue.\r\n");
}
/* Сделать принудительный вылов планировщика, позволив,  таким образом, выполняться другой задаче-передатчику.  Переключение на другую задачу произойдет быстрее,	чем окончится текущий квант времени. */ taskYIELD();
/* Функция, реализующая задачу-приемник */
 void vReceiverTask(void *pvParameters) { /* Переменная, которая будет хранить значение, полученное из очереди */
 long IReceivedValue;
/*Переменная, которая будет хранить результат выполнения */
xQueueReceive();
portBASE_TYPE xStatus; /* Бесконечный цикл */
for (;;) {
/*Индицировать состояние, когда очередь пуста */
 if (uxQueueMessagesWaiting(xQueue) != 0)  puts("Queue should have been emptv!\r\n");
/* Прочитать число из начала очереди. 1-й параметр — дескриптор очереди, из которой будет  происходить
чтение, очередь создана до запуска планировщика, и ее дескриптор сохранен в глобальной переменной xQueue.
2-й параметр — указатель на буфер, в который будет помешено число из очереди. В данном случае — указатель на переменную iReceivedValue.
3-й параметр — продолжительность тайм-аута, в течение которого задача будет находиться в блокированном
состоянии, пока очередь пуста. В данном случае	макроопределение portTICK_RATE_MS используется	для преобразования времени 100 мс в количество системных квантов. */
xStatus = xQueueReceive(xQueue, &amp;IReceivedValue, 100 / portTlCK_RATE_MS);
if (xStatus == pdPASS) /* Число успешно принято, вывести его на экран */
 { printf( "Received = %ld\r\n", IReceivedValue);}
else {
/* Данные не были прочитаны из очереди на протяжении тайм-аута 100 мс.	При условии наличия нескольких задач-передатчиков означает аварийную ситуацию */
puts("Could not receive from the queue.\r\n");
*/ Точка входа. С функции main() начнется выполнение  программы. */
 int main(void) { /*Создать очередь размером 5 элементов для хранения переменных типа long. Размер элемента установлен равным размеру переменной типа long.	Дескриптор созданной очереди сохранить в глобальной	переменной xQueue. •/
xQueue = xQucueCreatc

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

И момент времени (0) (рис. ) происходит запуск планировщика, который переводит в состояние выполнения задачу с наивысшим приоритетом — задачу-приемник. В момент времени (1) задача-приемник пытается прочи­тать элемент из очереди, однако очередь после создания пуста, и задача-приемник переходит в блокированное состояние до момента появ­ления данных в очереди либо до момента исте­чения тайм-аута 100 мс. В состояние выполне­ния переходит одна из задач-передатчиков, ка­кая именно— точно сказать нельзя, так как они имеют одинаковый приоритет, в нашем случае пусть это будет задача-передатчик 1. В момент времени (2) задача-передатчикзаписывает число 100 в очередь. В этот мо­мент выходит из блокированного состояния задача-приемник, так как она «ожидала» появ­ления данных в очереди и приоритет ее выше. Прочитав данные из очереди, она вновь бло­кируется, так как очередь снова пуста (момент времени (3)). Управление возвращается пре­рванной задаче-передатчику 1, которая вы­полняет API-функцию вызова планировщика taskYIELD() , в результате чего управление по­лучает равноприоритетная задача-передатчик 2 (момент времени (4)). Когда она записывает значение 200 в очередь, снова разблокируется высокоприоритетная задача-приемник - момент времени (5) и цикл повторяется.

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

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

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

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

На рис. изображена также структу­ра xData, которая выступает в данном случае типом элементов очереди. Структура содер­жит два целочисленных значения:

  • iMeaning — значение, смысл передаваемо­го через очередь параметра;
  • iValue — числовое значение параметра.

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

Задача обслуживания CAN-интерфейса отвечает за обработку входящих по CAN- пшне сообщений, декодирует их и посылает сообщение в виде структуры xData в задачу ПИД-регулятора. Значение члена структу­ры iMeaning «установка скорости» позволяет задаче ПИД-регулятора определить, что зна­чение iValue, равное 600, есть не что иное, как- новое значение уставки скорости вращения.

Задача обслуживания человеко-машин­ного интерфейса ответственна за взаимодей­ствие оператора с контроллером двигателя. Оператор может вводить значения параме­тров, давать команды контроллеру, наблюдать его текущее состояние. Когда оператор нажал кнопку аварийной остановки двигателя, за­дача обслуживания человеко-машинного ин­терфейса сформировала соответствующую структуру xData. Поле iMeaning указывает на нажатие оператором некоторой кноп­ки, а ноле iValue — уточняет какой именно: кнопки аварийного останова. Такого рода со­общения (связанные с возникновением ава­рийной ситуации) целесообразно помещать не в конец, а в начало очереди, так, чтобы за­дача ПИД-контроллера обработала их раньше остальных находящихся в очереди, сократив, таким образом, время реакции системы.

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/* Номера функций-передатчиков сообщений */
#define mainSENDER_l 1
#define mainSENDER_2 2
/• Объявить переменную-дескриптор очереди. Эта переменная будет использоваться для ссылки на очередь после ее создания. */
xQueueHandle xQueue;
/• Определить структуру, которая будет элементом очереди */
typedef struct
{
unsigned char ucValue;
unsigned char ucSource; } xData;
/* Определить массив из двух структур, которые будут записываться в очередь */
static const xData xStructsToSend[2] =
{
{100, mainSENDER_1}, /* Используется задачей-передатчиком */
{200, mainSENDER_2}  /* Используется задачей-передатчиком */
};
 
/* Функция, реализующая задачи-передатчики */
 void vSenderTask( void * pvParameters) { /* Будет создано несколько экземпляров задачи. В качестве
 параметра задаче будет передан указатель на структуру xData. */
/* Переменная, которая будет хранить результат выполнения  xQueueSendToBack()*/
 portBASE_TYPE xStatus;
/* Бесконечный цикл */
 for (;;) {
/* Записать структуру в конец очереди.
1-й параметр — дескриптор очереди, в которую будет производиться запись, очередь создана до запуска
планировщика, и ее дескриптор сохранен в глобальной переменной xQueue.
2-й параметр — указатель на структуру, которая будет записана и очередь, в данном случае указатель
передается при создании экземпляра задачи (pvParameters).
3-й параметр — продолжительность тайм-аута, в течение которою задача будет находиться в блокированном
состоянии, ожидая появления свободного места в очереди. Макроопределение portTICK_RATE_MS используется для
преобразования времени 100 мс в количество системных квантов. */
xStatus = xQueueSendToBack (xQueue, pvParameters, 100 / portTICK_RATE_MS);
if (xStatus != pdPASS) {
/* Запись в очередь не произошла по причине того, что очередь на протяжении тайм-аута оставалась
 заполненной. Такая ситуация свидетельствует об ошибке, так как	очередь-приемник создаст свободное место в
 очереди, как только обе задачи-передатчика перейдут в блокированное состояние */
puts("Could not send to the queue.\r\n");
)
/* Сделать принудительный вызов планировщика, позволив, таким образом, выполняться другой задаче-
передатчику. Переключение на другую задачу произойдет быстрее, чем окончится текущий квант времени. */ taskYIELD();
}
}
/* Функция, реализующая задачу-приемник */
 void vReceiverTask( void *pvParameters) { /*Структура, в которую будет копироваться прочитанная из очереди
структура */
xData хReceivedStructure; /*Переменная, которая будет хранить результат выполнения xQueueReceive() */ portBASE_TYPE xStatus; /* Бесконечный цикл */
 for (;;) {
/* Эта задача выполняется, только когда задачи-передатчики находятся в блокированном состоянии,
а за счет того, что приоритет у них выше, блокироваться они могут, только если очередь полна. Поэтому очередь в этот момент должна быть полна. То есть текущее количество элементов очереди должно быть равно ее размеру — 3*/
if (uxQueueMessagesWaiting(xQueue)!= 3) {
puts("Queue should have been full!\r\n");
)
/*Прочитать структуру из начала очереди.
1-й параметр — дескриптор очереди, из которой будет происходить чтение, очередь создана до запуска
планировщика, и ее дескриптор сохранен в глобальной переменной xQueue.
2-й параметр — указатель на буфер, в который будет скопирована структура из очереди. В данном случае —*	указатель на структуру xReceivedStructure.
3 й параметр — продолжительность тайм-аута. В данном случае задана равной 0, что означает задача не будет
"ожидать", если очередь пуста. Однако так как эта задача получает управление, только если очередь полна, то чтение	элемента из нее будет всегда возможно. */
xStatus = xQueueReceive(xQueue, &amp;xReceivedStructure, 0);
if (xStatus == pdPASS) { /* Структура успешно принята, вывести на экран название задачи, которая эту структуру поместила в очередь и значение абстрактного параметра */
if (xReceivedStructure.ucSource == mainSENDER_l) {
printf("From Sender 1 = %d\r\n", xReceivedStructure.ucValue); } else {
printf("From Sender 2 = %d\r\n", xReceivedStructure.ucValue);
}
} else {
/* Данные небыли прочитаны из очереди.
При условии, что очередь должна быть полна, означает аварийную ситуацию */
puts( "Could not receive from the queue.\r\n");
}
}
}
 
/* Точка входа. С функции main(} начнется выполнение программы. */
 int main(void) {
/*Создать очередь размером 3 элемента для хранения структуры типа xData.
Размер элемента установлен равным размеру структуры xData. Дескриптор созданной очереди сохранить в глобальной переменной xQueue. */
xQueue = xQueueCreate(3, sizeof(xData));
/* Если очередь успешно создана (дескриптор не равен NULL) */
if (xQueue != NULL) {
/*Создать 2 экземпляра задачи-передатчика. Параметр, передаваемый задаче при ее создании, указатель на структуру, которую экземпляр задачи-передатчика будет записывать в очередь.
Задача передатчик 1 будет постоянно записывать структуру xStructsToSend [0]
Задача-передатчик 2 будет постоянно записывать структуру xStructsToSend [1]
Обе задачи создаются с приоритетом 1.*/
xTaskCreate(vSenderTask, "Sender1", 1000, ( void * ) &amp;(xStructsToSend[0]), 2, NULL);
xTaskCreate(vSenderTask, "Sender2", 1000, ( void * ) &amp;(xStructsToSend[1]), 2, NULL);
/* Создать задачу-приемник, которая будет считывать числа из очереди.
Приоритет - 2, то есть выше, чем у задач-передатчиков.*/
xTaskCreate( vReceiverTask, "Receiver". 1000, NULL, 1, NULL);
/* Запуск планировщика. Задачи начнут выполняться. */
vTaskStart Scheduler();
} else {
/* Если очередь не создана */
}
/*При успешном создании очереди и запуске планировщика программа никогда "не дойдет" до этого места. */
for (;;);
}

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

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

Так как все задачи с приоритетом 2 теперь блокированы, управление получает задача- приемник, приоритет которой ниже и ра­вен «1» (момент времени (6)). Она считывает один элемент из очереди, освобождая таким образом место в очереди. Как только в оче­реди появилось свободное место, планиров­щик выведет из состояния блокировки ту за­дачу из числа «ожидавших», которая дольше остальных пребывала блокированной. В на­шем случае это задача-передатчик 2 (момент времени (7)). 'Гак как приоритет у нее выше, она вытеснит задачу-приемник и запишет следующий элемент в очередь. После чего она вызовет планировщик API-функцией taskYIELD(). Однако готовых к выполнению задач с более высоким или равным приори­тетом на этот момент нет, поэтому пере­ключения контекста не произойдет, и задача- передатчик 2 продолжит выполняться. Она попытается записать в очередь еще один элемент, но очередь заполнена, и задача- передатчик 2 перейдет в блокированное со­стояние (момент времени (8)).

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

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

Использование очередей для передачи больших объемов данных

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

Преимущества такого подхода:

  • Экономия памяти. Память при созда­нии очереди выделяется под все элемен­ты очереди, даже если очередь пуста. Использование небольших по объему за­нимаемой памяти указателей вместо объ­емных структур или массивов позволяет достичь существенной экономии памяти.
  • Меньшее время записи элемента в очередь и чтения его из очереди. При записи/чте­нии элемента из очереди происходит его побайтовое копирование. Копирование указателя выполняется быстрее копирова­ния объемных структур данных.

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

  • У памяти, адресуемой указателем, в каж­дый момент времени должна быть одна четко определенная задача-хозяин, ко­торая может обращаться к этой памя­ти. То есть необходимо гарантировать, что несколько задач не будут одновре­менно обращаться к памяти, на которую ссылается указатель. В идеальном случае только задача-передатчик должна иметь доступ к памяти, пока указатель на эту память находится в очереди. Когда же указатель прочитан из очереди, только задача-приемник должна иметь возмож­ность доступа к памяти.
  • Память, на которую ссылается указатель, должна существовать. Это требование ак­туально, если указатель ссылается на дина­мически выделенную память. Только одна задача должна быть ответственна за осво­бождение динамически выделенной памя­ти. Задачи не должны обращаться к памя­ти, если та уже была освобождена.
  • Нельзя использовать указатель на пере­менные, расположенные в стеке задачи, то есть указатель на локальные перемен­ные задачи. Данные, на которые ссылается указатель, будут неверными после очеред­ного переключения контекста.

Выводы:

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

Leave a Reply

You must be logged in to post a comment.