FreeRTOS. Операционная система реального времени для микроконтроллеров. Часть 9. Программная реализация таймеров.



Андрей Курниц
kurnits@stim.by

Это очередная статья из цикла, посвященного FreeRTOS — операционной системе для микроконтроллеров. Здесь читатель познакомится с нововведением последних версий FreeRTOS — встроенной реализацией про­граммных таймеров

Что представляет собой программный таймер?

В версии FreeRTOS V7.0.0 по сравне­нию с предыдущими версиями появилось существенное нововведение — встроен­ная реализация программных таймеров. Программный таймер (далее по тексту — таймер) во FreeRTOS — это инструмент, позволяющий организовать выполнение подпрограммы в точно заданные моменты времени.

Часть программы, выполнение которой инициирует таймер, в программе представ­лена в виде функции языка Си, которую в дальнейшем мы будем называть функцией таймера. Функция таймера является функ­цией обратного вызова (callback function). Механизм программных таймеров обеспечи­вает вызов функции таймера в нужные мо­менты времени.Программные таймеры предоставляют более удобный способ привязки выпол­нения программы к заданным моментам времени, чем использование API-функций vTaskDelay() и vTaskDelayUntil(), которые переводят задачу в блокированное состояние на заданный промежуток времени

Принцип работы программного таймера

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

Таймер может находиться в двух состоя­ниях: пассивном (Dorman state) и активном (Active state).

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

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

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

К таймеру могут быть применены следую­щие операции:

  1. Создание таймера — приводит к выде­лению памяти под служебную структуру управления таймером, связывает таймер с его функцией, которая будет вызываться при срабатывании таймера, переводит тай­мер в пассивное состояние.
  2. Запуск — переводит таймер из пассивного состояния в активное, таймер начинает от­счет времени.
  3. Останов — переводит таймер из активного состояния в пассивное, таймер прекраща­ет отсчет времени, функция таймера так и не вызывается.
  4. Сброс — приводит к тому, что таймер на­чинает отсчет временного интервала с на­чала. Подробнее об этой операции расска­жем позже.
  5. Изменение периода работы таймера.
Удаление таймера — приводит к освобож­дению памяти, занимаемой служебной структурой управления таймером.

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

  • режим интервального таймера (One-shot timer);
  • режим периодического таймера (Auto-reload timer).

Интервальный таймер

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

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

Периодический таймер

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

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

Режим работы таймера задается в момент его создания и не может быть изменен в процессе выполнения программы.

Сброс таймера и изменение периода

Во FreeRTOS есть возможность сбросить таймер после того, как он уже запущен. В результате сброса таймер начнет отсчитывать времен­ной интервал (равный периоду таймера) не с момента, когда таймер был запущен, а с момента, когда произошел его сброс (рис. 3).

Типичный пример использования операции сброса таймера — в устройстве, содержащем ЖКИ-дисплей с подсветкой. Подсветка дисплея включается по нажатию любой клавиши, а выключается спу­стя, например, 5 с после последнего нажатия. Если для отсчета 5 с ис­пользовать интервальный таймер, то операция сброса этого таймера должна выполняться при нажатии любой клавиши (подсветка в это время включена). Функция таймера должна реализовывать выклю­чение подсветки. В этом случае, пока пользователь нажимает на кла­виши, таймер сбрасывается и начинает отсчет 5 с с начала. Как только с момента последнего нажатия на клавишу прошло 5 с, выполнится функция таймера, и подсветка будет выключена.

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

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

Реализация программных таймеров во FreeRTOS

Функция таймера

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

void vTimerCallbackFunction( xTimerHandle xTimer );

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

void vTimerCallbackFunction( xTimerHandle xTimer ) {

// Код функции таймера

return;

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

Указатель на функцию таймера задан в виде макроопределения tmrTIMER_CALLBACK.

Задача обслуживания программных таймеров

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

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

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

Ограничение на вызов API-функций из функции таймера

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

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

Очередь команд таймеров

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

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

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

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

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

Дискретность отсчета времени

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

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

Эффективность реализации программных таймеров

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

  1. Для всех программных таймеров в про­грамме используется одна-единственная задача обслуживания таймеров и одна-единственная очередь коман
  2. Функция таймера выполняется в контексте задачи обслуживания таймеров, а не в кон­тексте обработчика прерывания микрокон­троллера.
  3. Процессорное время не расходуется зада­чей обслуживания таймеров, когда проис­ходит отсчет времени. Задача обслужива­ния таймеров получает управление, лишь когда истекает время, равное периоду ра­боты одного из таймеров.
  4. Использование программных таймеров не добавляет никаких вычислений в обра­ботчик прерывания от аппаратного таймера микроконтроллера, который используется для отсчета системных квантов времени.
  5. Программные таймеры реализованы на существующих механизмах FreeRTOS, поэтому использование программных тай­меров в программе повлечет минимальное увеличение размера скомпилированной программы.

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

Потребление оперативной памяти при использовании таймеров

Оперативная память, задействованная для программных таймеров, складывается из 3 составляющих:

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

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

Память, используемая задачей обслужи­вания таймеров, складывается из памяти, занимаемой блоком управления задачей tskTCB, — 70 байт и памяти стека, примем его равным минимальному рекомендованно­му configMINIMAL_STACK_SIZE = 256 слов (16-битных), что равно 512 байт. В сумме по­лучаем 70 + 512 = 582 байт.

Память, используемая очередью ко­манд таймеров, складывается из памяти для размещения блока управления очере­дью xQUEUE — 58 байт и памяти, в которой разместятся элементы очереди. Элемент оче­реди команд представляет собой структуру типа xTIMER_MESSAGE, размер которой равен 8 байт. Пусть используется очередь длиной 10 команд, тогда для размещения их в памяти потребуется 8х 10 = 80 байт. В сумме получаем 58+80 = 138 байт.

Каждый таймер в программе обслужива­ется с помощью структуры управления тай­мером xTIMER, ее размер составляет 34 байт. Так как таймеров в программе 10, то памяти потребуется 34х 10 = 340 байт.

Итого при условиях, оговоренных выше, для добавления в программу 10 програм­мных таймеров потребуется 582+138+340 = = 1060 байт оперативной памяти.

настройки FreeRTOS

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

Чтобы использовать программные тай­меры в своей программе, необходимо сде­лать следующие настройки FreeRTOS. Файл с исходным кодом программных тайме­ров /Source/timers.с должен быть включен в проект. Кроме того, в исходный текст про­граммы должен быть включен заголовочный файл croutine.h, содержащий прототипы API-функций для работы с таймерами:

#include “timers.h”

Также в файле конфигурации FreeRTOSConfig.h должны присутствовать следующие макроопределения:

  1. configUSE_TIMERS. Определяет, включе­ны ли программные таймеры в конфигу­рацию FreeRTOS: 1 — включены, 0 — ис­ключены. Помимо прочего определяет, будет ли автоматически создана задача обслуживания таймеров в момент запуска планировщика.
  2. configTIMER_TASK_PRIORITY. Задает при­оритет задачи обслуживания таймеров. Как и для всех задач, приоритет задачи обслужи­вания таймеров может находиться в преде­лах от 0 до (configMAX_PRIORITIES-l). Значение приоритета задачи обслужи­вания таймеров необходимо выбирать с осторожностью, учитывая требования к создаваемой программе. Например, если задан наивысший в программе приоритет, то команды задаче обслуживания таймеров будут передаваться без задержек, а функция таймера будет вызываться сразу же, когда время, равное периоду таймера, истекло. Наоборот, если задаче обслуживания тайме­ров назначен низкий приоритет, то переда­ча команд и вызов функции таймера будут задержаны по времени, если в данный мо­мент выполняется задача с более высоким приоритетом.

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

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

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

4. configTIMER_TASK_STACK_DEPTH. Задает размер стека задачи обслуживания таймеров. Задается не в байтах, а в словах, равных разрядности процессора. Тип дан­ных слова, которое хранится в стеке, задан в виде макроопределения portSTACK_TYPE в файле portmacro. h. Функция таймера вы­полняется в контексте задачи обслужива­ния таймеров, поэтому размер стека зада­чи обслуживания таймеров определяется потреблением памяти стека функциями таймеров.

Работа с таймерами

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

Дескриптор таймера представляет собой переменную типа xTimerHandle. При соз­дании таймера FreeRTOS автоматически на­значает ему дескриптор, который далее ис­пользуется в программе для операций с этим таймером.

Функция таймера автоматически получает дескриптор таймера в качестве своего аргу­мента. Для выполнения операций с тайме­ром внутри функции этого таймера следует использовать дескриптор таймера, получен­ный в виде аргумента.

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

Создание/удаление таймера

Для того чтобы создать программный таймер, следует вызвать API-функцию xTimerCreateO. Ее прототип:

xTimerHandle xTimerCreate( const signed char *pcTimerName, portTickType xTimerPeriod, unsigned portBASETYPE uxAutoReload void * pvTimerID, tmrTIMER_CALLBACK pxCallbackFunction );

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

  1. pcTimerName — нультерминальная (закан­чивающаяся нулем) cтрока, определяющая имя таймера. Ядром не используется, а слу­жит лишь для наглядности и при отладке.
  2. xTimerPeriod — период работы таймера. Задается в системных квантах времени, для задания в миллисекундах следует ис­пользовать макроопределение portTICK_ RATE_MS. Например, для задания перио­да работы таймера равным 500 мс следует присвоить аргументу xTimerPeriod значе­ние выражения 500/portTICK_RATE_MS. Нулевое значение периода работы таймера не допускается.
  3. uxAutoReload — определяет тип создавае­мого таймера. Может принимать следую­щие значения:

 

  • pdTRUE — будет создан периодический таймер.
  • pdFALSE — будет создан интервальный таймер.

 

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

void vCallbackFunction( xTimerHandle xTimer );

Указатель на функцию таймера задан так­же в виде макроопределения tmrTIMER_ CALLBACK.

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

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

Ранее созданный таймер может быть уда­лен. Для этого предназначена API-функция xTimerDelete(). Ее прототип:

portBASETYPE xTimerDelete( xTimerHandle xTimer, portTickType xBlockTime );

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

  1. xTimer — дескриптор таймера, полу­ченный при его создании API-функцией xTimerCreateO .
  2. 2.    xBlockTime — определяет время тайм-аута — максимальное время нахождения вызывающей xTimerDelete() задачи в бло­кированном состоянии, если очередь ко­манд полностью заполнена и нет возмож­ности поместить в нее команду об уни­чтожении таймера.
  3. Возвращаемое значение — может прини­мать два значения:

 

  • pdFAIL — означает, что команда об уда­лении так и не была помещена в очередь ко­манд, а время тайм-аута истекло.
  • pdPASS — означает, что команда об уда­лении успешно помещена в очередь команд.

Вызов xTimerDelete() приводит к осво­бождению памяти, занимаемой структурой управления таймером xTIMER.

API-функции xTimerCreateO и xTimerDelete() недопустимо вызывать из обработчиков прерываний.

Запуск/останов таймера

Запуск таймера осуществляется с помощью API-функции xTimerStartQ. Ее прототип:

portBASETYPE xTimerStart( xTimerHandle xTimer, portTickType xBlockTime );

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

  1. xTimer — дескриптор таймера, полу­ченный при его создании API-функцией xTimerCreateO.
  2. 2.    xBlockTime — определяет время тайм-аута — максимальное время нахождения вызываю­щей xTimerStartQ задачи в блокированном состоянии, если очередь команд полностью заполнена и нет возможности поместить в нее команду о запуске таймера.
  3. Возвращаемое значение — может прини­мать два значения:

 

  • pdFAIL — означает, что команда о запуске таймера так и не была помещена в очередь команд, а время тайм-аута истекло.
  • pdPASS — означает, что команда о запуске успешно помещена в очередь команд.

Запуск таймера может быть произве­ден и с помощью вызова API-функции xTimerResetO, подробно об этом — в описа­нии API-функции xTimerResetQ ниже.

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

portBASE_TYPE xTimerStop( xTimerHandle xTimer, portTickType xBlockTime );

Аргументы и возвращаемое значение: 1. xTimer — дескриптор таймера, полу­ченный при его создании API-функцией xTimerCreate().

  1. 2.    xBlockTime — определяет время тайм-аута — максимальное время нахождения вызываю­щей xTimerStopQ задачи в блокированном состоянии, если очередь команд полностью заполнена и нет возможности поместить в нее команду об останове таймера.
  2. Возвращаемое значение — может прини­мать два значения:

 

  • pdFAIL — означает, что команда об оста­нове таймера так и не была помещена в очередь команд, а время тайм-аута ис­текло.
  • pdPASS — означает, что команда об оста­нове успешно помещена в очередь ко­манд.

API-функции xTimerStartQ и xTimerStopQ предназначены для вызова из задачи или функции таймера. Существуют версии этих API-функций, предназначенные для вызова из обработчиков прерываний, о них будет сказано ниже.

Сброс таймера

Сброс таймера осуществляется с помощью API-функции xTimerResetQ. Ее прототип:

portBASETYPE xTimerReset( xTimerHandle xTimer, portTickType xBlockTime );

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

  1. xTimer — дескриптор таймера, полу­ченный при его создании API-функцией xTimerCreateO).
  2. 2.    xBlockTime — определяет время тайм-аута — максимальное время нахождения вызывающей хTimerReset0 задачи в блоки­рованном состоянии, если очередь команд полностью заполнена и нет возможности поместить в нее команду о сбросе таймера.
  3. Возвращаемое значение — может прини­мать два значения:

 

  • pdFAIL — означает, что команда о сбросе таймера так и не была помещена в оче­редь команд, а время тайм-аута истекло.
  • pdPASS — означает, что команда о сбро­се таймера успешно помещена в очередь команд.

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

Допускается вызов xTimerReset(), когда тай­мер уже создан, но планировщик еще не за­пущен. В этом случае отсчет времени начнет­ся не с момента вызова xTimerReset(), а с мо­мента запуска планировщика.

Легко заметить, что API-функции xTimerReset() и xTimerStart() полностью экви функцией xTimerCreate().

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

API-функция xTimerResetO предназначена для вызова из тела задачи или функции тай­мера. Существует версия этой API-функции, предназначенная для вызова из обработчика прерывания, о ней будет сказано ниже.

Изменение периода работы таймера

Независимо от того, в каком состоянии в данный момент находится таймер: в ак­тивном или в пассивном, период его работы можно изменить посредством API-функции xTimerChangePeriodQ. Ее прототип:

portBASETYPE xTimerChangePeriod( xTimerHandle xTimer, portTickType xNewPeriod, portTickType xBlockTime );

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

  1. xTimer — дескриптор таймера, полу­ченный при его создании API-функцией xTimerCreateQ.
  2. 2.    xNewPeriod — новый период работы тай­мера, задается в системных квантах.
  3. xBlockTime — определяет время тайм-аута — максимальное время нахождения вызывающей xTimerChangePeriod() задачи в блокированном состоянии, если очередь команд полностью заполнена и нет воз­можности поместить в нее команду об из­менении периода таймера.
  4. Возвращаемое значение — может прини­мать два значения:

 

  • pdFAIL — означает, что команда об из­менении периода таймера так и не была помещена в очередь команд, и время тайм-аута истекло.
  • pdPASS — означает, что команда об из­менении периода успешно помещена в очередь команд.

API-функция xTimerChangePeriodQ предна­значена для вызова из тела задачи или функ­ции таймера. Существует версия этой API-функции, предназначенная для вызова из обра­ботчика прерывания, о ней будет сказано ниже.

Получение текущего состояния таймера

Для того чтобы узнать, в каком состоя­нии — в активном или в пассивном — в дан­ный момент находится таймер, служит API-функция xTimerhTimerActiveO. Ее прототип:

portBASE_TYPE xTimerIsTimerActive( xTimerHandle xTimer );

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

  • pdTRUE, если таймер находится в актив­ном состоянии.
  • pdFALSE, если таймер находится в пассив­ном состоянии.

API-функция xTimerhTimerActiveO пред­назначена для вызова только из тела задачи или функции таймера.

Получение идентификатора таймера

При создании таймеру присваивается идентификатор в виде указателя void*, что позволяет связать таймер с произвольной структурой данных.

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

void *pvTimerGetTimerID( xTimerHandle xTimer );

Аргументом является дескриптор таймера, идентификатор которого необходимо полу­чить. pvTimerGetTimerlDO возвращает ука­затель на сам идентификатор.

работа с таймерами

из обработчиков прерываний

Есть возможность выполнять управление таймерами из обработчиков прерываний ми­кроконтроллера. Для рассмотренных выше API-функций xTimerStartO, xTimerStopO, xTimerChangePeriodQ и xTimerResetO су­ществуют версии, предназначенные для вызова из обработчиков прерываний: xTimerStartFromlSR 0, xTimerStopFromlSR О, xTimerChangePeriodFromlSR() и xTimerResetFromlSRO. Их прототипы:

portBASETYPE xTimerStartFromISR( xTimerHandle xTimer, portBASE_TYPE *pxHigherPriorityTaskWoken ); portBASETYPE xTimerStopFromISR( xTimerHandle xTimer, portBASE_TYPE *pxHigherPriorityTaskWoken ); portBASETYPE xTimerChangePeriodFromISR( xTimerHandle xTimer, portTickType xNewPeriod, portBASE_TYPE *pxHigherPriorityTaskWoken );

portBASETYPE xTimerResetFromISR( xTimerHandle xTimer, portBASE_TYPE *pxHigherPriorityTaskWoken );

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

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

Появился дополнительный аргу­мент pxHigherPriorityTaskWoken. API-функции устанавливают значение *pxHigherPriorityTaskWoken в pdTRUE, если в данный момент выполняется за­дача с приоритетом меньше, чем у задачи

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

Учебная программа

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

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

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

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

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

#include
#include
#include “FreeRTOS.h”
#include “task.h”
#include “timers.h”
/*	*/
/* Количество интервальных таймеров */
#define NUMBER_OF_TIMERS   3
/* Целочисленные идентификаторы интервальных таймеров */
#define ID_TIMER_1	111
#define ID_TIMER_2	222
#define ID_TIMER_3	333
/*	*/
/* Дескриптор периодического таймера */
xTimerHandle	xAutoReloadTimer;
/* Массив дескрипторов интервальных таймеров */
xTimerHandle	xOneShotTimers[NUMBER_OF_TIMERS];
/* Массив идентификаторов интервальных таймеров */
const  unsigned portBASETYPE uxOneShotTimersIDs
[NUMBER_OF_TIMERS] = { ID_TIMER_1, ID_TIMER_2,
ID_TIMER_3 };
/* Период работы периодического таймера = 1 секунда */
unsigned int uiAutoReloadTimerPeriod = 1000 / portTICKRATEMS;
/*	*/
/* Функция периодического таймера.
*	Является функцией обратного вызова.
*	В программе не должно быть ее явных вызовов.
*	В функцию автоматически передается дескриптор таймера в виде аргумента xTimer. */
void vAutoReloadTimerFunction(xTimerHandle xTimer) {
/* Сигнализировать о выполнении.
*	Вывести сообщение о текущем времени, прошедшем с момента запуска планировщика. */
printf(“AutoReload timer. Time = %d sec\n\r”, xTaskGetTickCount() / configTICK_RATE_HZ);
/* Увеличить период работы периодического таймера на 1 секунду */
uiAutoReloadTimerPeriod += 1000 / porfTICK_RATE_MS;
/* Установить новый период работы периодического таймера.
*	Время тайм-аута (3-й аргумент) обязательно должно быть 0!
*	Так как внутри функции таймера нельзя вызывать блокирующие API-функции. */
xTimerChangePeriodfxTimer, uiAutoReloadTimerPeriod, 0);
}
/*	*/
/* Функция интервальных таймеров.
*	Нескольким экземплярам интервальных таймеров соответствует одна-единственная функция.
*	Эта функция автоматически вызывается при истечении времени любого из связанных с ней таймеров.
*	Для того чтобы выяснить, время какого таймера истекло, используется идентификатор таймера */
void vOneShotTimersFunction(xTimerHandle xTimer) {
/* Указатель на идентификатор таймера */ unsigned portBASE_TYPE *pxTimerID;
/* Получить идентификатор таймера, который вызывал эту функцию таймера */ pxTimerlD = pvTimerGetTimerID(xTimer);
/* Различные действия в зависимости от того, какой таймер вызывал функцию */
switch (*pxTimerID) {
/* Сработал интервальный таймер 1 */
case ID_TIMER_1:
/* Индикация работы + текущее время */
printf CAtWftGneShot timer ГО
/* Запустить интервальный таймер 2 */
xTimerStart(xOneSliotTimers[l], 0);
break; /* Сработал интервальный таймер 2 */ case ID_TIMER_2:
/* Индикация работы + текущее время */
printf(tAt\t\t\tGneShot tiiTierro= %d Tir^
/* Запустить интервальный таймер 3 */
xTimerStart(xOneSliotTimers[2], 0);
break; case ID_TIMER_3:
/* Индикация работы + текущее время */
printf(“\t\t\t\tDneShot timer ID = %d Time = %d sec\n\r”, *pxTirnerID, xTaskGetTickCountf) / configTICK_RATE_HZ);
puts(“\n\r\t\t\t\tAbout to delete AutoReload timer!);
fflushQ;
/* Удалить периодический таймер. * После этого активных таймеров в программе не останется. */
xTimerDeletefxAutoReloadTimer, 0);
break;
}
}
/*	*/
/* Точка входа в программу. */
short main( void )
{
unsigned portBASE_TYPE i;
/* Создать периодический таймер.
*	Период работы таймера = 1 секунда
*	Идентификатор таймера не используется (0). */
xAutoReloadTimer = xTimerCreate(“AutoReloadTimer”, uiAutoReloadTimerPeriod, pdTRUE, 0, vAutoReloadTimerFunction);
/* Вьшолнить сброс периодического таймера ДО запуска планировщика
*	Таким образом, он начнет отсчет времени одновременно с запуском планировщика */
xTimerReset(xAutoReloadTimer, 0);
/* Создать 3 экземпляра интервальных таймеров.
*	Период работы таймеров = 12 секунд.
*	Каждому из них передать свой идентификатор.
*	Функция для них всех одна — vOneShotTimersFunction(). */
for (i = 0; i < NUMBER_OF_TIMERS; i++) {
xOneShotTimers[i] = xTimerCreate(“OneShotTimer_n”, 12000 / portTICK_RATE_MS, pdFALSE, (void*) 8aixQneShotTimersIDs[i], vQneShotTimersFunction); }
/* Вьшолнить сброс только первого интервального таймера.
*	Именно он начнет отсчитывать время сразу после запуска планировщика
*	Остальные 2 таймера после запуска планировщика останутся в пассивном состоянии. */
xTimerReset(xOneShotTimers[0], 0);
/* Индицировать текущее время.
*	Оно будет равно 0, так как планировщик еще не запущен. */
printf(“Timers start! Time = %d sec\n\r\n\r”, xTaskGetTickCount() / configTICK_RATE_HZ);
!* Запуск планировщика
*	Автоматически будет создана задача обслуживания таймеров.
*	Таймеры, которые были переведены в активное состояние (например, вызовом xTimerReset())
*	ДО этого момента, начнут отсчет времени. */
vTaskStartScheduler();
return 1;
}

Для корректной компиляции учебной программы конфигураци­онный файл FreeRTOSConfig.fi должен содержать следующие строки:

1 1( 10 ) configMINIMAL_STACK_SIZE

fdefine configUSE_TIMERS fdefine configTIMER_TASK_PRIORITY fdefine configTIMER_QUEUE_LENGTH fdefine configTIMER_TASK_STACK_DEPTH

Результат работы учебной программы приведен на рис. 6.

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

В графическом виде работа учебной программы представлена на рис. 7.

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

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

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

/* Функция интервальных таймеров.
*	Нескольким экземплярам интервальных таймеров соответствует одна-единственная функция
*	Эта функция автоматически вызывается при истечении времени любого из связанных с ней таймеров.
*	Для того чтобы выяснить, время какого таймера истекло, используется идентификатор таймера */
void vOneShotTimersFunction(xTimerHandle xTimer) {
/* Различные действия в зависимости от того, какой таймер вызывал функцию */ /* Сработал интервальный таймер 1? */ if (xTimer == xOneShotTimers[0]) {
/* Индикация работы + текущее время */
printf(“\t\t\t\tOneShot timer ID = %d. Time = %d sec\n\r”, *pxTimerID, xTaskGetTickCount() / configTICK_RATE_HZ);
xTimerChangePeriod(xAutoReloadTimer, 6000, 0);
/* Запустить интервальный таймер 2 */
xTimerStart(xOneShotTimers[1], 0); /* Сработал интервальный таймер 2? */ } else if (xTimer == xOneShotTimers[1]) {
/* Индикация работы + текущее время */
printf(“\t\t\t\tOneShot timer ID = %d. Time = %d sec\n\r”, *pxTimerID, xTaskGetTickCount() / configTICK_RATE_HZ);
/* Запустить интервальный таймер 3 */
xTimerStart(xOneShotTimers[2], 0); /* Сработал интервальный таймер 3? */ } else if (xTimer == xOneShotTimers[2]) {
/* Индикация работы + текущее время */
printf(“\t\t\t\tOneShot timer ID = %d. Time = %d sec\n\r”, *pxTimerID, xTaskGetTickCount() / configTICK_RATE_HZ);
puts(“\n\r\t\t\t\tAbout to delete AutoReload timer!);
fflush();
/* Удалить периодический таймер.
* После этого активных таймеров в программе не останется */
xTimerDelete(xAutoReloadTimer, 0); } }

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

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

Выводы

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

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

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

Leave a Reply

You must be logged in to post a comment.