View Categories

Таймер на uptime

Сборник реализаций асинхронного программного таймера. Далее по тексту:

  • tmr - переменная таймера типа uint32_t
  • prd - период таймера
  • millis() - аптайм в миллисекундах. Можно заменить на micros() для микросекундного таймера или другой реализацией (не Arduino)

Блокирующие конструкции #

Цикл с тайм-аутом #

Аналог задержки, в котором мы просто выполняем свой код указанное время:

uint32_t ms = millis();

while (millis() - ms < 5000) {
    // тут в течение 5000 миллисекунд вертится код
    // удобно использовать для всяких калибровок
}

Свой delay #

Самая простая конструкция - задержка, аналог delay. Нужно запомнить текущее время и ожидать в цикле, когда новое время станет больше запомненного на указанное время задержки:

void myDelay(uint32_t ms) {
    uint32_t prev = millis();
    while (millis() - prev < ms);
}

Данную функцию можно использовать вместо delay - по сути это то же самое (но delay точнее, т.к. мы можем попасть на функцию между миллисекундами). Аналогично можно сделать задержку и на micros().

Своей задержке можно добавить дополнительный функционал - например вызов функции во время цикла задержки:

void myDelay(uint32_t ms, void (*func)()) {
    uint32_t prev = millis();
    while (millis() - prev < ms) func();
}
myDelay(5000, [](){
    // выполняется во время задержки
});

Классический таймер #

Запуск/перезапуск таймера:

tmr = millis();

Основная конструкция:

if (millis() - tmr >= prd) {
    // сброс
    // выполнить действие
}

Конструкция корректно проходит переполнение аптайма

Сброс тип 1 #

if (millis() - tmr >= prd) {
    tmr = millis();
    // выполнить действие
}
  • "Уходит" с фазы, если в коде есть задержки и прочие блокирующие участки, во время выполнения которых uptime успевает увеличиться на время, большее периода таймера. Это может быть проблемой например для счёта времени и других похожих ситуаций, когда фаза таймера не должен смещаться
  • Если заблокировать выполнение кода на время, большее чем один период - алгоритм просто скорректирует эту разницу, так как мы сбрасываем его актуальным значением uptime

Сброс тип 2 #

if (millis() - tmr >= prd) {
    tmr += prd;
    // выполнить действие
}
  • Конструкция всегда идёт по фазе, даже если в коде присутствует задержка: время следующего срабатывания с момента старта таймера всегда кратно периоду
  • Если таймер пропустит период - он "сработает" несколько раз подряд, пока не догонит время

Сброс тип 3 #

uint32_t left = millis() - tmr;

if (left >= prd) {
    tmr += prd * (left / prd);
    // выполнить действие
}
  • Данная конструкция позволяет соблюдать фазу, но сбрасывать пропущенные вызовы
  • Целочисленное деление left / prd позволяет получить целое количество переполнений с отбрасыванием остатка, поэтому скобки стоят именно так
  • К переменной таймера прибавляем период, умноженный на количество переполнений. Если вызов таймера не был пропущен - произойдёт умножение на 1

Можно оптимизировать деление:

uint32_t left = millis() - tmr;

if (left >= prd) {
    tmr += prd * ((left >= prd * 2) ? (left / prd) : 1);
    // выполнить действие
}

Возможно это и будет самой лучшей конструкцией "не уходящего" таймера со сбросом пропусков.

Сброс тип 4 #

if (millis() - tmr >= prd) {
    do {
        tmr += prd;
        if (tmr < prd) break;       // переполнение uint32_t
    } while (tmr < millis() - prd); // защита от пропуска шага
    // выполнить действие
}

Тоже интересный вариант - соблюдает фазу, сбрасывая пропущенные срабатывания (деление заменено циклом).

Облегчение таймера #

Переменная таймера не обязательно должна быть uint32_t - если период таймера укладывается в 2 байта (до 65 535) или 1 байт (до 255), то для экономии оперативки можно брать переменную соответствующего типа и привести к нему аптайм и вычисления. Например:

static uint16_t tmr;

if ((uint16_t)((uint16_t)millis() - tmr) >= prd) {
    tmr = millis();
    // выполнить действие
}
static uint8_t tmr;

if ((uint8_t)((uint8_t)millis() - tmr) >= prd) {
    tmr = millis();
    // выполнить действие
}

В данном случае "переполнение" будет происходить гораздо раньше - соответственно максимальному значению типа. Но таймер корректно проходит это переполнение

Первый запуск #

Чтобы таймер сработал сразу же, а затем через период, можно использовать следующую конструкцию:

if (!tmr || millis() - tmr >= prd) {
    // сброс
}

Таймер на остатке от деления #

В Сети часто можно встретить вот такую конструкцию: условие выполняется, когда остаток от деления millis() на период равен нулю:

if (millis() % prd == 0) {
    // выполнить действие
}

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

  • Операция "остаток от деления" довольно тяжёлая и медленная, размещение нескольких таких таймеров в основном цикле сильно замедлит программу
  • В реальной программе может создаться задержка продолжительностью дольше 1 мс и существует довольно высокий риск пропуска срабатывания такого таймера
  • В то же время, условие срабатывания таймера будет верно целую миллисекунду и действие может выполниться несколько раз подряд, что недопустимо в большинстве случаев

Таймер с опережением #

Часто можно встретить "таймер на миллис" такого вида:

// запуск/перезапуск таймера
tmr = millis() + prd;
// проверка срабатывания
if (millis() >= tmr) {
    tmr = millis() + prd;
    // выполнить действие
}
  • Визуально эта конструкция легче классической, так как экономит одно вычитание. Это действительно так: экономия составляет ровно 1 такт процессора (на AVR): классическая конструкция выполняется условно 18 тактов, а эта - 17, что незначительно
  • Конструкция некорректно переходит через переполнение аптайма: когда переменная таймера переполнится, миллис будет больше неё вплоть до своего переполнения и условие будет верно всё это время

Не используйте данную конструкцию в проектах, которые могут работать больше 50 суток без перезагрузки

Улучшение #

Можно улучшить данную конструкцию, чтобы она переходила через переполнение:

// запуск/перезапуск таймера
tmr = millis() + prd;
if (millis() >= tmr) {
    if (millis() - tmr >= UINT32_MAX - prd) return; // защита от переполнения

    tmr = millis() + prd;   // сброс тип 1
    // tmr += prd;          // сброс тип 2
    // выполнить действие
}
  • Два типа сброса - такие же как у классического таймера: второй соблюдает фазу и может привести к нескольким вызовам после пропуска, первый - нет
  • В такой конструкции период не должен быть длиннее чем максимальное значение типа / 2

Для облегчения переменной таймера нужно привести всё к нужному типу и указать соответствующий максимум:

// запуск/перезапуск таймера
tmr = (uint16_t)millis() + prd;
if ((uint16_t)millis() >= tmr) {
    if ((uint16_t)millis() - tmr >= UINT16_MAX - prd) return;
    // сброс
    // выполнить действие
}

Тайм-аут #

Если нужно, чтобы асинхронный таймер сработал один раз (без перезапуска) - можно ввести bool флаг состояния таймера:

if (flag && millis() - tmr >= prd) {
    flag = false;
    // выполнить действие
}

Для запуска такого таймаута нужно сделать:

flag = true;
tmr = millis();

Для экономии памяти и упрощения кода можно использовать саму переменную таймера в качестве флага:

if (tmr && millis() - tmr >= prd) {
    tmr = 0;
    // выполнить действие
}

Для запуска такого таймаута нужно сделать:

tmr = millis();
if (!tmr) --tmr;    // защита от значения 0

Готовые конструкции #

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

Класс таймера #

Минимальный класс периодического таймера, будем использовать в рамках этих уроков и примеров. Его можно выделить в отдельный файлик Timer.h и подключать в проекты, либо просто скопировать сам класс в скетч (без pragma и include):

// === Timer.h ===
#pragma once
#include <Arduino.h>

class Timer {
  public:
    Timer(uint32_t prd = 0, bool start = false) : _prd(prd) {
        if (start) Timer::start();
    }

    void setTime(uint32_t ms) {
        _prd = ms;
    }

    void start() {
        _tmr = millis();
        if (!_tmr) --_tmr;
    }

    void stop() {
        _tmr = 0;
    }

    bool tick() {
        if (_tmr && millis() - _tmr >= _prd) {
            start();
            return true;
        }
        return false;
    }

    operator bool() {
        return tick();
    }

  private:
    uint32_t _tmr, _prd;
};

Использовать можно так:

Timer timer;

void setup() {
    Serial.begin(115200);

    timer.setTime(500);
    timer.start();
}

void loop() {
    // timer 1
    if (timer) Serial.println("timer!");

    // timer 2
    static Timer period(1000, true);
    if (period) Serial.println("period!");

    // timer 3
    static Timer timeout(2000, true);
    if (timeout) {
        timeout.stop();
        Serial.println("timeout!!! ============");
    }
}

Библиотека таймера #

В прошлой версии уроков была рекомендована моя библиотека TimerMS, но я её переделал и получилась библиотека GTimer - она более лёгкая и более универсальная - работает с millis и micros, позволяет выбрать тип сброса, имеет 3 режима работы и подключение обработчика. Можно установить из менеджера библиотек и использовать в проектах и экспериментах. Примеры:

// обычный
#include <Arduino.h>
#include <GTimer.h>

GTimer<millis> tmr1;

void setup() {
    Serial.begin(115200);

    tmr1.setMode(GTMode::Timeout);
    tmr1.setTime(2000);
    tmr1.start();
}

void loop() {
    if (tmr1) Serial.println("timeout");

    static GTimer<millis> tmr2(500, true);
    if (tmr2) Serial.println("interval");
}
// обработчик
#include <Arduino.h>
#include <GTimer.h>

GTimerCb<millis> tmr1, tmr2;

void onTimer() {
    Serial.println("ready 2");
}

void setup() {
    Serial.begin(115200);

    // лямбда
    tmr1.startInterval(500, []() {
        Serial.println("ready 1");

        // обращение к текущему таймеру
        // static_cast<GTimerCb<millis>*>(thisGTimer)->stop();
    });

    // внешний
    tmr2.startInterval(1000, onTimer);
}

void loop() {
    tmr1.tick();
    tmr2.tick();
}
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх