суббота, 14 сентября 2013 г.

Мигаем без delay() с комфортом

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

к сожалению обсуждение написание макросов выходит за рамки данной статьи. фактически этот макрос - изложение прошлой статьи на языке понятном компилятору. что-бы "черновую работу" - он делал за нас 

Размещаем вверху скетча что-то такое:

#define DO_EVERY_V(varname,interval,action) \
static unsigned long __lastDoTime_##varname = 0; \
if( (millis()-__lastDoTime_##varname>interval )){ \
  {action;}\
  __lastDoTime_##varname=millis();\
}\
#define DO_EVERY_V_1(varname,interval,action)  DO_EVERY_V(varname,interval,action)
#define DO_EVERY(interval,action) DO_EVERY_V_1(__LINE__,interval,action)

После  этого мы можем писать в нашем скетче периодически действия в таком виде:

DO_EVERY(ИНТЕРВАЛ_ДЕЙСТВИЯ_В_МИЛЛИСЕКУНДАХ, ЧТО_НУЖНО_СДЕЛАТЬ);
или, если нам нужно выполнить несколько действий в таком
  DO_EVERY(ИНТЕРВАЛ_ДЕЙСТВИЯ_В_МИЛЛИСЕКУНДАХ,{
       ЧТО_НУЖНО_СДЕЛАТЬ1;
       ЧТО_НУЖНО_СДЕЛАТЬ2;
       ....
  });

Все. Дальше компилятор сам "развернет" DO_EVERY в объявление переменной, проверку "наступило-ли время" и т.п.


Наш пример из прошлой статьи  можно переписать в таком виде:

/* Blink without Delay
 2013
 by alxarduino@gmail.com
 http://alxarduino.blogspot.com/2013/09/ComfortablyBlinkWithoutDelay.html
 */
 
#define LED_PIN  13      // номер выхода,подключенного к светодиоду
#define  BLINK_INTERVAL  5000UL  // интервал между включение/выключением светодиода (5 секунд)
#define PRINT_INTERVAL 1000UL  // периодичность вывода времени в Serial (1 cекунда)
#define SERIAL_SPEED 9600 // скорость работы Serial

#define DO_EVERY_V(varname,interval,action) \
static unsigned long __lastDoTime_##varname = 0; \
if( (millis()-__lastDoTime_##varname>interval )){ \
  {action;}\
  __lastDoTime_##varname=millis();\
}\

#define DO_EVERY_V_1(varname,interval,action)  DO_EVERY_V(varname,interval,action) 
#define DO_EVERY(interval,action) DO_EVERY_V_1(__LINE__,interval,action)


 
void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(LED_PIN, OUTPUT);      
  
  // задаем скорость работы ком-порта
  Serial.begin(SERIAL_SPEED);
  
  
}
 
void loop()
{
  // мигаем диодом   (периодически переключаем его состоянии)
  DO_EVERY(BLINK_INTERVAL,digitalWrite(LED_PIN,!digitalRead(LED_PIN))); 
  
  // периодически выводим время в Serial
  DO_EVERY(PRINT_INTERVAL,{
    Serial.print("Current time:");
    Serial.println(millis());
  });

}

Все хорошо, но... что-то меня пугает

loop() у нас стал гораздо более лаконичным. Фактически в нем осталось только "что нам нужно делать", без "технической кухни", но все-таки, вот эта "магия в начале скетча" выглядит несколько "ошарашивающе". Захламляет саму суть скетча. Кухня осталась, просто переехала в другое место. 
А можно ли как-то обойтись без вставки этого макроса?  К сожалению - нет. Должен же компилятор как-то узнать "как обеспечивать переодичность". Но, хорошая новость, мы можем "спрятать" все это "за кулисы". Что-бы оно было, но не мозолило глаза. 
Я оформил этот макрос в виде библиотеки (хотя, конечно "библиотека" - это громко сказано :)

Cкачиваем библиотеку  отсюда https://bitbucket.org/alxarduino/leshakutils



  1. Скачиваем "свежую версию"
  2. Распаковываем в папку [путь к вашей ArduinoIDE]\libraries\Распаковываем вместе с именем папки. 
  3. Переименовываем распакованную папку из имени вида "alxarduino-leshakutils-3ef5efa1498a"  в "LeshakUtils"
  4. В результате все файлы должны попасть в папку [путь к вашей ArduinoIDE]\libraries\LeshakUtils\
  5. Перезапускаем ArduinoIDE (!!! Важно, без этого на найдет новую библиотеку)
Теперь , что-бы наша магия заработала, нам, вверху скетча нужно добавить всего одну строку (вместо того "страшного прицепа"):

#define <TimeHelpers.h>

 Теперь наш пример выглядит так:

/* Blink without Delay
 2013
 by alxarduino@gmail.com
 http://alxarduino.blogspot.com/2013/09/ComfortablyBlinkWithoutDelay.html
 */
 
#include <TimeHelpers.h>  
 
#define LED_PIN  13      // номер выхода,подключенного к светодиоду
#define  BLINK_INTERVAL  _SEC_(5)  // интервал между включение/выключением светодиода (5 секунд)
#define PRINT_INTERVAL _SEC_(1)  // переодичность вывода времени в Serial (1 cекунда)
#define SERIAL_SPEED 9600 // скорость работы Serial


 
void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(LED_PIN, OUTPUT);      
  
  // задаем скорость работы ком-порта
  Serial.begin(SERIAL_SPEED);
}
 
void loop()
{
  // мигаем диодом   (переодически переключаем его состоние)
  DO_EVERY(BLINK_INTERVAL,digitalWrite(LED_PIN,!digitalRead(LED_PIN))); 
  
  // переодически выводим время в Serial
  DO_EVERY(PRINT_INTERVAL,{
    Serial.print("Current time:");
    Serial.println(millis());
  });
  
}

Все. Теперь у нас все чисто и опрятно.

А что это за _SEC_()?

Если вы внимательно смотрели на прошлый скетч, то заметили что у нас произошло еще одно изменение: интервалы я теперь задал в секундах, а не в миллисекундах.
Это стало возможно, потому что в библиотеку я добавил еще "макросы помогалки" переводящие секунды/минуты/часы/дни в миллисекунды (ну лениво мне каждый раз высчитывать сколько миллисекунд в часе : и считать количество нулей в длинной цифре типа 60000):

#define _SEC_(sec) 1000UL*sec
#define _MIN_(mn)  _SEC_(60*mn)
#define _HR_(h)  _MIN_(60*h)
#define _DAY_(d) _HR(24*d)  

Так что теперь, если мне нужно, скажем, что-бы диод мигал раз 10-ть минут, а время выводилось в serial раз в два часа - я могу прямо так и написать:

#define  BLINK_INTERVAL  _MIN_(10) 
#define PRINT_INTERVAL _HR_(2) 

Но это - по желанию. Если привыкли все писать в миллисекундах - это возможность сохранилась. Просто "пишем цифру" как и раньше

#define PRINT_INTEVAL 7200000UL // 2 часа

А в чем преимущество перед другими библиотеками?

Действительно, для выполнения переодических для ардуины до меня написана далеко не одна библиотека (который используют прерывания таймеров, или как и наша работает через millis())
Скажем библиотека Arduino Playground - SimpleTimer Library ("идеологически" - она очень близка к нашему подходу. внутри у нее -тоже millis() используется).

В первую очередь  - мне просто хотелось написать "свою" :)  Во вторых - я не буду отговаривать вас использовать SimpleTimer. Она умеет больше чем моя "библиотека" (моя, пока умеет только периодическое действие выполнять). Но... за счет того что моя библиотека сделана с помощью макросов, а не объектов - само ее подключение не приводит к дополнительному расходу памяти. Она более "легковестная". У меня расход памяти будет в точности таким же как если бы мы "все писали руками".
Вообщем под разные ситуации - разные библиотеки будут более удобными. В каких-то случаях SimpleTimer предпочтительней, в каких-то - моя.

UPD1:

Раз в коментариях возник вопрос, попробую объяснить "почему тут не будет переполнения".
Оно будет. Но будет "два раза". После того как millis() перепрыгнет границу и начнет опять отсчет с нуля можно заподозрить что выражение if(millis()-lastDoTime>inteval) больше никогда не сработает. Так как разница (millis()-lastDoTime) - будет отрицательной (пока millis() опять не станет боьлше lastDoTime). И соответственно всегда меньше любого интервала.

Но.... это в математике. А тут нам нужно еще помнить про типы. И то что время мы храним в unsigned long. Следовательно отрицательных чисел - у нас просто не бывает. Попытка сохранить отрицательное числов в беззнаковую переменную, опять вызовет переполнение-переход. Только "в обратную сторону". И интервал (millis()-lastDoTime) - все-таки будет правильным. Несмотря на переполнение.

Что-бы это все "легче представить" (и не путать мозг "большими числами"). Представим что у нас время у нас вообще хранится в byte ( тоже безнаковый). Он "переполнится" уже на числах больше 255.

void setup(){
  Serial.begin(9600);
  byte interval=10;
  byte lastDoTime=250;

  // две "эмуляции millis()" после переполнения. Обе - меньше lastDoTime
  byte millis1=2; // не прошло еще время выполнение
  byte millis2=20; // прошло

  byte diff1=millis1-lastDoTime;
  byte diff2=millis2-lastDoTime;

  Serial.println(diff1); // нет, тут не -253, а 8-мь, отрицательное число тоже "переполнит", 
  Serial.println(diff2); // а тут у нас 26
  
  Serial.println(diff1>interval); // false - еще не прошло достаточно времени
  Serial.println(diff2>interval); // true - а вот теперь прошло.
}
void loop(){}


Если все равно, не понятно как происходит "переполнение в обратную сторону" и откуда там взялись 8 и 26, то можно рассмотреть такой пример.

Предположим нам нужно "прикинуть", что сохранится в переменную в таком случае
byte diff3=5-10;

Давайте эту -10, разобьем на слагаемые.

diff3=5-(5+1+4)
Откроем скобки
diff3=5-5-1-4 = (0 - 1) -4

Вот в это 0-1, в случае byte, дает нам в результате не -1, максимальное число 255.
У нас числовая ось, как-бы "закольцована" (в обе стороны).
diff3=(0-1)-4= 255 -4 = 251;

Поэтому сделав println(diff3), мы увидим выводе 251, а не -5




21 комментарий:

  1. Leshak в магическом макросе надо бы предусмотреть переход millis() через 0 когда пройдет 50 дней. Предлагаю добавить такую функцию.

    ОтветитьУдалить
  2. Спасибо, за внимательность, за такими вещами нужно следить.
    Но, в данном случае, это "ложная тревога".
    Не будет проблем с переходом. Именно поэтому делал (millis()-lastDoTime)>interval
    А не более интуитивный
    nextDoTime=millis()+interval;

    if(nextDoTime>millis()){.....}

    Вариант с вычитаем - проблемы с "переходом" не имеет. Там происходит переполнение, вы правы, но оно "происходит два раза". Поэтому "логика" - не ломается.

    ОтветитьУдалить
  3. Дополнил статью. Попытался в UPD1 более развернуто ответить почему-же "переход millis() через ноль" - уже предусмотрен.

    ОтветитьУдалить
  4. Спасибо за пояснение, так правда проще - пойду поправлю свой код.

    ОтветитьУдалить
  5. Лучше всетаки сделать nextDoTime, переполнение будет работать также как и в случае вычитания, но вы не будете при каждом вызове делать операцию сложения с unsigned long

    ОтветитьУдалить
  6. "переполнение будет работать" - нет, не будет.
    Или я вас неправильно понял (тогда нужен кусочек кода от вас, что-бы понять).
    Мои рассуждения таковы: millis() будет подходить "к границе", когда millis()+interval больше чем максимальный Unsigned long, в nextDoTime образуется "маленькое число". При этом сама millis() еще "большая". И условие if(millis()>nextDoTime) - начнет срабатывать "когда не требовалось этого". И будет срабатывать до тех пор, пока millis() не переполнится.
    Попробуйте в виде кода реализовать подход nextDoTime, это самый надежный способ узнать "кто из нас прав" :)

    ОтветитьУдалить
  7. Serial.println(diff1); // нет, тут не -253, а 8-мь, отрицательное число тоже "переполнит",
    Возможно, в комментарии ошибка, так как 2 - 250 = -248, но ни как не -253, или теперь я вообще ничего не понимаю? :)

    ОтветитьУдалить
  8. Этот комментарий был удален автором.

    ОтветитьУдалить
  9. Спасибо, очень удобно. А как попеременно помигать, например, тремя и более светодиодами?

    #define LED1_INTERVAL 1000UL
    #define LED2_INTERVAL 2000UL
    #define LED3_INTERVAL 3000UL
    ...

    DO_EVERY(LED1_INTERVAL,digitalWrite(LED_PIN1,!digitalRead(LED_PIN1)));
    DO_EVERY(LED2_INTERVAL,digitalWrite(LED_PIN2,!digitalRead(LED_PIN2)));
    DO_EVERY(LED3_INTERVAL,digitalWrite(LED_PIN3,!digitalRead(LED_PIN3)));

    Так?

    ОтветитьУдалить
  10. Этот комментарий был удален автором.

    ОтветитьУдалить
  11. Скажите а можно ли это использовать для мигания светодиода время свечения светодиода и время когда он не светится задавалось разное?

    ОтветитьУдалить
  12. Конечно можно. Кто же вам запретит? ;)
    Я бы попробовал, тогда BLINK_INTERVAL объявить как переменную (не забыть тип unsigned long), и в действии, передаваемом в DO_EVERY кроме переключения светодиода, менял бы значения этой переменной.
    blinkInterval=!digitalRead(LED_PIN)?_SEC(2)_:_SEC_(5)
    Перевод: если сейчас светик не горит, то следующий раз поменять состояния (включить) через 2 сек. В против случае (горит) через 5 сек переключить (выключить).

    ОтветитьУдалить
  13. Но, в данном случае (светиться разное время), не обязательно использовать подход "переодического действия". Можно просто в loop() сделать что-то типа
    digitalWrite(LED_PIN, (millis() % 10000UL)<2000UL);
    Так тоже будет 2 сек гореть, 8-мь не гореть.
    Взяли текущие время. Поделили его не 10 сек. Если остаток от деления меньше 2 сек. светику ставится "гореть", если больше "не гореть".
    Например от старта котролера прошла 21 сек. Остаток от деления на 10 - 1 сек. Значит условие <2000UL, выполнится и в digitalWrite уйдет true (hight), а на 24секунде (24000), остаток от деления будет 4сек (4000) и в digitalWrite уйдет low.
    Но это все хорошо на больших интервалах. Если вы хотите ШИМ (PWM) програмно делать, то это лучше делать аппаратно, либо через таймеры (есть куча готовых библиотек для програмного шима).

    ОтветитьУдалить
  14. Спасибо за совет. Хочу пояснить , я не програмист, я инженер КИП. Програмировал промышленные контролеры. Там с таймерами влпросы решаются на много проще. Поэтому буду пробовать. А ардуино хочу приспособить к парнику. Буду регулировать температуру ( есть готовый скетч) и в него хочу вставить периодическое включение зуммера для отпугивания кротов. Пробовал delay, но тогда регулятор температуры глючит. Буду пробовать реализовать то что предлагаете Вы.

    ОтветитьУдалить
  15. Большое спасибо. Попробовал digitalWrite(LED_PIN, (millis() % 10000UL)<2000UL); и все сразу заработало.

    ОтветитьУдалить
  16. Этот комментарий был удален автором.

    ОтветитьУдалить
  17. Этот комментарий был удален автором.

    ОтветитьУдалить
  18. Этот комментарий был удален автором.

    ОтветитьУдалить
  19. Скажите а как в ниже приведенном коде в цикле for, delay() заменить на millis()

    for(int i = 0; i < 20; i++) {
    leds[i] = CHSV(0, 0, 0);
    leds[40 - i] = CHSV(0, 0, 0);
    LEDS.show();
    delay(30);
    }

    ОтветитьУдалить
  20. Вот это мозг! Подскажите пожалуйста, как написать макрос создания выполнения функции не с циклическим выполнением, а с задержкой по времени. Не знаю как отловить срабатывание функции с помощью одной функции, а не опросом всех экземпляров класса.

    ОтветитьУдалить
  21. Например:
    нажали кнопку 1 запустили функцию timer(1000,Serial.println("1"));
    нажали кнопку 2 запустили функцию timer(2000,Serial.println("2"));
    Как в функции void loop() {}
    выполнить через указанное время, указанное действие?

    ОтветитьУдалить