Таймеры служат для упрощения организации время-зависимых процессов. С их помощью можно выделять кванты времени работы задач или просто функций, не являющихся задачами, выдавать указания по времени работы другим задачам, наконец, просто для отсчета временных интервалов. Таймеры в OSA привязаны к системным тикам (периодам вызова OS_Timer). При вызове OS_Timer все активные таймеры увеличиваются на 1, а при переполнении какого-либо из них устанавливается соответствующий флаг переполнения.
OSA предоставляет возможность использования четырех типов таймеров:
Каждый из этих тймеров имеет свои преимущества и недостатки и, в зависимости от задачи, один тип таймера может оказаться эффективнее других. Ниже приведены особенности каждого типа таймеров.
Таймеры задач - это те счетчики, которые хранятся в дескрипторах задач и используются для формирования задержек (OS_Delay) и ожидания событий с выходом по таймауту (OS_xxx_Wait_TO). Раньше их функциональность этим ограничивалась. Начиная с версии 100210, появилась возможность использовать эти таймеры для второстепенных целей. Например, теперь можно инициализировать таймер и вызвать функцию, которая будет работать до тех пор, пока таймер не закончит счет.
Определения в файле OSAcfg.h:
OS_ENABLE_TTIMERS | Для использования таймеров задач в файле osacfg.h должна быть определена эта константа. |
OS_TTIMER_SIZE | Задает размерность таймеров: 1, 2 или 4 (по умолчанию 2). |
OS_TTIMERS_OPTIMIZE_SIZE | Включает оптимизацию кода OS_Timer() по размеру (по умолчанию код оптимизирован по скорости) |
OS_BANK_TASKS | Определяет банк памяти для хранения дескрипторов задач: 0, 1, 2 или 3 (по умолчанию 0) |
Статические таймеры - это массив из OS_STIMERS счетчиков размерностью OS_STIMER_SIZE (обе константы задаются в файле osacfg.h). Для работы с ними сервисам нужно указывать ID - номер таймера (в виде целого числа). Старший бит счетчика используется для индикации его состояния: установлен - таймер считает, сброшен - таймер остановлен. Таким образом, сам счетчик таймера на 1 бит меньше разрядности переменной, в которой он содержится. Т.е., если OS_STIMER_SIZE = 2 байта, то под счетчик выделяются только 15 бит. Статические таймеры самые компактные из всех.
Раньше назначение каждого статического таймера было предопределено с начала программы. Т.е. цель, с которой использовался таймер, должна была быть известна заранее, а следовательно - за каждым таймером закреплялся свой номер. Это было неудобно, т.к. возникали сложности с переносом модулей, использующих статические таймеры, в другие проекты. Начиная с версии 100210 появилась возможность назначать таймеры в ходе программы (см. ниже)
Определения в файле OSAcfg.h:
OS_STIMERS | Этой константой задается количество используемых в программе статических таймеров |
OS_STIMER_SIZE | Задает размерность таймеров: 1, 2 или 4 (по умолчанию 2) |
OS_STIMERS_OPTIMIZE_SIZE | Включает оптимизацию кода OS_Timer() по размеру (по умолчанию код оптимизирован по скорости) |
OS_STIMERS_ENABLE_ALLOCATION | Включает дополнительный код для назначения таймеров, а также доболнительный массив битов, отвечающих за занятость таймеров. Становятся доступны сервисы OS_Stimer_Alloc, OS_Stimer_Free, OS_Stimer_Found |
OS_BANK_STIMERS | Определяет банк памяти для хранения массива таймеров: 0, 1, 2 или 3 (по умолчанию 0) |
Со статическими таймерами можно работать двумя способами:
Первый способ предпочтительнее, хотя и немного менее компактный и менее быстрый. Он позволяет переносить модули в другие проекты не заботясь о соблюдении нумерации таймеров.
Этот тип таймеров введен с версии 100210. Все запускаемые таймеры выстраиваются в очередь так, что первым в ней стоит тот, которому считать меньше всех. Далее - в порядке возрастания оставшегося времени счета. Это позволяет очень быстро обрабатывать большое количество таймеров сервисом OS_Timer(). При большом количество таймеров это существенно ускоряет работу OS_Timer, что может оказаться незаменимым качеством при обработке таймеров в прерывании.
Определения в файле OSAcfg.h:
OS_ENABLE_QTIMERS | Для использования очереди таймеров должна быть определена эта константа |
OS_QTIMER_SIZE | Задает размерность таймеров: 1, 2 или 4 (по умолчанию 2) |
Таймеры определяются в программе как обычные переменные:
OST_QTIMER qt_1, qt_2;
Перед работой с таймером он должен быть создан сервисом OS_Qtimer_Create. При запуске таймер добавляется в очередь, занимая в ней место в соответствии с длительностью счета. При каждом вызове OS_Timer() происходит уменьшение на 1 только первого в очереди таймера (чем и объясняется высокая скорость обработки большого количества таймеров). Когда таймер досчитывает до 0, он удаляется из очереди и устанавливается его бит переполнения.
Например в очередь помещаются два таймера с временами счета 30 и 10 тиков:
OS_Qtimer_Run(qt_1, 30); OS_Qtimer_Run(qt_2, 10);
Сначала в начало пустой очереди поместится таймер qt_1 со временем счета = 30. Затем его потеснит новый таймер qt_2, он встанет в начало очереди, т.к. ему считать меньше. При этом время счета таймера qt_1 изменится с 30 на 20. Так происходит потому, что первые 10 тиков будет считать таймер qt_2, а qt_1 будет дожидаться своей очереди. Как только qt_2 досчитает и удалится из очереди, начнет уменьшаться qt_1, вставший на этот момент в начало очереди. 10 тиков уже отсчитаны и ему останется только 20. Если теперь будет запущен еще один таймер qt_3 со временем счета 100:
OS_Qtimer_Run(qt_3, 100);
то он поместится в конец очереди, а значение его счетчика будет уменьшено на значение первого в очереди, т.е. на 20. Т.е. реально в счетчик запишется значение 80.
(Устаревший, не рекомендуется к использованию). Динамические таймеры тоже выстраиваются в очередь, только они не сортируются по оставшемуся времени счета, а располагаются в ней в порядке добавления. При каждом вызове OS_TImer на 1 уменьшаются все счетчики, а учитывая, что это делается через косвенную адресацию, время их обработки может быть очень большим.
Важным свойством динамического таймера является то, что после переполнения он продолжает счет (в отличие от всех остальных, которые после переполнения останавливается). Это позволяет отсчитывать равные интервалы времени вне зависимости от того, в какой момент таймер обработан задачей. Например, таймер был запущен на отсчет интервала в 1 секунду. Секунда прошла (произошло переполнение таймера), а задача, ожидающая этот таймер, не смогла начать выполняться из-за того, что сейчас выполняется или готова к выполнению более приоритетная задача. В результате этого задача сможет обработать таймер только через некоторое время после его переполнения (например, через 100 мс). Но, обработав его, она вновь задает интервал счета в 1 секунду, и при этом учитывается, что уже прошло 100 мс. Поэтому следующее переполнение произойдет через 900 мс. Все остальные таймеры (и таймеры задач, и статические таймеры, и очередь таймеров) останавливают счет после переполнения.
Определения в файле OSAcfg.h:
OS_ENABLE_DTIMERS | Для использования динамических таймеров должна быть определена эта константа |
OS_DTIMER_SIZE | Задает размерность таймеров: 1, 2 или 4 (по умолчанию 2) |
Таймеры определяются в программе как обычные переменные:
OST_DTIMER dt_1, dt_2;
Перед работой с таймером он должен быть создан сервисом OS_Qtimer_Create (это сразу же добавит его в очередь активных таймеров, но он еще не будет считать). После работы с таймером он должен быть удален из очереди, чтобы разгрузить функцию OS_Timer() от ненужных действий над неиспользуемым таймером. Следует внимательно следить за тем, когда создаются таймеры, т.к. OSA не имеет механизма проверки того, что таймер уже присутствует в очереди.
Здесь для удобства сведены максимально допустимые задержки (округлены в меньшую сторону) для наиболее часто используемых значений системного тика:
Интервал вызова OS_Timer | 1-байтовые | 2-байтовые | 4-байтовые |
---|---|---|---|
Диапазон значений | 0-255 | 0-65535 | 0-4294967295 |
1 ms | 255 мс | 64 сек | 48 суток |
10 ms | 2.4 сек | 10 мин | 490 суток |
18.2 ms | 9.2 сек | 18 мин | 900 суток |
256 us | 130 мс | 16 сек | 12 суток |
1024 us | 520 мс | 65 сек | 50 суток |
8192 us | 4 с | 8 мин | 400 суток |
Поддержка таймеров - очень важная характеристика ОС. Работа любого устройства, служащее человеку, должна соответствовать восприятию реальности самим человеком. Большинство процессов взаимодействия устройства с человеком, с другим устройством или с внешней средой являются времязависимыми. Когда мы нажимаем кнопку, устройство выполняет операции по подавлению дребезга, которые не могут выполниться очень быстро из-за механических особенностей контакта, а также не могут выполниться слишком медленно, чтобы соответствовать требованиям эргономики. Когда стиральная машина включает насос для забора воды, она не может его держать включенным вечно, если вода не поступает, но и не может при отсутствии поступления воды отключить его мгновенно, поскольку давление в трубах и длина шланга вносят некоторую инерционность. Когда мы подключаем одно устройство к другому (например, флеш-память к компьютеру), устройства должны установить связь или, если этого сделать не удалось, в течение какого-то времени повторять попытки с каким-то интервалом. И т.д. Другими словами устройство должно иметь возможность измерять время. Время, измеренное устройством, обычно дискретно, относительно и часто неравномерно, но, тем не менее, при таких характеристиках этого достаточно, чтобы удовлетворить наши потребности.
Таким образом, в устройствах, управляемых микроконтроллерами, программное обеспечение микроконтроллеров должно иметь возможность измерять время. Т.к. операция очень частая, то ее разумно вынести в отдельню библиотеку, чтобы каждый раз не писать подпрограммы управления замерами времени с нуля. Когда создавалась операционная система OSA, было принято решение снабдить ее механизмом отсчета времени. Изначально дескриптор каждой задачи имел свой счетчик, изменяемый во времени, который позволял формировать нужные задержки или прерывать ожидание событий, если они не наступали в течение какого-то времени. Эти счетчики я назвал "таймеры задач", т.к. к каждой задаче был жестко привязан один счетчик.
Однако для большинства проектов требовались дополнительные таймеры, которые позволяли бы, во-первых, нескольким задачам получать доступ к одному и тому же таймеру, во-вторых, выполнять задержки задачам с помощью таймеров задач, не сбивая счет дополнительного таймера, и в-третьих, передавать таймеры в виде параметров функциям, работа которых также времязависима. Так в OSA были добавлены пользовательские таймеры (сейчас они переведены в статус устаревших и называются Статические таймеры старого типа). Но работа с ними была довольно неудобной и местами утомительной, т.к. с целью экономии ресурсов контроллеров были применены не очень удачные с точки зрения удобства способы управления.
Со временем эти таймеры пришлось заменить статическими таймерами, которые были менее гибкими, но более формализованными и понятными. Ранее в программе могли одновременно применяться пользовательские таймеры различных размерностей (8 бит, 16 бит, 24 бита и 32 бита), теперь на все статические таймеры выделена одна размерность. Но это позволило устранить путаницу с нумерацией и конфигурированием таймеров.
Тем не менее оставалась сложность с переносом модулей из проекта в проект, т.к. при нем требовалось изменять конфигурацию ноовго проекта так, чтобы номер статического таймера, используемого в модуле, оказался незанятым, а количество таймеров, задаваемое в файле конфигурации, соответствовало количеству реально применяемых таймеров. Поэтому возникла необходимость добавить новый тип таймеров - динамические таймеры. Их можно было определять, добавлять и удалять в ходе выполнения программы, они организовывались в виде однонаправленного списка, что было очень удобно, т.к. неиспользуемые можно было удалять или передавать другим задачам. Однако это удобство нивелировалось тем, что их обработка производилась через косвенную адресацию, т.е. занимало сравнительно большее время, а учитывая, что обработка таймеров (сервис OS_Timer) чаще всего используется в прерывании, это накладывало ограничение на количество одновременно работающих таймеров. Например, для обработки 20 таймеров на PIC18 потребовалось бы почти 200 тактов (для сравнения для обработки 20 статических - только 60).
Со временем пришла идея заменить динамические таймеры очередью таймеров. Смысл замены состоял в том, что в очереди вычисления производятся только над первым в порядке возрастания таймером, а все остальные внесены в очередь с поправкой на время самого младшего таймера. Т.е. все таймеры в хвосте очереди (т.е. все, кроме головного) содержат не фактическое время счета, а инкремент по отношению к предыдущему в очереди. Таймреы со значениями 10, 100, 150 и 280 хранятся в очереди в виде: 10, +90, +50, +130. Это позволило сильно ускорить обработку большого количества таймеров. Пускай в программе активны хоть 50 таймеров, один инкремен производится за 11 тактов (для PIC16). Правда, есть оговорка: это время увеличивается в вмомент переполнения, т.к. требуется произвести операции по удалению таймера из очереди. На этом, к сожалению, преимущества перед другими типами таймеров заканчиваются, т.к. по всем остальным характеристикам (скорость инициализации, используемая под таймеры RAM, используемая под функции обработки ROM) они сильно проигрывают остальным типам.
Поэтому было принято решение доработать таймеры задач и статические таймеры. Для таймеров задач была добавлена возможность работать с ними в пользовательских целях (т.е. инициализировать время, а в произвольных местах задачи проверять таймауты), а также пользоваться ими из функций, не являющихся функциями-задачами (но обяательно вызванных из функций-задач). Для статических таймеров появилась возможность избавиться от жесткой привязки номеров таймеров. Т.е. появилась возможность назначать эти номера в ходе выполнения программы, что сильно упрощает перенос модулей в другие проекты (правда, ценой добавления двух функций, т.е. ценой ROM)
Итак, большое количество типов таймеров - это результат эволюции OSA, усложненный совместимостью с программами, написанными с предыдущими версиями OSA.
Реально для большинства проектов можно ограничится таймерами задач. Единственное их ограничение - невозможность отслеживать значение одного таймера двумя и более задачами. Но они вполне могут справиться с большинством возлагаемых на контроллер задач по контролю за временем.
Тем не менее, по указаным выше причинам (повторю: контроль одного таймера несколькими задачами; параллельный отсчет нескольких задержек одной задачей) иногда есть необходимость применить дополнительные таймеры. Учитывая, что со временем этих таймеров образовалось слишком много, возникает путаница и вопросы, какой лучше. Сразу скажу, что нет смысла применять несколько типов таймеров параллельно (таймеры задач - не в счет). Т.е. не нужно включать и статические и динамические и очередь только потому, что у каждого из них есть какие-то преимущества и для конкретной задачи подойдет именно такой, а не другой. Достаточно использовать таймер задач + один тип таймера из пользовательских.
На мой взгляд, учитывая то, что динамические таймеры медлительны, очередь таймеров прожорлива, а статические таймеры получили возможность назначаться в ходе выполнения программы, я бы советовал использовать именно статические таймеры. Динамические уже устарели, а очередь есть смысл применять только в одном случае: когда в программе используется большое количество таймеров. В добавок можно сказать, что иногда с целью экономии ресурсов можно отказаться от таймеров задач в пользу статических таймеров. Например в программе есть 5 задач, а таймер нужен только двум из них. Получается, что 6 байт (3 таймера по 2 байта) будут висеть мертвым грузом. В таком случае есть смысл отключить таймеры задач (вернее, не включать их), а использовать 2 статических таймера.
Что можно хорошего сказать о динамических таймерах, т.е. почему я их окончательно не перевел в устаревшие? У них есть одно преимущество, которое иногда может оказаться полезным: они продолжают счет даже после переполнения, что позволяет отмерять серию временных интервалов более точно без потерь на ожидание задачи стать готовой с высшим приоритетом.
Выбор типа таймеров должен быть сделан исходя из возлагаемых на них задач, их количества, частоты переполнения. Ниже я для примера привел таблицу, в которой постарался описать формулу вычисления ресурсопотребления. Для примера я рассмотрел результаты, полученные для PICC18 при размерности таймеров = 2 байта (результаты для других компиляторов будут отличаться, но общая картина останется такой же).
TTIMERS | STIMERS | DTIMERS | QTIMERS | |||
---|---|---|---|---|---|---|
(speed)* | (size)* | (speed)* | (size)* | |||
ROM, words | t*6+30 | 57 | t*4 (+60)* * | 14 (+60)* * | 74 | 482 |
RAM, bytes | t*2 | t*2 (+t/8)* * | t*4+2 | |||
OS_Timer, cycles | t*4+To*1 | t*15+Ta*6+To*2+4 | t*3+Ta*1 | t*8+Ta*3+3 | t*9+Ta*13+5 | 17+To*22 |
* - таймеры задач и статические таймеры можно оптимизировать по скорости (по умолчанию) или по объему ROM, включив в файл конфигурации константы OS_TTIMERS_OPTIMIZE_SIZE или OS_STIMERS_OPTIMIZE_SIZE
* * - при включенном механизме распределения статических таймеров добавляется 60 слов ROM и по 1 байту RAM на каждую неполную восьмерку таймеров
Здесь:
Т.к. в таблице фигурируют три переменных, то и выбирать тип таймера нужно по трем параметрам: общее количество таймеров, количество активных таймеров и частота переполнений. Что-такое общее количество таймеров, понятно, - это сколько их всего присутствует в программе (например определено 12 переменных типа OST_DTIMER, следовательно, t = 12); количество активных таймеров - это сколько таймеров считают в данный момент. Например, при 12 определенных переменных типа OST_DTIMER мы в список работающих добавили только 2 (в этом случае Ta=2); частота переполнения - это как часто таймеры будут досчитывать до 0, т.е. до момента, когда OS_Timer, помимо умешьшения счетчика таймера, должен будет еще установить бит переполнения (а в случаях с QTIMER еще и удалить таймер из очереди). Но этот параметр в виде частоты усложнит формулу, поэтому я ограничился параметром To, который показывает мгновенное (т.е. на текущее выполнение сервиса OS_Timer) количество таймеров, которые переполняются одновременно.
Окончательный выбор таймера зависит от критерия, по которому мы будет оптимизировать программу. Например, если требуется экономия RAM, то желательно выбирать статические таймеры; если при большом количестве таймеров требуется высокая скорость их обработки, - очередь таймеров. Ниже приведен пример расчета ресурсов, занимаемых пятью таймерами и пятьюдесятью.
TTIMERS | STIMERS | DTIMERS | QTIMERS | |||
---|---|---|---|---|---|---|
(Speed) | (size) | (speed) | (size) | |||
ROM, words | 60 | 57 | 80 | 74 | 74 | 482 |
RAM, bytes | 10 | 11 | 22 | |||
OS_Timer, cycles min/cycles max* | 20 (25) | 109 (119) | 20 (20) | 58 (58) | 50 (115) | 17 (127) |
* - отмечено время выполнения OS_Timer без переполнений (т.е. все таймеры уменьшились на 1, но ни один не досчитал до нуля), в скобках отмечено максимальное время выполнения, когда все таймеры переполнились одновременно (переполнения возникают тем чаще, чем меньшие значения загружаются в таймер)
Как видно из таблицы, самыми компактными по объему кода и по использованию RAM являются таймеры задач и статические таймеры. По скорости примерно равны таймеры задач и статические таймеры с оптимизацией по скорости, а также очередь таймеров. В данном случае выбор очевиден в пользу таймеров задач или статических с оптимизацией по скорости.
Также хочу обратить внимание, что оптимизация статических таймеров и таймеров задач по объему кода неэффективна при маленьком количестве таймеров. Как видно, при 5 таймерах объем используемой ROM почти равен, зато заметен проигрыш в скорости при использовании оптимизации по объему.
В общем, из таблицы видно, что при малом количестве таймеров проблема выбора типа таймера неактуальна. Можно выбирать любой (разве что только QTIMERS в заментом проигрыше из-за большого объема используемой ROM).
TTIMERS | STIMERS | DTIMERS | QTIMERS | |||
---|---|---|---|---|---|---|
(Speed) | (size) | (speed) | (size) | |||
ROM, words | 330 | 57 | 260 | 74 | 74 | 482 |
RAM, bytes | 100 | 107 | 202 | |||
OS_Timer, cycles min/cycles max* | 200 (250) | 1054 (1154) | 200 (200) | 553 (553) | 1105 | 17 (1122) |
* - отмечено время выполнения OS_Timer без переполнений (т.е. все таймеры уменьшились на 1, но ни один не досчитал до нуля), в скобках отмечено максимальное время выполнения, когда все таймеры переполнились одновременно (переполнения возникают тем чаще, чем меньшие значения загружаются в таймер)
Теперь видно, что правильный выбор типа таймера и режима его работы сэкономит нам какой-нибудь ресурс. Например видно, что самый экономный по ROM и RAM - таймер задач с оптимизацией по объему. Но он сильно проигрывает по скорости, будет расточительством при каждом вызове OS_Timer зависать в прерывании на тысячу тактов. Зато мы видим, что очередь таймеров считает так же быстро, как и при малом количестве таймеров. Есть одно "но", я уже о нем упоминал: когда первый в очереди таймер переполняется, операции по его удалению займут время. Поэтому, чем реже переполняются таймеры, тем быстрее будет среднее время выполнения OS_Timer (другими словами, если предполагается использование таймеров для отсчета небольших интервалов, то очередь может оказаться не такой эффективной).