View Categories

Программный таймер

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

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

void loop() {
    проверить_кнопку();
    опросить_датчик();
    мигнуть_светодиодом();
    обновить_дисплей();
}

Выполнять все эти задачи сотни тысяч раз в секунду нам не нужно: кнопку достаточно проверять например каждые 10 мс, датчик - 5 секунд, мигать светодиодом - раз в секунду, обновлять дисплей - каждые 100 мс. Если просто добавить соответствующий delay в каждую задачу - вся программа начнёт выполняться с периодом, равным сумме задержек. И если задаче датчика с самым большим периодом это особо не помешает, то вот опрос кнопки каждые 5 секунд - это беда: отловить нажатие с таким медленным опросом будет практически невозможно. Да и вся остальная программа начнёт работать некорректно.

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

Урок написан для объяснения принципов работы типовых конструкций с таймером. В реальных проектах пользуйтесь библиотеками, например GTimer

Таймер аптайма #

Одна из базовых вещей, которая есть почти в любом фреймворке или вручную написанной программе - возможность получить время, которое прошло с момента запуска программы, в случае с МК - с момента его включения. Такое время называется аптайм (uptime). На базе этой казалось бы примитивной возможности можно создать временную конструкцию практически любой сложности - всё что касается таймеров, тайм-аутов, измерения периодов времени и прочее прочее. В Arduino фреймворке тоже есть такая функция, даже целых две:

  • millis() - время с момента запуска в миллисекундах (мс, в 1 с - 1'000 мс)
  • micros() - время с момента запуска в микросекундах (мкс, в 1 с - 1'000'000 мкс)

Простейший тест:

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

void loop() {
    Serial.print(millis());
    Serial.print(',');
    Serial.println(micros());
    delay(1000);
}
  • millis() и micros() работают "в фоне" - счёт времени идёт независимо от delay-задержек и сложных вычислений. На некоторых платформах счёт идёт в прерываниях таймера, поэтому отключение прерываний будет влиять на точность аптайма - он начнёт отставать
  • Функции возвращают данные типа uint32_t (максимальное значение 4 294 967 295), то есть значение millis() переполнится через 49.7 суток, а micros() - через 1.2 часа, после переполнения счёт снова пойдёт с 0. По сути, это переполнение ни на что не влияет - нужно просто учитывать его при разработке алгоритмов

Измерение времени #

Основная идея использования аптайма - запоминание его текущего значения и дальнейшее сравнение с новым актуальным значением. Чтобы это работало корректно, нужно вычитать из аптайма прошлое значение и сравнивать результат с нужным временем - в таком случае переполнение не будет играть роли, т.к. вычисления производятся в беззнаковой арифметике. Например текущий аптайм - 1000 мс. Мы хотим отсчитать 500 мс. Запоминаем 1000 и ждём, когда разность аптайма и прошлого значения достигнет 500. Это произойдёт на значении аптайма 1500: 1500 - 1000 == 500:

Что случится, если переполнение произойдёт как раз между началом и концом периода? Например millis() равен 4 294 967 096. Через 200 мс он досчитает до 4 294 967 295 (макс. значение) и начнёт счёт с нуля, досчитает до 300. В беззнаковом вычислении 300 - 4294967096 == 500 и конструкция отработает период полностью корректно, переполнение абсолютно никак на неё не повлияет.

Переполнение ограничивает максимальный измеряемый период, в случае с millis() это ~50 суток. В реальных проектах таймеры с таким периодом уже никто не делает (используется источник времени), но всё равно это ограничение можно обойти - например сделать таймер на сутки и вести счётчик суток, хотя с типом uint32_t хватит даже счётчика секунд - его хватит на 136 лет.

Для точного измерения периодов времени длиной в несколько миллисекунд лучше использовать микросекундный таймер, т.к. миллисекундный таймер имеет точность в 1 мс - старт таймера может быть как в начале текущей миллисекунды, так и в конце неё:

Программный таймер #

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

Дополнительную теорию и другие примеры реализации таймеров можно найти в отдельном уроке

Пример таймера отложенного запуска - код начнёт вызываться через 1000 мс после сброса таймера:

uint32_t tmr;

void setup() {
    Serial.begin(115200);
    Serial.println("start");

    tmr = millis(); // сброс таймера
}

void loop() {
    if (millis() - tmr >= 1000) {
        // этот код начинает вызываться
        // через 1000 мс после старта
        Serial.println("hello");
    }
}

Таймер можно сбрасывать и после срабатывания - получится периодический таймер. Данный код выводит hello раз в секунду:

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

void loop() {
    static uint32_t tmr;

    if (millis() - tmr >= 1000) {
        tmr = millis();     // сброс таймера
        Serial.println("hello");
    }
}

Таким образом у нас получается типовая конструкция асинхронного "таймера на millis":

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

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

uint32_t myTimer1, myTimer2, myTimer3;

void setup() {}

void loop() {
    // таймер на 500 мс (2 раза в сек)
    if (millis() - myTimer1 >= 500) {
        myTimer1 = millis();
        // выполнить действие 1
    }

    // таймер на 333 мс (3 раза в сек)
    if (millis() - myTimer2 >= 333) {
        myTimer2 = millis();
        // выполнить действие 2
    }

    // таймер на 100 мс (10 раз в сек)
    if (millis() - myTimer3 >= 100) {
        myTimer3 = millis();
        // выполнить действие 3
    }
}

Вот так мы можем например 10 раз в секунду опросить датчик, два раза в секунду выводить показания на дисплей и три раза в секунду мигать лампочкой. Красота! Расписывать эту конструкцию каждый раз - не очень удобно, пользуйтесь библиотеками или классом таймера из сборника по ссылке выше.

Примеры #

Отложенный запуск #

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

uint32_t tmr;  // переменная таймера
bool state;    // состояние таймера

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);

    // включить светодиод
    digitalWrite(LED_BUILTIN, HIGH);

    // запустить таймер
    tmr = millis();
    state = true;
}

void loop() {
    // тайм-аут 3 секунды
    if (state && millis() - tmr >= 3000) {
        // остановить таймер
        state = false;

        // выключить светодиод
        digitalWrite(LED_BUILTIN, LOW);
    }
}

Мигаем светодиодом #

В прошлом уроке про конечный автомат мы сократили пример мигания светодиодом до одного delay. Теперь можно и вовсе избавиться от него - заменить на программный таймер из этой главы:

void blinkLED() {
    static uint32_t tmr;
    if (millis() - tmr >= 500) {
        tmr = millis();

        static bool flag;   // флаг состояния светодиода
        digitalWrite(LED_BUILTIN, flag = !flag);
        // инвертировать флаг и подать значение на пин
    }
}

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    blinkLED();
}

Таким образом мы превратили классическую блокирующую задачу мигания светодиодом в полностью асинхронную - она больше не тормозит остальную программу.

Переключение периода #

При помощи флага можно переключать преиод таймера, например чтобы сделать программный ШИМ сигнал. В уроке про ШИМ мы делали программный ШИМ на задержках, давайте переделаем под неблокирующую конструкцию на таймере с периодами 9 и 1 мс:

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    static uint32_t tmr;
    static bool flag;
    if (millis() - tmr >= (flag ? 9 : 1)) {
        tmr = millis();
        digitalWrite(LED_BUILTIN, flag = !flag);
    }
}

Светодиод горит ярко. Если поменять периоды местами - начнёт светить тускло. Для увеличения диапазона настройки можно перейти к микросекундам:

void loop() {
    static uint32_t tmr;
    static bool flag;
    if (micros() - tmr >= (flag ? 50 : 1000)) {
        tmr = micros();
        digitalWrite(LED_BUILTIN, flag = !flag);
    }
}

Получим уже в районе 1000 ступеней яркости.

Избавление от циклов #

Довольно часто бывает нужно сделать асинхронным цикл, особенно если он с задержками. Например следующий цикл for просто выводит счётчик с периодом 100 мс, и так по кругу:

void loop() {
    for (int i = 0; i < 10; i++) {
        Serial.println(i);  // выводит от 0 до 9
        delay(100);
    }
}

Чтобы развернуть такой цикл, достаточно вынести переменную-счётчик в область существования за суперцикл (глобально или статически), а инкремент и условие перенести в суперцикл. Получится своеобразная машина состояний на 10 состояний, при каждом вызове происходит переход в следующее состояние, а из последнего - в первое:

void loop() {
    static int i = 0;
    Serial.println(i);
    i++;
    if (i >= 10) i = 0;     // сброс в начало "цикла"
    delay(100);
}

От задержки здесь можно избавиться, применив периодический таймер:

void loop() {
    static uint32_t tmr;
    if (millis() - tmr >= 100) {
        tmr = millis();

        static int i = 0;
        Serial.println(i);
        i++;
        if (i >= 10) i = 0;
    }
}

Кода стало больше, появилось больше переменных в оперативной памяти - но он стал асинхронным.

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

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