Первое, с чем сталкивается осваивающий 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
Имело смысл это делать?

Проголосовало 185 человек. Воздержалось 78 человек.

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

Поделиться с друзьями
-->

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


  1. kahi4
    09.01.2017 20:43
    +4

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


    1. nwwind
      09.01.2017 20:53

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


      1. kahi4
        09.01.2017 21:28
        +2

        Не совсем.


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


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


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


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


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


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


        1. roboter
          10.01.2017 11:02

          Я могу ошибаться, но помоему у UNO всего 2 прерывания (hardware).


          1. nwwind
            10.01.2017 11:57

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


          1. a-tk
            10.01.2017 12:50
            +1

            Это прерывания по приходу внешних сигналов. Всего же векторов прерываний куда больше. Только надо иметь в виду, что прерывания таймеров могут использоваться какими-то библиотеками, которые «застолбили» их за собой.


        1. nwwind
          10.01.2017 11:55

          rtos да, но она тоже память ест. Речь не идёт о чём-то серьёзном и да, для микроволновки или теплицы сгодится. Для промышленного применения, возможно, не стоит и ide ардуиновской пользоваться, верно?

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

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

          Перспективы же роста — перейти на stm32 :)


          1. HunterSpy
            10.01.2017 17:43

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

            p.s. Исключение когда проект делается любителем ардуино, для таких же ардуинщиков там может быть и серия.


            1. nwwind
              10.01.2017 17:50

              +100500
              ППКС :)


    1. Lerk
      10.01.2017 10:45

      Ардуино вообще по сути набор костылей над 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. nwwind
        10.01.2017 11:58
        +1

        Ардуино — это кубики для начинающих и для простых поделок. Для этого прокладка ардуины, его абстракция над железом заслуживают всяческого уважения и поощрения. :)


  1. lopatoid
    09.01.2017 20:57
    +4

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


    1. nwwind
      10.01.2017 11:58
      +3

      Чёрт! Там даже протокол такой же. Пойду убью себя ап стену… :(


  1. aso
    10.01.2017 09:39

    О-хо-хонюшки, а не проще ли запилить «многозадачность» в духе RTOS?
    Ну, т.е. — main loop, и отдельные «задачи», вызываемые по таймеру — каждую секунду,
    каждые 100мс, 10мс, etc.
    Понятно, что это не настоящая многозадачность — «задачи» должны сами успевать завершаться до следующего цикла, но, тем не менее…


    1. nwwind
      10.01.2017 11:59

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


      1. aso
        10.01.2017 12:10

        Только в этой «многозадачности» не видать «задач».
        А так, написать некий кусок кода без явно выделенного шедулера в том или оном виде, и назвать его «многозадачностью» — можно.
        Но зачем?


    1. vvzvlad
      10.01.2017 13:29

      Почему бы тогда не взять rtos?


      1. aso
        10.01.2017 13:50

        Да лучше, наверное.
        Но люди, зачем-то, изобретают велосипеды — я этот факт не обсуждал.


      1. nwwind
        10.01.2017 17:10

        Можно и ртос, если влезет. Я с нею ещё не играл.
        Боюсь, что «взять ртос» примерно равно «забыть про arduino ide».


        1. kahi4
          10.01.2017 20:01

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


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


          1. nwwind
            10.01.2017 20:09

            Есть, я попробовал уже. Сделаю так и так, посмотрю, что получится, можно сравнить будет.
            У меня на подходе конкретное изделие :) как раз.


  1. Keroro
    10.01.2017 09:50

    Таймер это хорошо, но помимо времени бывает нужно неблокируеще ждать ответы от других устройств, уровень на ножке и т.д. Мне для этих целей нравится библиотека 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
    


    1. nwwind
      10.01.2017 12:01

      Хммм… Пойду там макросы потрошить. Интересно.


  1. assad77
    10.01.2017 10:40

    в целом направление правильное. это путь в сторону state машины. а чистым таймером в случае если хочется асинхронного выполнения все равно не обойдешься.


    1. nwwind
      10.01.2017 12:02

      Спасибо.
      state-machine под это будет в следующей статье. Я там с клавиатурой разбирался и в итоге получился абстректный класс с МКА внутри. Сегодня/завтра опубликую.


  1. talkingpipe
    10.01.2017 12:05

    Я предпочёл эдакую кооперативную многозадачность (здравствуй, Windows 3.1 :) ).
    https://github.com/emelianov/Run
    Использование прерываний это не отменяет: в прерывании выставил флаг, а в основном цикле выполнил ресурсоемкую часть.


  1. nwwind
    10.01.2017 12:06

    Использование таймера заставляет всё делать с ним, то есть, нельзя абстрагироваться от него.
    Ээээ…
    Ну, если я спрячу таймер1 внутри своего класса, а потом захочу в самом скетче им попользоваться (я же не знаю потроха класса SmartDelay), то код превратится в тыкву.
    В задачах как раз стояло сделать нечно, что можно спрятать, подключить и забыть про код внутри. Ардуино-стайл некий.


  1. a-tk
    10.01.2017 12:06

    Поздравляю, Вы изобрели очередную простенькую кооперативную многозадачность.
    Основная проблема здесь в том, что семантика его использования достаточно страшная и непонятная.
    Если сделать нечто наподобие такого (код большой, прячу под спойлер и отбрасываю много чего)

    Фрагмент .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);
    
    


    1. nwwind
      10.01.2017 12:07

      Спасибо за макросы, я как раз ломаю голову над ними, чтобы заменить if(obj.Now) { действие }
      С другой стороны, так тоже ничего, понятно.


      1. a-tk
        10.01.2017 12:17

        Обратите внимание на конструкцию вида

        bool TaskFunc(int &state)
        {
        switch (state)
        {
        case 0: // начальное
        //
          state = __LINE__; return false; case __LINE__: // в одну строку.
        default:
          state = -1;
          return true;
        }
        }
        


        1. nwwind
          10.01.2017 16:11

          Опс!


      1. a-tk
        10.01.2017 17:03
        +1

        Кстати, в той реализации, которую Вы здесь видите, у таски есть состояния «активна», «заблокирована» и «завершена» и планировщик по-разному себя с ними ведёт.
        Там выше есть строчка:
        #define TASK_WAIT_FOR(Object) this->WaitFor(Object); this->state = __LINE__; return false; case __LINE__:
        Так вот, WaitFor указывает планировщику, что таска заблокирована на указанном объекте, после чего сохраняется состояние. Выполнение с точки case __LINE__: начнётся после того, как объект станет сигнальным. Это чем-то напоминает дескрипторы, на которых выполняются блокирующие вызовы в операционке.


  1. rstepanov
    10.01.2017 13:30
    +2

    Купите уже «синюю таблетку» на STM32 и пишите под FreeRTOS, ардуино — это детский сад, ясельная группа.


    1. nwwind
      10.01.2017 16:11
      +1

      Я ещё в яслях :)
      У меня есть stm32, это сильно не то. Там порог входа сильно выше. Там всё очень сложно и даже помигать светодиодом чтобы, надо мозг ломать долго. Ардуина для поделок, я их и делаю.


  1. Bismuth208
    10.01.2017 16:07

    Год назад делал подобное на С, только по истечению интервала вызывались функции. В итоге вышел менеджер задач на state machine. Через погода нашел это: https://geektimes.ru/post/255770/


    1. nwwind
      10.01.2017 16:15

      Я не хотел делать на колбеках такое изначально. Там выше интересный код с использованием __LINE__ в качестве состояния, кстати. И вообще, макросы заслуживают там вдумчивого чтения.
      Я, если и начну делать что-то дальше, сделаю на ООП плюсово и наследованием от класса SmartTask, который будет дёргать методы порождённого класса, а SmartOS :) будет по списку бегать таких тасков.


  1. wyfinger
    10.01.2017 16:15

    Конечно я проголосовал за «Полезно», но не пойму чем подход с циклической проверкой времени хуже?
    Вернее оно у Вас и реализовано, но поскольку это микроконтроллер, когда я писал для него, казалось целесообразнее делать наиболее понятно, чтобы все сразу на виду. Вот как-то так, или здесь.
    Но, наверное, это дело вкуса.


    1. nwwind
      10.01.2017 16:18

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


  1. neco
    11.01.2017 06:46

    Древние древние мудрецы, изучавшие когда-то теорию программирования знали о существовании…
    кончеечных автоматов
    еще можно посмотреть вот сюда и вот сюда


    1. nwwind
      11.01.2017 12:53

      Спасибо. Обе статейки были прочитаны, конечно же.