Замена delay() для неблокирующих задержек в Arduino IDE

Первое, с чем сталкивается осваивающий Arduino новичок, это неприятное свойство функции delay () — блокирование выполнения программы. Множество примеров в интернете используют эту функцию, но практическое применение как-то намекает, что лучше без неё обойтись.

Как и положено начинающему, я изобрёл велосипед сделал свою реализацию неблокирующей задержки. Задача стояла так:

  • Обеспечить псевдомногозадачность, чтобы разные события происходили в своё время, со своими интервалами и не блокировали друг-друга.
  • Было удобно этим пользоваться.
  • Можно было оформить как библиотеку и легко включать в другие проекты без копипастов.

Подсмотрев, что большинство ардуинских библиотек сделаны с применением ООП, я тоже решил не выделываться и написал класс SmartDelay, который можно получить с гитхаба как zip для добавления в Arduino IDE или сделать git clone в ~/Arduino/libraries/

В результате получилось вот такое.

#include 

SmartDelay foo(1000000UL); // в микросекундах

void loop () {
  if (foo.Now()) {
    // Код здесь выполняется каждый интервал в микросекундах, указанный в конструкторе выше.
  }
  //Прочий код
}

Метод Now () возвращает true, если интервал прошёл. В этом случае отсчёт начинается снова на тот же интервал. То есть, Now () каждый раз «перезаряжается» автоматически.

Классическое мигание светодиодом можно сразу усложнить до мигания двумя. Например, лампочки подключены к ножкам 12 и 11, должны мигать с интервалом в 1с и 777 мс соответственно.

#include 

SmartDelay led12(1000000UL); 
SmartDelay led11(777000UL);

setup () {
  pinMode(12,OUTPUT);
  pinMode(11,OUTPUT);
}

byte led12state=0;
byte led11state=0;

void loop () {
  if (led12.Now()) {
      digitalWrite(12,led12state);
      led12state=!led12state;
  }
  if (led11.Now()) {
      digitalWrite(11,led11state);
      led11state=!led11state;
  }
}

В цикле можно выполнять ещё что-то, мигание светодиодов не будет блокировать выполнение этого кода.

Понятно, что это не полная замена delay (), который останавливает поток на заданное время, надо писать программу всегда как МКА (механизм конечных автоматов). То есть, хранить состояние и в зависимости от него переходить к нужному месту кода.

Старый вариант:

...
action1();
delay(1000);
action2();
delay(500);
action3();
...

Новый вариант:

byte state=0;
SmartDelay d();
...
switch (state) {
case 0: 
  action1(); 
  d.Set(1000000UL);
  state=1;
  break;
case 1:
  if (d.Now) {
    action2();
    d.Set(500000UL);
    state=2;
  }
  break;
case 2:
  if (d.Now) {
    action3();
    d.Stop();
    state=0;
  }
  break;
}
...

Метод Set (интервал) устанавливает новый интервал и возвращает старый. Просто посмотреть на интервал можно методом Get ();

Stop () останавливает обработку и Now () всегда возвращает false.

Start () возобновляет работу и Now () начинает работать как обычно.

Если надо притормозить подсчёт времени, но не останавливать совсем, то есть метод Wait (). Например, если мигает светодиод 12, а при нажатии кнопки не мигает, достаточно добавить вот такой код в loop () в примере с двумя диодами выше:

...
if (digitalRead(9)) led12.Wait();
...

Так, при высоком уровне сигнала на 9 ноге диод на 12 мигать не будет и продолжит, когда там появится 0.

Когда по такому «таймеру» отрисовывается экран, например, и параллельно обрабатываются кнопки, то бывает нужно перерисовать экран или часть сразу после нажатия на кнопку, а не ждать окончания интервала. Для этого служит метод Reset (), после которого следующий вызов Now () вернёт true. Например:

SmartDelay display(1000000UL);

void loop() {
  if (btClick()) display.Reset(); // ткнул в кнопку, надо отрисовать экранчик.
  if (display.Now()) screenRedraw(); // отрисовка экранчика.
}

Из багов я вижу только, что не учитывается переполнение счётчика микросекунд, а в остальном да, надо почистить код. Мне не нравится, как сделан Reset (), пока думаю.

Объектный подход мне понравился, позволяет спрятать весь код в библиотеку, в которую можно потом уже никогда не заглядывать. Теперь эта маленькая библиотечка живёт во всем моих проектах. :)

Проект на гитхабе вот: github.com/nw-wind/SmartDelay

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

  • 9 января 2017 в 20:43

    0

    http://playground.arduino.cc/Code/Timer1 Все остальное — костыли

    • 9 января 2017 в 20:53

      0

      Это из той же оперы, на самом деле. Всё-равно заставляет переписывать логику программы.
      В прерывании же надо что-то сделать и быстро выйти. С моим подходом нет такого жёсткого отграничения. Понятно, что это не реальное время и не многозадачность, так, припарка. Для ардуининых задач пока хватает :)
      Timer1 — сила, но…
      • 9 января 2017 в 21:28

        +1

        Не совсем.


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


        Во-вторых, вообще есть два пути: либо конечный автомат (state machine), либо многопоточность. Первый реализуется без каких-либо таймеров, часто используется во всяких устройствах типа микроволновки. Когда же устройству пора становится умнее, например, обрабатывать нажатия на кнопки и рисовать что-то на экран, первое время костыль вроде вашего smartDelay подойдет, но чем раньше от него отказаться, тем лучше. Благо, многозадачность делается в несколько строк кода. И еще больше благо — проверенных временем и миллиардами различных устройств реализаций предостаточно, тот же rtos (да, он больше про реальное время, но выпотрошить и достать только диспетчер задач из него можно).


        Ну и в третьих — обрабатывать нажатые кнопки тоже стоит по прерываниям. Но там все хитрее и в случае с ардуиной (уж не знаю, умеет ли она в прерывания на пинах, ATmega328, на чем она построена, вроде как умеет, но не на всех ножках) может быть достаточно проблематично и проще действительно влепить планировщик задач.


        Ну и планировщик задач на две (или больше заранее известные) задачи делается в 5 строчек кода.


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


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

  • 9 января 2017 в 20:57

    +1

    Всё придумано до нас!
    http://robotsbigdata.com/docs-arduino-timer.html
    Есть в ардуиновском library manager, так что даже с гитхаба качать ничего не надо.

© Habrahabr.ru