Сборник реализаций асинхронного программного таймера. Далее по тексту:
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();
}