Arduino

технологии

Что лучше: millis() или delay()

Такая, казалось бы, простая и часто используемая функция, как delay(), может являться источником трудноуловимых ошибок. Давайте рассмотрим, когда ее можно использовать, а когда нет.

Итак, как работает функция delay()? Она останавливает выполнение программы на заданное количество миллисекунд. Например, если нам нужно, чтобы спикер пропищал 1 секунду, мы напишем следующий код:

void loop()
{
  tone(speakerPin, 1000);
  delay(1000);
  noTone(speakerPin);
  ...

Или нам нужно опрашивать какой-либо датчик 1 раз в 5 секунд:

void loop()
{
  value = sensor.getValue();
  delay(5000);
}
Все это будет работать правильно. Но давайте немного усложним задачу. Например, нам нужно раз в 5 секунд получать данные с датчика, а по нажатию на кнопку, выводить их на экран:
void loop()
{
  ...
  value = sensor.getValue();
  buttonState = digitalRead(BUTTON_PIN);
  if (buttonState == HIGH) {
    display.show(value);
  }
  ...
  delay(5000);
}

И вот тут мы столкнемся с ситуацией, когда наша кнопка не всегда отрабатывает по нажатию. Обратимся к документации :

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

Это означает, что если во время выполнения функции delay() будет нажата кнопка, то наш код не отработает. Причем, если при задержке в 5 секунд, как в примере, мы это увидим сразу, то если задержка будет небольшая, то нажатия не будут срабатывать только в редких случаях. А если код скетча будет сильно сложный, то ошибку найти будет довольно непросто.

Итак, что же делать? И тут нам на помощь приходит замечательная функция millis().
Из документации:

Функция возвращает количество миллисекунд, прошедших с момента запуска текущей программы. Это число будет переполнено (обнулится) примерно через 50 дней.

Перепишем код последнего примера на ее использование:

#include <Arduino.h>
...
#define LOOP_MILLIS 5000
...
void loop()
{
  static unsigned long lastLoop = 0;
  ...
  unsigned long now = millis();
  if (lastLoop > now) {
    lastLoop = 0;
  }
  ...
  if (now > (lastLoop + LOOP_MILLIS)) {
    value = sensor.getValue();
    lastLoop = now;
  }
  ...
  buttonState = digitalRead(BUTTON_PIN);
  if (buttonState == HIGH) {
    display.show(value);
  }
}

Итак, поясню, что же здесь происходит. В LOOP_MILLIS мы помещаем требуемое время задержки - 5000 миллисекунд.
В начале функции loop() на строке 7 мы определяем статическую переменную lastLoop и инициализируем ее нулем. Обратите внимание, что значение 0 будет ей присвоено только при первом вызове функции loop() (советую ознакомиться со статьей про статические переменные и преимущества их использования).
Далее (строчка 9) создается уже обычная локальная переменная now, в которую помещается значение миллисекунд, прошедших с момента запуска скетча. Строчки 10-12 пока пропускаем. И на строке 14 видим условие, в теле которого мы вызывает действия, которые нужно повторять каждые 5 секунд, и перезаписываем переменную lastLoop текущим значением переменной now. Таким образом мы сохранили время последнего вызова нашей операции. И далее, она будет вызвана тогда, когда с момент последнего ее вызова прошло 5 секунд (что и означает условие на строчке 14).
Теперь вернемся к строчкам 10-12. Вспомним, что в документации по функции millis() было написано, что примерно раз в 50 дней происходит переполнение и функция вновь будет возвращать значения начиная с 0. Поэтому необходимо проверить, что произошло переполнение (только при переполнении значение переменной lastLoop, которая содержит время последнего запуска, может быть больше, чем значение переменной now), и также обнуляем переменную lastLoop. Если этого не сделать, то условие на строке 14 просто перестанет выполняться, так как now всегда будете меньше, чем lastLoop.