STM Урок 111. FreeRTOS. Очереди. Часть 1



Продолжаем изучение операционной системы FreeRTOS и некоторых тонкостей программирования с её применением. Прошлый урок 110 по приоритетам задач показал, что работа с задачами, имеющими одинаковый приоритет, никогда не переходящих в состояние «Блокирована», может привести к некоторым интересным последствиям, особенно если слишком много кода приходится выполнять в функции задач. Поэтому наиболее важный код, который должен быть выполнен обязательно правильно, мы должны как-то отделить от другого кода. Мы этим занимались, когда изучали семафоры. Но семафоры и мьютексы могут не всегда приводить к желаемым результатам, когда слишком много желающих его захватить. Корректности тут никакой не будет. Поэтому здесь могут помочь очереди. А каким образом они могут помочь, если их назначение совершенно другое, мы увидим в ходе изучения урока.

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

Очередь — это своего рода массив значений как правило одного типа, в который мы можем сохранять значения из одной задачи или нескольких, а выбирать их из другой задачи или других задач. Только данный массив представляет собой такой буфер, в который доступ мы получаем не к любому элементу, а именно к определённому, поэтому данное свойство очереди мы должны учитывать. Данный буфер чаще всего используется по типу FIFO (первым пришел — первым вышел), реже этот буфер может быть использован как LIFO или стек (магазин), где принцип уже другой (последним пришёл — первым вышел). Данный тип очереди используется гораздо реже и для этого используется другая функция выборки. Думаю, что нам такая функция (по крайней мере в рамках данного занятия), не потребуется, и голову мы забивать себе не будем.

Размер очереди (или данного буфера) не ограничен, но только эту возможность также нужно использовать с умом. Если мы сделаем слишком большой размер очереди, то мы можем долго ждать того, когда функция-приёмник (или получатель) получит данные от функции-источника (или провайдера).

Когда используются очереди в системе реального времени FreeRTOS? Да очень часто, когда есть риск перепутывания данных, например, при печати на принтер, либо при выводе в порт. Мы в этом случае организовываем отдельную задачу передачи в порт информации, а данные для неё черпаем из очереди, в которую они придут из других задач.

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

Давайте немного отдохнём от моих заумных фраз, чтобы у нас не закипели головы, и перейдём к проекту.

Проект мы создадим на основе проекта урока 110 TASK_PRIOPITIES и назовём его TASKS_QUEUES.

Откроем наш проект в проектогенераторе Cube MX, и, вообще ничего не трогая сгенерируем проект для SystemWorkbench, откроем его там, настроим оптимизацию 1 и уберём отладочную конфигурацию, если таковая будет иметь место в проекте.

Откроем файл main.c, закомментируем строки с ошибками в инициализации DMA2D, соберём проект, если всё нормально собралось, то продолжаем дальше нашу работу над проектом.

 

 

Исправим немного вывод шапки для актуальности

 

TFT_DisplayString(0, 10, (uint8_t *)"Queues", CENTER_MODE);

 

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

 

osThreadId Task01Handle,Task02Handle,Task03Handle,TaskStringOutHandle;

 

Создадим для функции задачи прототип

 

void Task01(void const * argument);

void TaskStringOut(void const * argument);

 

Добавим для задачи функцию выше функции Task1

 

//---------------------------------------------------------------

void TaskStringOut(void const * argument)

{

  for(;;)

  {

    sprintf(str1,"task %lu", osKernelSysTick());

    TFT_DisplayString(120, 60, (uint8_t *)str1, LEFT_MODE);

  }

}

//---------------------------------------------------------------

 

Это пока заготовка и здесь мы будем выводить строку с количеством прошедших системных квантов в определённое место.

 

В функции main() создадим нашу задачу и присвоим ей более высокий приоритет, чем наши подопытные три задачи, причём даже выше, чем мы им присваиваем после, а не при создании. Стек тоже назначим побольше, так как строки могут быть разные

 

arg03.delay_per = 439;

osThreadDef(tskstrout, TaskStringOut, osPriorityBelowNormal, 0, 1280);

TaskStringOutHandle = osThreadCreate(osThread(tskstrout), NULL);

 

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

 

 

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

 

 

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

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

Есть ещё ряд тонкостей по использованию очередей, но, думаю, не будем себе забивать голову и, если кому-то это интересно, то всё это можно почитать в официальной документации разработчика ОС.

Теперь опять вернёмся к практике и, следовательно, к нашему проекту.

Сначала мы попробуем более простой тип очереди — это обычные данные (не массивы и не структуры).

Создадим специальную глобальную переменную для очереди

 

osThreadId Task01Handle,Task02Handle,Task03Handle,TaskStringOutHandle;

osMessageQId pos_Queue;

 

Добавим макрос для размера очереди

 

struct_arg arg01, arg02, arg03;

#define QUEUE_SIZE (uint32_t) 1

 

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

Создадим очередь в функции main() в специально отведённом для этой цели месте

 

/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... *//* USER CODE END RTOS_QUEUES */

osMessageQDef(pos_Queue, QUEUE_SIZE, uint16_t);

pos_Queue = osMessageCreate(osMessageQ(pos_Queue), NULL);

/* USER CODE END RTOS_QUEUES */

 

Код для создания очереди чем-то напоминает код для создания задач и семафоров.

То есть, здесь мы объявили очередь количеством в один элемент типа шестнадцатибитного беззнакового целого числа.

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

 

for(;;)

{

  osMessagePut(pos_Queue, arg->y_pos, 100);

}

 

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

Теперь перейдём в функцию задачи вывода строки на дисплей TaskStringOut и добавим там переменную специального типа структуры, предназначенной для свойств и событий очереди

 

void TaskStringOut(void const * argument)

{

  osEvent event;

 

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

 

for(;;)

{

  event = osMessageGet(pos_Queue, 100);

  if (event.status == osEventMessage)

  {

    sprintf(str1,"task %lu", osKernelSysTick());

    TFT_DisplayString(120, event.value.v, (uint8_t *)str1, LEFT_MODE);

  }

}

 

Вот и всё!

Соберём код, прошьём контроллер и проверим результат. Я не буду показывать все шесть этапов изменения приоритетов, покажу только последний этап, когда все задачи будут работать с одинаковым приоритетом. Можете поверить мне на слово, что таким образом (без артефактов) они теперь работают всегда

 

 

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

 

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

 

 

Предыдущий урок Программирование МК STM32 Следующая часть

 

 

Отладочную плату можно приобрести здесь STM32F746G-DISCOVERY

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

STM FreeRTOS. Очереди

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*