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

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

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

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

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

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

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

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

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

    #include <SmartDelay.h>
    
    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

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

    Имело смысл это делать?
    Поделиться публикацией

    Похожие публикации

    Комментарии 47
      +4

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

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

          Не совсем.


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


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


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


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


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


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

            0
            Я могу ошибаться, но помоему у UNO всего 2 прерывания (hardware).
              0
              так и есть, но есть нюанс, там есть три группы прерываний и можно назначить им обработчик. Погугли changeInterrupt или как-то так. Но обработчик ставится на группу и внутри него надо разбираться, какая нога дёрнула, это делается не очень переносимым кодом. Не ардуинский стиль получается.
                +1
                Это прерывания по приходу внешних сигналов. Всего же векторов прерываний куда больше. Только надо иметь в виду, что прерывания таймеров могут использоваться какими-то библиотеками, которые «застолбили» их за собой.
                0
                rtos да, но она тоже память ест. Речь не идёт о чём-то серьёзном и да, для микроволновки или теплицы сгодится. Для промышленного применения, возможно, не стоит и ide ардуиновской пользоваться, верно?

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

                Я думаю перейти с микросекунд в более удобные миллисекунды, практика показала, что лишние нули лично мне не нужны, кстати.

                Перспективы же роста — перейти на stm32 :)
                  0
                  Ардуиной вообще не стоит пользоваться если проект предполагает серию. Но для одиночных изделий вполне сносно.

                  p.s. Исключение когда проект делается любителем ардуино, для таких же ардуинщиков там может быть и серия.
                    0
                    +100500
                    ППКС :)
              0
              Ардуино вообще по сути набор костылей над atmega328.
              И если уж изучать работу таймера, то лучше таки по гайду типа такого: Newbie's Guide to AVR Timers © Dean Camera ( http://www.github.com/abcminiuser/avr-tutorials/blob/master/Timers/Output/Timers.pdf?raw=true )
                +1
                Ардуино — это кубики для начинающих и для простых поделок. Для этого прокладка ардуины, его абстракция над железом заслуживают всяческого уважения и поощрения. :)
              +4
              Всё придумано до нас!
              http://robotsbigdata.com/docs-arduino-timer.html
              Есть в ардуиновском library manager, так что даже с гитхаба качать ничего не надо.
                +3
                Чёрт! Там даже протокол такой же. Пойду убью себя ап стену… :(
                0
                О-хо-хонюшки, а не проще ли запилить «многозадачность» в духе RTOS?
                Ну, т.е. — main loop, и отдельные «задачи», вызываемые по таймеру — каждую секунду,
                каждые 100мс, 10мс, etc.
                Понятно, что это не настоящая многозадачность — «задачи» должны сами успевать завершаться до следующего цикла, но, тем не менее…
                  0
                  Задача каждая должна укладываться в квант времени. Здесь реализована ленивая кооперативная многозадачность, которой часто и так хватает, а частенько именно её и надо.
                    0
                    Только в этой «многозадачности» не видать «задач».
                    А так, написать некий кусок кода без явно выделенного шедулера в том или оном виде, и назвать его «многозадачностью» — можно.
                    Но зачем?
                    0
                    Почему бы тогда не взять rtos?
                      0
                      Да лучше, наверное.
                      Но люди, зачем-то, изобретают велосипеды — я этот факт не обсуждал.
                        0
                        Можно и ртос, если влезет. Я с нею ещё не играл.
                        Боюсь, что «взять ртос» примерно равно «забыть про arduino ide».
                          0

                          https://www.hackster.io/feilipu/using-freertos-multi-tasking-in-arduino-ebc3cc


                          Для ардуино есть несколько портов free rtos. Да и чему там не влазить? Если не брать всякие аллокаторы и прочее.

                            0
                            Есть, я попробовал уже. Сделаю так и так, посмотрю, что получится, можно сравнить будет.
                            У меня на подходе конкретное изделие :) как раз.
                      0
                      Таймер это хорошо, но помимо времени бывает нужно неблокируеще ждать ответы от других устройств, уровень на ножке и т.д. Мне для этих целей нравится библиотека Protothreads. Портируется в Ардуину за 2 клика, и получаем почти что ОСРВ кооперативного типа.
                      Пример из практики: нужно сделать так, чтоб кнопка срабатывала при нажатии большем 3 сек, при этом основная программа не блокировалась в проверке уровня на кнопке.

                      //в основном цикле:
                      main()
                      {
                      //bla bla bla
                      PT_SCHEDULE(ScanKey(&ScanKey_pt)); //неблокирующее сканирование клавиатуры
                      //bla bla bla
                      }
                      
                      //сама функция проверки, преобразуется из обычной функции в неблокирующую 3-мя строчками кода.
                      //void ScanKey(void)
                      PT_THREAD(ScanKey(struct pt *pt))
                      {
                      PT_BEGIN(pt);
                      
                      	if (keys.SW3)
                      	{
                      		timer_set(&timer_service, KEY_DELAY);
                      		PT_WAIT_WHILE(pt, ((!SW3) && (timer_expired(&timer_service)==0))); // "ждем" пока кнопку удерживают и не истёк таймаут
                      	
                      
                      		if(timer_expired(&timer_service) && (!SW3))
                      		{
                                              //время вышло а кнопка нажата-всё ОК
                      			Regim=1;	
                      		}
                      		else Regim=0;	
                      	}	
                      
                      
                      PT_END(pt);
                      }//eof keys
                      
                        0
                        Хммм… Пойду там макросы потрошить. Интересно.
                        0
                        в целом направление правильное. это путь в сторону state машины. а чистым таймером в случае если хочется асинхронного выполнения все равно не обойдешься.
                          0
                          Спасибо.
                          state-machine под это будет в следующей статье. Я там с клавиатурой разбирался и в итоге получился абстректный класс с МКА внутри. Сегодня/завтра опубликую.
                          0
                          Я предпочёл эдакую кооперативную многозадачность (здравствуй, Windows 3.1 :) ).
                          https://github.com/emelianov/Run
                          Использование прерываний это не отменяет: в прерывании выставил флаг, а в основном цикле выполнил ресурсоемкую часть.
                            0
                            Использование таймера заставляет всё делать с ним, то есть, нельзя абстрагироваться от него.
                            Ээээ…
                            Ну, если я спрячу таймер1 внутри своего класса, а потом захочу в самом скетче им попользоваться (я же не знаю потроха класса SmartDelay), то код превратится в тыкву.
                            В задачах как раз стояло сделать нечно, что можно спрятать, подключить и забыть про код внутри. Ардуино-стайл некий.
                              0
                              Поздравляю, Вы изобрели очередную простенькую кооперативную многозадачность.
                              Основная проблема здесь в том, что семантика его использования достаточно страшная и непонятная.
                              Если сделать нечто наподобие такого (код большой, прячу под спойлер и отбрасываю много чего)
                              Фрагмент .h-файла
                              #define TASK_CLASS(TypeName) TypeName
                              
                              #define TASK_BEGIN(TypeName, Locals) class TASK_CLASS(TypeName) : public StatefullTaskBase { \
                              private: \
                              	struct Locals; \
                              public: \
                              	virtual bool Step() override { \
                              switch (this->state) {	\
                              case -1: return true; \
                              case 0:
                              
                              
                              #define TASK_BODY_END ;} return true; }
                              #define TASK_CLASS_END };
                              
                              #define TASK_END TASK_BODY_END TASK_CLASS_END
                              
                              #define TASK_YIELD() this->state = __LINE__; return false; case __LINE__:
                              
                              #define TASK_WAIT_FOR(Object) this->WaitFor(Object); this->state = __LINE__; return false; case __LINE__:
                              
                              #define TASK_YIELD_WHILE(cond) this->state = __LINE__; case __LINE__: if ((cond)) return false;
                              
                              
                              #define SECOND *1000LL
                              #define SECONDS SECOND
                              #define MINUTE *(60LL*1000LL)
                              #define MINUTES MINUTE
                              #define HOUR *(3600LL*1000LL)
                              #define HOURS HOUR
                              
                              #define TASK_SLEEP(timeout) this->sleep.Start(timeout); TASK_WAIT_FOR(&this->sleep);
                              
                              #define TASK_PERIODICALLY(period, action) for (;;) {this->sleep.Start(period); action; TASK_WAIT_FOR(&this->sleep);}
                              
                              #define TASK_POLL(action) for(;;) {action; TASK_YIELD();}
                              
                              #define TASK_WAIT_CONDITION(callback) TASK_WAIT_FOR(callback)
                              
                              #define TASK_WAIT_SIGNAL(hSignal) TASK_WAIT_FOR(hSignal)
                              
                              #define TASK_SET_SIGNAL(hSignal) hSignal->Set()
                              
                              #define TASK_WAIT_VALUE(hValueHolder, variable) TASK_WAIT_FOR(hValueHolder);  variable = hValueHolder->Get();
                              
                              #define TASK_SET_VALUE(hValueHolder, value) hValueHolder->Set(value);
                              
                              

                              , то можно делать независимые таски в таком духе:
                              Пример таски
                              DEFINE_TELEMETRY(PowerMonitorRecord)
                              {
                              	u16 Value;
                              };
                              
                              TASK_BEGIN(PowerMonitorTask, {})
                              TASK_PERIODICALLY(5 SECONDS,
                              	telemetry << CreateRecord(GetState())
                              );
                              TASK_BODY_END
                              
                              
                              PowerMonitorRecord GetState()
                              {
                              	return PowerMonitorRecord{ 0 };
                              }
                              
                              
                              TASK_CLASS_END
                              


                              Да, макросы, но результат явно менее страшный, чем кодирование руками конечного автомата.
                                0
                                Спасибо за макросы, я как раз ломаю голову над ними, чтобы заменить if(obj.Now) { действие }
                                С другой стороны, так тоже ничего, понятно.
                                  0
                                  Обратите внимание на конструкцию вида
                                  bool TaskFunc(int &state)
                                  {
                                  switch (state)
                                  {
                                  case 0: // начальное
                                  //
                                    state = __LINE__; return false; case __LINE__: // в одну строку.
                                  default:
                                    state = -1;
                                    return true;
                                  }
                                  }
                                  
                                  +1
                                  Кстати, в той реализации, которую Вы здесь видите, у таски есть состояния «активна», «заблокирована» и «завершена» и планировщик по-разному себя с ними ведёт.
                                  Там выше есть строчка:
                                  #define TASK_WAIT_FOR(Object) this->WaitFor(Object); this->state = __LINE__; return false; case __LINE__:
                                  Так вот, WaitFor указывает планировщику, что таска заблокирована на указанном объекте, после чего сохраняется состояние. Выполнение с точки case __LINE__: начнётся после того, как объект станет сигнальным. Это чем-то напоминает дескрипторы, на которых выполняются блокирующие вызовы в операционке.
                                +2
                                Купите уже «синюю таблетку» на STM32 и пишите под FreeRTOS, ардуино — это детский сад, ясельная группа.
                                  +1
                                  Я ещё в яслях :)
                                  У меня есть stm32, это сильно не то. Там порог входа сильно выше. Там всё очень сложно и даже помигать светодиодом чтобы, надо мозг ломать долго. Ардуина для поделок, я их и делаю.
                                  0
                                  Год назад делал подобное на С, только по истечению интервала вызывались функции. В итоге вышел менеджер задач на state machine. Через погода нашел это: https://geektimes.ru/post/255770/
                                    0
                                    Я не хотел делать на колбеках такое изначально. Там выше интересный код с использованием __LINE__ в качестве состояния, кстати. И вообще, макросы заслуживают там вдумчивого чтения.
                                    Я, если и начну делать что-то дальше, сделаю на ООП плюсово и наследованием от класса SmartTask, который будет дёргать методы порождённого класса, а SmartOS :) будет по списку бегать таких тасков.
                                    0
                                    Конечно я проголосовал за «Полезно», но не пойму чем подход с циклической проверкой времени хуже?
                                    Вернее оно у Вас и реализовано, но поскольку это микроконтроллер, когда я писал для него, казалось целесообразнее делать наиболее понятно, чтобы все сразу на виду. Вот как-то так, или здесь.
                                    Но, наверное, это дело вкуса.
                                      0
                                      Задача стояла максимально спрятать код, сделать библиотеку а ля ардуина. У меня там кода всего ничего, но он не мешает прочтению кода уже пользовательского, по делу, прячет переменные состояния и таймера в приватные у класса. То есть, не отвлекает от основной логики.
                                      0
                                      Древние древние мудрецы, изучавшие когда-то теорию программирования знали о существовании…
                                      кончеечных автоматов
                                      еще можно посмотреть вот сюда и вот сюда
                                        0
                                        Спасибо. Обе статейки были прочитаны, конечно же.
                                        0
                                        Через while проще же без delay и у вас какое-то нестабильное моргание, то есть у меня какие-то разные промежутки выдает через Ваш код.

                                        void loop() { q=0;
                                          while (q<80000) { // здесь можем увеличивать или уменьшать паузу, меняя значение q меньше какого-то численного значения
                                          digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
                                          q++;
                                          }
                                          q=0;
                                          while (q<80000) {    
                                          digitalWrite(LED_BUILTIN, LOW);
                                          q++; }    // turn the LED off by making the voltage LOW     
                                          
                                        }

                                          0
                                          И какой период моргания будет тут? Кстати, какого типа у нас тут q? int?
                                            0
                                            1) Я мерил на глаз сколько нужно мне (это чуть-меньше секунды вроде) так-то можно точно посчитать вроде через тактовую частоту, но мне лень.
                                            2) float

                                            p/s
                                            Я думал что это банальное решение без delay через while, но что-то поисковик такое решение через while не дает мне. Я вроде читал про такое в первых мануалах про Ардуино.
                                              0
                                              Чуть больше секунды пауза на глаз, сорри.
                                              Вот полный код:

                                              float q=0;
                                                
                                              
                                              
                                              void setup() {
                                                // initialize digital pin LED_BUILTIN as an output.
                                                pinMode(LED_BUILTIN, OUTPUT);
                                                Serial.begin(9600);
                                                
                                              }
                                              
                                              
                                              void loop() { q=0;
                                                while (q<80000) {// здесь можем увеличивать или уменьшать паузу, меняя значение q меньше какого-то численного значения
                                                digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
                                                q++;
                                                }
                                                q=0;
                                                while (q<80000) {    // wait for a second
                                                digitalWrite(LED_BUILTIN, LOW);
                                                q++; }    // turn the LED off by making the voltage LOW     
                                                
                                              }


                                                0
                                                Я бы за такой код Вас уволил.
                                                  0
                                                  Вы меня заинтриговали, что не так? Сначала спрашиваете где q=80000 не int ли это? Можете посмотреть какое максимальное значение принимает int: arduino.ru/Reference/Int
                                                  Потом выражаете свое фи, хотя код мегапрост и работает с четким миганием и без delay, что мне и нужно было.
                                                    +1
                                                    Потому что проблем в этом коде уйма.
                                                    Во-первых, использование инкремента для вещественных типов не рекомендуется, поскольку возможны ситуации, когда «проинкрементированное» значение будет равно исходному.
                                                    Во-вторых, Ваш код чувствителен к частоте контроллера, к интенсивности возникновения прерываний, к реализации операции digitalWrite (теоретически constexpr-эквивалент может быть выполнен за 1 такт, но если его пока нет, это не значит что он не может появиться).
                                                    И да, глобальная переменная с неадекватным именем вырвала глаза.

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

                                          Самое читаемое