Самый простой программный таймер

Если у вас стоит задача посчитать время от а до б, а потом выполнить определенные действия в связи с этим, то данный вариант вам вполне пригодится.
Самый весомый плюс у данной системы — нет необходимости в прерываниях.
Так же у данного таймера отсутствует весомый обработчик событий. Все необходимые вычисления происходят распределённо.


Итак, ближе к телу:

Код заголовка timer.h:
typedef unsigned short time_t;

typedef enum
{
  started = 0,
  stopped,
  first
}Timer_flag;

typedef struct{
  time_t start;
  time_t interval;
  Timer_flag flag;
}Timer_t;

void timer_set(Timer_t *t, time_t interval);
void timer_reset(Timer_t *t);
void timer_restart(Timer_t *t);
Timer_flag timer_expired(Timer_t *t);


код тела:

#include "timer.h"

void timer_set(Timer_t *t, time_t interval)
{
  t->interval = interval;
  t->start = get_time();
  t->flag = started;
}

void timer_reset(Timer_t *t)
{
  t->start += t->interval;
  t->flag = started;
}

void timer_restart(Timer_t *t)
{
  t->start = get_time();
  t->flag = started;
}

Timer_flag timer_expired(Timer_t *t)
{
  if(t->flag)
    return t->flag;
  if((time_t)(get_time() - t->start) >= (time_t)t->interval)
  {
    t->flag = stopped;
    return first;
  }
  return started;
}


Использование.

Чтобы таймер заработал, необходимо описать функцию get_time();
В этой функции, например, можно читать текущее значение аппаратного таймера. Да, просто читаем значение и все.
пример:

static inline unsigned short get_time(void)
{
  return TIM3->CNT;
}


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

Можно конечно настроить иначе, используя прерывание, и в самом прерывании инкрементировать некую переменную
void TIM3_Handler(void)
{
//..сброс флагов;
  ctr++;
}

и тогда get_time() должен возвращать значение той самой ctr

Ключевая функция этого таймера — timer_expired(). ее нужно вызывать перед тем, как требуется произвести действие после/до срабатывания того самого таймера, адрес структуры которого указываете в теле функции.

Теперь применение:


Timer_t t; //может быть локальной

{
//1. Цикличная повторяемость
  timer_set(&t, interval/*кол-во тиков*/);
  //....
  if(timer_expired(&t))
  {
    timer_reset(&t); //повторить срабатывание с прежним интервалом
                     //без учета задержки при обработке
    /*...something...*/;
  }

//2. Контроль ошибки по времени
  timer_set(&t, interval/*кол-во тиков*/); //время возникновения ошибки
  //....

  if(/*event*/)
  {
    timer_restart(&t); //сбрасываем таймер, оттягиваем время срабатывания
    /*...something...*/;
  }
  else
    if(timer_expired(&t))
       /*ошибка*/;//Событие долго не приходило.

//3. Первичное срабатывание
  switch(timer_expired(&t))
  {
  case started:
    //запущен;
    break;
  case first:
    //первая обработка срабатывания;
    break;
  case stopped:
    //остановлен;
    break;
  }
}


Пожалуй из важных замечаний, размерность time_t и размерность возврата функции get_time() должна совпадать, а так же быть беззнаковой. Только в этом случае у вас не будет проблем при переполнении счетчика.
Я использовал в данном примере unsigned short основываясь на размерности аппаратного таймера TIM3. Если использовать вариант с прерыванием, то можно легко увеличить до unsigned int.

Кстати говоря, у STM32 на некоторых корпусах имеются аппаратные 32-х битные таймеры, и если их настроить на 1 микросекунду, то можно получить совсем нехилую точность, а максимальный интервал задержки будет 4294 секунды (71,5 минут), что вполне хватит.

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

Поясняющие картинки:
timer_set:

timer_restart:

timer_reset:
  • -1
  • 28 июня 2016, 01:12
  • Mihail

Комментарии (37)

RSS свернуть / развернуть
У меня такая реализация. Ссылка.
0
typedef time_t unsigned short;
Я бы предпочел типы из stdint.h.
Timer_flag timer_expired(Timer_t *t)
{
  if(t->flag)
    return t->flag;

Во всех остальных местах используются символьные имена, лучше и тут иметь if(t->flag != started), полагаю, столь простой случай только TCC не оптимизирует.
Пожалуй из важных замечаний, размерность time_t и размерность возврата функции get_time() должна совпадать.
Кроме того, результат get_time должен быть unsigned.

Ну и не в пользу этого таймера хочется заметить, что выдержка временных интервалов им довольно неточная и сильно зависит от загруженности процессора. Подобные таймеры довольно популярны в самопальных игровых движках (для регулярного обновления состояния игры) и типичная проблема — неравномерное, дерганое движение объектов из-за неравномерности формируемых интервалов (большой джиттер, т.е.).
0
  • avatar
  • Vga
  • 28 июня 2016, 10:39
типы из stdint.h намеренно не применял, чтобы не инклюдить… Здесь я показал «теорию» работы данного таймера.

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

Поэтому данный таймер привлекает простотой и функциональностью.
0
if(t->flag != started)
Кстати, так раньше здесь и было. Убрал, потому что started в принципе должен быть равен нулю, потому как название функции timer_expired() как бы намекает, что она имеет ненулевой результат, когда сработает и нулевой, когда запущен.
0
 Подобные таймеры довольно популярны в самопальных игровых движках

Да не только в самопальных, во многих движках время обновления гуляет. В том же Unity нельзя полагаться на фиксированный интервал обновления, приходится всегда отталкиваться от фактического (Time.deltaTime). Или в более профессиональных движках по другому?
0
еще раз: данный таймер уступает по джитерру только в том случае, если обработчик вызывается напрямую из прерывания. Во всех остальных случаях джиттер такой же. А вот если использовать 32-битный аппаратный, настроить на 1 мкс, то можно делать задержки в десятках микросекунд особо не парясь, не считая такты.
0
Это не претензия к Вашему таймеру, я понимаю как он работает, плюсы и минусы такого подхода.

Меня заинтересовал комментарий коллеги Vga касательно игровых движков. У меня здесь не так много опыта, а он, ЕМНИП, работал в геймдеве.
0
Или в более профессиональных движках по другому?
Профессионалы рекомендуют делать одно обновление на один кадр, считая движение по дельте времени) Но фиксированный UPS сильно проще в обсчете.
0
Профессионалы рекомендуют делать одно обновление на один кадр, считая движение по дельте времени) 

Ну, да, я это и имел ввиду, в том же Unity  смещение обычно вычисляют как velocity * Time.deltaTime а не просто через константу приращения координаты.

Я думал, что в профессиональных движках просчет сцены отделен от рендеринга. В этом, ИМХО, есть смысл, например в сетевых играх, когда сцена должна быть синхронизирована с сервером, а рендеринг может (и будет) у каждого клиента тормозить по своему…
0
Апдейт от рендера отделен, да, но когда игрушка требовательная и работает на пределе возможностей железа (а для профессионального геймдева это типичная ситуация), то оптимален вариант привязки обновления к фреймрейту. Т.е., грубо говоря, работает примерно такой цикл:
while(running){
  Update(CurTime() - LastUpdate());
  Render();
}


Что до синхронизации в сетевых игрушках — это отдельная тема, и она сильно зависит от жанра. Для TBS и подобных вообще не особо важно время прихода пакета, бывают реализации до, несколько утрированно, отправки пакета при клике игрока и отображения реакции на клик по получении ответного пакета. А вот для сетевого шутера важно реальное время прихода пакета, так что обработается он в ближайшем апдейте. Для RTS, в свою очередь, самое важное — строгая синхронизация состояния игры на обоих сторонах, вплоть до одинакового порядка вызова random() на обеих сторонах :)
+1
то оптимален вариант привязки обновления к фреймрейту
P.S. Это из тех времен, когда процессоры были одноядерными. Многопоточная игрушка — совершенно отдельное веселье. Но, тем не менее, и там нет особого смысла обновлять чаще рендера (или наоборот).
0
Спасибо за информацию. Я думал, что в “профессиональных” движках апдейт сцены и рендеринг выполняются асинхронно, цикл из Update(), Render() — это удел более примитивных движков.
0
Асинхронно их имеет смысл выполнять только на многопоточных системах, но вообще игры довольно плохо распараллеливаются, и просто разделить «один поток обновляет, второй рендерит» неэффективно.
0
А вообще, кстати, сейчас доступны сырки достаточно крутых движков — как минимум Кармаковских по Tech 4 или Tech 5, и, ЕМНИП, какие-то из Unreal Engine и Cry Engine. VALVe обещает опенсорсный Source 2, но он еще не вышел вроде. Так что можно посмотреть, как оно сделано.
0
Я смотрел исходники Клармака (ЕМНИТ, DOOM III, когда он его опубликовал) — там не то что “без ста грамм не разобраться”, там даже «литра» мало :)

Мои скромные потребности в геймдеве полностью порывает Unity, хотя я не могу судить об эффективности данного движка, т. к. с другими я, по сути, не работал…
0
А ведь его код хвалили как достаточно хороший и чистый. Для UE класс с несколькими сотнями публичных членов — норма :)
0
А ведь его код хвалили как достаточно хороший и чистый.

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

З.Ы. Меня всегда тянуло в геймдев, одно время даже всерьез думал подтянуть нужные скилы и перейти на другую работу. Но пообщавшись с потенциальными коллегами — передумал. Ибо в мечтах хочется писать игры уровня ААА а по факту ребята сидят и струячат примитивные платформеры по несколько игр в месяц, пытаясь взять колличеством и в надежде на то, что когда-то одна из их игр «выстрелит».
0
Чтобы в нем разобраться — нужно большая мотивация и куча времени.
Смотря насколько разобраться. Если целью стоит только глянуть, как реализован цикл update-render, то задача сильно упрощается.
Ибо в мечтах хочется писать игры уровня ААА а по факту ребята сидят и струячат примитивные платформеры по несколько игр в месяц,
Ну, некоторые так струячат-струячат, а потом идут на работу в Square Enix, Ace Combat пилить. С другой стороны, в большой корпорации работа имеет свои минусы…
ААА сегодня, увы, уже не удел одиночек. Слишком большой объем работы. Но область все равно интересная, особенно если заниматься этим для души.
0
Ну, некоторые так струячат-струячат
Ошибка выжившего, на одного такого, который ушел в Square приходится сотни других (сравнимых по скилам) который таи и остаются пилить примитивные платформеры. В играх ошибка выжившего наиболее явно проявляется, ибо все знают много случаев когда примитивная по сложности игра вдруг «выстреливает»

Энтрерпрайз, например, полная противоположность. Там нет громких взлетов и падений, ты монотонно сидишь и годами пилишь одну программу (о существовании которой никто, кроме заказника, не знает). Там все стабильно, монотонно и ненапряжно. Но это так скучно … :(
0
Ошибка выжившего, на одного такого, который ушел в Square приходится сотни других (сравнимых по скилам) который таи и остаются пилить примитивные платформеры.
Ну, насчет «сравнимых по скилам» еще вопрос, тот чел, про которого я — действительно толковый (но при этом нельзя сказать, чтобы у него что-то «стреляло»). А так — многие нередко и отказываются, платформер может и примитивный, но свой.
0
А так — многие нередко и отказываются, платформер может и примитивный, но свой.
Ну, как минимум нужно, чтобы «свой» платформер хоть какие-то деньги приносил. Да и, как по мне, писать очередной «свой» платформер дело скучное, ибо программирования как такового там практически нет. Я не могу представить, что можно придумать сложного в платформерах или аналогичных по классу играх, коими забиты на 90% AppStore и Google Play.

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

Да и вообще, для меня загадочно выглядит политика компаний в геймдеве. Есть куча примеров, когда студия выпускает удачную игру (вот казалось бы успех) и потом распадается. Взять хотя бы Black Isle или GSC Game World

Вот поэтому я и сижу в уютненьком энтерпрайзе, который мне чертовски надоел, и облизываюсь на геймдев (но не в виде бадишопа) :)
0
Вот поэтому я и сижу в уютненьком энтерпрайзе, который мне чертовски надоел, и облизываюсь на геймдев (но не в виде бадишопа) :)
В таком случае самое время замутить стартап :)
0
Спасибо, похожее у меня реализовано, но как заметил Vga — точность +\- лапоть и это все убивает, точные задержки не сделать, а в них то же есть необходимость.
0
Не для всех задач важен джиттер (плюс борьба с ним довольно накладна, так что есть смысл таймеры с требованиями к джиттеру отрабатывать отдельной системой). Собсна, потому данный подход и популярен.
0
ну ежели есть необходимость в точных задержках, то тогда от прерываний не уйти. Тогда второй мой таймер вполне оправдан к применению.
0
Да, согласен, но второй таймер показался мне сложным, хотелось что то среднее между первым (простым) и вторым, но у меня пока не получилось, вот и смотрю в сторону Гуру.
0
я вот сейчас подумал… Данный таймер лишен еще одного недостатка относительно классической проверки флагов в мегафункции. Постоянное время задержки между обработкой флагов и их использованием.
0
Вообще в последнее время для медленных таймеров я использую именно эту конструкцию. Если события по нему обрабатываются в суперцикле, то нет совершенно никакой необходимости городить прерывания и флаги. Учитывать количество этих таймеров, грузить систему дополнительными прерываниями и прочим. В крайнем случае, минимальное прерывание с одним лишь счетчиком. А вот если нужны жесткие задержки, то тут без прерываний не обойтись совсем, и здесь подходят именно аппаратные таймеры. 1- 2 типа задержек всегда можно решить с помощью аппаратных таймеров. Тем более, что их наклепали предостаточное количество. Конечно все зависит от устройства, его нужд в кол-ве подобных точных задержках и прочих… Но чаще всего — их совсем немного или их нет вообще. Но если требуется много — никуда не денешься — нужно на прерываниях мудрить. Но таких устройств — 1% от общего числа.
0
А почему нельзя здесь системные тики использовать? Или я что то не понял?
0
какие системные тики? Вы имеете ввиду ОС?
0
системные тики — системного таймера (для этого он и предназначен — для отчетов времени), а не обычного из набора таймеров. И никаких ОС!
0
Если вы о таймере SysTick процессоров arm (нужно уточнять, что вы имеете ввиду), то использовать его неудобно поскольку он 24-х разрядный.
0
да о нем милом, только почему неудобно?
0
нет такой разрядности числа в си, только 16, 32. Поэтому его нужно будет приводить к 16-ти разрядному числу, что резко сократит диаппазон. Если использовать прерывание SysTick, то да, так же можно в прерывании завести счетчик. Но зачем козе баян, если можно считывать с 32-х разрядных таймеров значение напрямую? Впрочем, в каждом случае свое решение.
0
Поэтому его нужно будет приводить к 16-ти разрядному числу, что резко сократит диаппазон.
Так приведите к 32-ти разрядному, что бы не сокращать диапазон.
0
Mihail -у надо обязательно к 16 ;)
0
привести к 32-х не получится. Вы это поймете, если чутка подумаете.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.