воскресенье, 15 сентября 2013 г.

Читаем аналоговые кнопки. Часть 1

Первоначально эта статья задумывалась как продолжение  Еще одни часы на Ардуине. Часть 2. Выводим время на экран . Но потом решил что будет лучше "освоится с кнопками отдельно", а потом эти наработки - применим к построению часов.
Поэтому, в этой статье я буду использовать вывод в Serial, а не на экран. Что-бы те у кого нет такого LCD KeyPad Shiled могли повторить у себя мои эксперименты с помощью обычных кнопок и резисторов.

Если вы будете собирать cамостоятельно не обязательно брать "в точности такие же номиналы резисторов", мы все равно будет учится "калибровать скетч" под те резисторы которые удалось найти :)

Железная составляющая

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


Согласно документации на шилд кнопки этого шилда подключены к пину A0.
Когда мы нажимаем кнопку у нас образуется делитель напряжения . Его верхнее плечо равно 2K, а нижние - зависит от того какую кнопку мы нажали. C помощью A0 пина мы замеряем какое напряжение образовалось на этом делителе и так узнаем "какая кнопка была нажата".

Давайте посмотрим что происходит когда мы нажимаем кнопку LEFT.  Ток пройдет через резистор R2 (верхние плечо делителя) и пойдет через резисторы R3,R4,R5 , через кнопку, в землю. Как мы знаем (надеюсь) последовательное соединение резисторов можно мысленно заменить одним резистором с номиналом равным их сумме. Складываем R+R4+R5=0.33K+0.62K+1K=1.95K . Это у нас получился номинал нижнего плеча делителя при нажатой кнопке LEFT. Верхнее плечо  мы уже знаем - 2K. Далее с помощью закона ома, калькулятора или онлайн сервиса вычисляем что делить 2K/1.95K должен дать нам на выходе 2.47V  (при пяти-вольтовом питании) . Это напряжение мы прочитаем с помощью analogRead(A0).
Но analogRead() возвращает нам значение не в вольтах, а в неких абстрактных "попугаях".  Из документации мы знаем что "Напряжение  поданное на аналоговый вход, обычно от 0 до 5 вольт будет преобразовано в значение от 0 до 1023, это 1024 шага с разрешением 0.0049 Вольт." Значит при нажатии кнопки LEFT analogRead() вернет нам число 2.47V/0.0049V~=504
По этой цифре мы и можем понять что нажата кнопка LEFT.

Вообщем-то, если мы поняли суть делителя. То мы можем немного "схитрить" и сразу высчитывать значение "в попугаях", не занимаясь конвертацией в "вольты". Главное соотношение номинала верхнего и нижнего плеча. Напряжение на делителе вычисляется Uвых.=Uпит. * Rнижнего /(Rверхние+Rнижнее)  . В "попугаях" у нас Uпит.=1024.
Подставляем:  1024*1.95/(2+1.95)~=505  (цифра вышла на "один попугай больше" за счет округления, нам это не существенно).

Для закрепления давайте еще посчитаем что выйдет если нажать DOWN. Нижнее плечо, в этом случае, у нас образуется резисторам R4 и R4 , Rнижние=330 Ом + 620 Ом= 0.33K+0.62K=0.95K
Подставляем Uвых=1024*0.95/(2+0.95)~= 330
Значит, при нажатии кнопки DOWN, analogRead(A0) нам будет возвращать цифру 330
Вернее "около этой цифры". Так как реальные, в отличии от идеальных, резисторы имеют погрешность. От процентов до десятков процентов.

А можно как-то не заниматься этими расчетами?

Ну конечно можно. Мы может просто подключить все эти резисторы/кнопки к арудине, начать нажимать кнопки и смотреть что там образуется на analogRead(A0) :)

Вливаем вот такой скетчик:
#define SERIAL_SPEED 9600
#define KEYS_INPUT_PIN A0 // аналоговый вход на который через резисторы привешены кнопки

#define SAMPLES 10 // сколько раз, для усреднения будем читать кнопку
#define SAMPELS_DELAY 100 // пауза между замерами

#define DETECT_DIFF 30 // насколько значение KEYS_INPUT_PIN должно отличаться от NO_KEYS_LEVEL что-бы мы поняли что "кнопка нажата нужно замерять"


//в эти переменые будем делать наши замеры
int avg; // среднее значение
int max; // максимально значение
int min; // минимальное


//
int NO_KEYS_LEVEL; // тут будем хранить значение  KEYS_INPUT_PIN означающие "кнопки не нажаты"

void setup(){
  Serial.begin(SERIAL_SPEED);
  Serial.println("Please release all keys"); // не нажимайте пока кнопки
  
  delay(500); // даем время убрать руки с кнопок

  Serial.println("Measure NO_KEYS" );  
  
  // делаем замер KEYS_INPUT_PIN когда ни одна кнопка не нажата
  readSamples();
  
  NO_KEYS_LEVEL=avg; // запомнили какой уровень означает "кнопка не нажата"
  
  
  printLevels();
  
}

void loop(){
  if(abs(analogRead(KEYS_INPUT_PIN ) - NO_KEYS_LEVEL)>DETECT_DIFF){ // нажата какая-то кнопка
     Serial.print("Start measure....");
     readSamples(); // кнопку нужно продолжать держать, не отпускать
     Serial.println("done. Please release key");
     printLevels();
     delay(2000); // даем время убрать руку с кнопки
  }
}


// фукнция делает SAMPLES замер и находит их среднее, максимум и минимум
void readSamples(){
  // вначале все сбрасываем 
  min=1024; 
  max=0;

   // вводим временную переменную float, 
   //что-бы на малых значениях не терять точность из округрления
  float tmpAvg;
  


  for(unsigned int i=0;i<SAMPLES;i++){  
  
    int val=analogRead(KEYS_INPUT_PIN); // делаем замер
  
    //ищем максимум/минимум
    if(valmax)max=val;
    
    // добавляем замер к среднему
    tmpAvg+=((float)val/SAMPLES);
    
    delay(SAMPELS_DELAY);
  }
  

  
  avg=round(tmpAvg); // округляем среднее до целого
  
   
}

// выводит среднее, минимальное и максимальное значение замеров
void printLevels(){
  Serial.print("avg=");Serial.print(avg,DEC);
  Serial.print(", min=");Serial.print(min,DEC);
  Serial.print(", max=");Serial.print(max,DEC);
  Serial.println();
}




Открываем Serial монитор и нажимаем по очереди кнопки RIGHT/UP/DOWN/LEFT/SELECT
На моем шилде у меня вывелось такое (держим кнопку пока не появится надпись "Please release key"):
Please release all keys
Measure NO_KEYS
avg=1023, min=1023, max=1023
Start measure....done. Please release key
avg=0, min=0, max=0
Start measure....done. Please release key
avg=98, min=97, max=98
Start measure....done. Please release key
avg=253, min=253, max=254
Start measure....done. Please release key
avg=405, min=405, max=405
Start measure....done. Please release key
avg=635, min=635, max=635


Что в "переводе" означает такие значения для кнопок

  • ничего не нажато - 1023 
  • RIGHT - 0
  • UP - 98
  • DOWN - 253
  • LEFT - 405
  • SELECT - 635

Как видите  значение отличается от "расчетных". Тут возможны два объяснения: либо я ошибся в расчетах , либо это погрешность резисторов + сами кнопки дорожки и т.п. - тоже имеют какое-то сопротивление. Я склоняются в версии "погрешность". Потому что сам производитель указывает  что для версии шилда  V1.0 и  V1.1 analogRead возвращает чуть-чуть разные значения. При этом судя по схеме номиналы резисторов на кнопках у них - одинаковые. Значит разницу можно объяснить либо погрешностью, либо производитель использует резисторы отличные от  указанных в документации.
Но в данном случае это - не важно. 
Главное что теперь я знаю какие значение возвращает analogRead(A0) именно на моем конкретном экземпляре шилда. Я просто буду пользоваться именно этими цифрами при дальнейшем написании кода. Вы можете сделать аналогичный замер на своем шилде, и соответственно "подправить примеры" если будет такая необходимость.

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

Так а как же читать кнопки?

Простейший путь - вы уже знаете. Взять пример от производителя . Ключевая функция в нем

int read_LCD_buttons()
{
 adc_key_in = analogRead(0);      // read the value from the sensor 
 // my buttons when read are centered at these valies: 0, 144, 329, 504, 741
 // we add approx 50 to those values and check to see if we are close
 if (adc_key_in > 1000) return btnNONE; // We make this the 1st option for speed reasons since it will be the most likely result
 // For V1.1 us this threshold
 if (adc_key_in < 50)   return btnRIGHT;  
 if (adc_key_in < 250)  return btnUP; 
 if (adc_key_in < 450)  return btnDOWN; 
 if (adc_key_in < 650)  return btnLEFT; 
 if (adc_key_in < 850)  return btnSELECT;  
 
 // For V1.0 comment the other threshold and use the one below:
/*
 if (adc_key_in < 50)   return btnRIGHT;  
 if (adc_key_in < 195)  return btnUP; 
 if (adc_key_in < 380)  return btnDOWN; 
 if (adc_key_in < 555)  return btnLEFT; 
 if (adc_key_in < 790)  return btnSELECT;   
*/
 
 
 return btnNONE;  // when all others fail, return this...
}

Только теперь мы можем понять "почему она именно такая". Что значат эти цифры. И перепроверить производителя если он ошибся. Скажем для кнопки DOWN. Судя по коду кнопка будет опознаваться если analogRead(A0) вернул число в диапазоне от 250 до 450 (точнее 451)
Смотрим "намерянные цифры". У меня DOWN возвращает 253. Что "попадает в диапазон". Кнопка распознается правильно. Хотя и опасно близко к границе диапазона. Может начать "глючить" (температура поменялась и т.п.)
Смотрим кнопку "SELECT'. По коду должна быть в диапазоне 650-850. А у меня - 635. Значит эта функция с моим шилдом будет работать не верно. Вместо SELECT распознает кнопку LEFT.
Но... ниже в комментариях мы видим еще цифры. Для старой версии шилда. Там другие диапазоны. Там SELECT должен лежать в интервале 555-790 - мне подходит :)
DOWN - должен лежать в 195-380 , а у меня 253. Тоже все верно.
LEFT/UP/RIGHT - тоже все совпадает.
Вывод - у меня версия шилда 1.0 Мне нужно закоментировать (или выкинуть) версию кода для V1.1 и использовать "For V1.0"
К сожалению нигде на шилде я не смог найти указания его версии. Так что пришлось выяснять "кто он" вот таким способом.

И все это было только ради того что-бы выяснить какая версия шилда?

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

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

Поместить вверху скетча что-то типа:

#define A_PINS_BASE 100 // номер с которого начинается нумерация наших "псевдо-кнопок".

#define PIN_RIGHT 100
#define PIN_UP 101
#define PIN_DOWN 102
#define PIN_LEFT 103
#define PIN_SELECT 104

Обратите внимание, что для btnNONE - у нас нет пина. Так как мы "сменили парадигму". Ушли от "код клавиши", к идее "цифровых пинов". А для обычных пинов - нет пина "никакой" :)

Следующие что мы поменяем в "идеологии". Мы будем проверять не "попадается ли значение analogRead()" в какой-то диапазон. А возьмем "ожидаемое значение" (которое получили с помощью измерительного скетча выше) и будем сравнивать analogRead с ним. Если он "отличаемся" не больше чем, скажем на 50 - cчитаем что кнопка нажата. Если больше - считаем не нажатой.

Но что-бы написать эту функцию нам нужно для каждой кнопки задать "ожидаемые значение analogRead()". Сделаем это с помощью массива структур

struct A_PIN_DESC{ // определяем  структуру которой будем описывать какое значение мы ожидаем для каждого псевдо-пина
   byte pinNo; // номер пина
   int expectedValue;// ожидаемое значение
};

A_PIN_DESC expected_values[]={ // ожидаемые значения для псевдо-кнопок
   { PIN_RIGHT,0},
   { PIN_UP,98},
   { PIN_DOWN,253},
   { PIN_LEFT,405},
   { PIN_SELECT,635}
};

#define A_PINS_COUNT sizeof(expected)/sizeof(A_PIN_DESC) // вычисляем сколько у нас всего "псевдо-кнопок" заданно.
#define A_POSSIBLE_Aberration 50 // допустимое отклонение analogRead от ожидаемого значения, при котором псевдо кнопка считается нажатой

Ну а теперь, мы можем написать свою версию digitalReadA(). Которая может читать и обычные цифровые пины, так  и "псевдо".

bool digitalReadA(byte pinNo){
  if(pinNo<A_PINS_BASE)return digitalRead(pinNo); // если номер пина маленький - значит это настоящий пин. читаем его "по обычному", через digitalRead()
  
  for(byte i=0;i<A_PINS_COUNT;i++){ // ищем описание нашего всевдо-пина
     A_PIN_DESC pinDesc=expected_values[i];// берем очередное описание
     if(pinDesc.pinNo==pinNo){ // нашли описание пина?
        int value=analogRead(A0); // производим чтетине аналогово входа
        
        return (abs(value-pinDesc.expectedValue)<A_POSSIBLE_ABERRATION); // возвращаем HIGH если отклонение от ожидаемого не больше чем на A_POSSIBLE_ABERRATION
     }
  }
  
  return LOW; // если не нашли описания - считаем что пин у нас LOW
  
}

Вот и все.
Теперь мы можем взять любой стандартный пример, скажем
http://www.arduino.cc/en/Tutorial/Switch - включение, выключение светодиода по нажатию обычной кнопки и вся его "переделка" сведется к замене двух строчек :
В качестве inPin мы укажем не D2, а PIN_SELECT и вызов функции digitalRead(inPin) заменим на вызов нашего "расширенного аналога" digitalReadA(inPin)

/* Read Analog Keys
 2013
 by alxarduino@gmail.com
  http://alxarduino.blogspot.com/2013/09/ReadAnalogKeys.html
 */

#define A_PINS_BASE 100 // номер с которого начинается нумерация наших "псевдо-кнопок".
 

#define PIN_RIGHT 100
#define PIN_UP 101
#define PIN_DOWN 102
#define PIN_LEFT 103
#define PIN_SELECT 104

struct A_PIN_DESC{ // определяем  структуру которой будем описывать какое значение мы ожидаем для каждого псевдо-пина
   byte pinNo; // номер пина
   int expectedValue;// ожидаемое значение
};

A_PIN_DESC expected_values[]={ // ожидаемые значения для псевдо-кнопок
   { PIN_RIGHT,0},
   { PIN_UP,98},
   { PIN_DOWN,253},
   { PIN_LEFT,405},
   { PIN_SELECT,635}
};

#define A_PINS_COUNT sizeof(expected_values)/sizeof(A_PIN_DESC) // вычисляем сколько у нас всего "псевдо-кнопок" заданно.
#define A_POSSIBLE_ABERRATION 50 // допустимое отклонение analogRead от ожидаемого значения, при котором псевдо кнопка считается нажатой




bool digitalReadA(byte pinNo){
  if(pinNo<A_PINS_BASE)return digitalRead(pinNo); // если номер пина маленький - значит это настоящий пин. читаем его "по обычному", через digitalRead()
  
  for(byte i=0;i<A_PINS_COUNT;i++){ // ищем описание нашего всевдо-пина
     A_PIN_DESC pinDesc=expected_values[i];// берем очередное описание
     if(pinDesc.pinNo==pinNo){ // нашли описание пина?
        int value=analogRead(A0); // производим чтетине аналогово входа
        
        return (abs(value-pinDesc.expectedValue)<A_POSSIBLE_ABERRATION); // возвращаем HIGH если отклонение от ожидаемого не больше чем на A_POSSIBLE_ABERRATION
     }
  }
  
  return LOW; // если не нашли описания - считаем что пин у нас LOW
  
}



// ***************************************** ADAPTED FOR USE Analog keys arduino tutorial:http://www.arduino.cc/en/Tutorial/Switch


/* switch
 * 
 * Each time the input pin goes from LOW to HIGH (e.g. because of a push-button
 * press), the output pin is toggled from LOW to HIGH or HIGH to LOW.  There's
 * a minimum delay between toggles to debounce the circuit (i.e. to ignore
 * noise).  
 *
 * David A. Mellis
 * 21 November 2006
 */

int inPin = PIN_SELECT;         // the number of the input pin
int outPin = 10;       // the number of the output pin

int state = HIGH;      // the current state of the output pin
int reading;           // the current reading from the input pin
int previous = LOW;    // the previous reading from the input pin

// the follow variables are long's because the time, measured in miliseconds,
// will quickly become a bigger number than can be stored in an int.
long time = 0;         // the last time the output pin was toggled
long debounce = 200;   // the debounce time, increase if the output flickers

void setup()
{
  pinMode(inPin, INPUT);
  pinMode(outPin, OUTPUT);
}

void loop()
{
  reading = digitalReadA(inPin); 

  // if the input just went from LOW and HIGH and we've waited long enough
  // to ignore any noise on the circuit, toggle the output pin and remember
  // the time
  if (reading == HIGH && previous == LOW && millis() - time > debounce) {
    if (state == HIGH)
      state = LOW;
    else
      state = HIGH;

    time = millis();    
  }

  digitalWrite(outPin, state);

  previous = reading;
}


 
Правда мне пришлось сделать еще одно косметическое изменения. Я поменял номер пина на котором мигаем диодом с 13-того (встроенный в ардуину диод), на 10-тый. outPin = 10 сделал.
Сути это не меняет. Просто с нахлобученным шилдом - не видно встроенного светика. Он оказывается "под ним". Поэтому пришлось мигать 10-тым пином. На нем висит подсветка дисплея.
Так что если вы владелец шилда - у вас будет включатся выключатся подсветка при нажатии кнопки SELECT. Если шилда у вас нет и вы сами собирали/подключали кнопки - оставьте outPin=13 как в оригинальном туториале и смотрите на встроенный светодиод.

Что дальше?

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


суббота, 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




воскресенье, 8 сентября 2013 г.

Делаем несколько дел одновременно-периодически

продолжение статьи МИГАЕМ СВЕТОДИОДОМ БЕЗ DELAY() или всегда ли официальный примеру учат "хорошему".

Сразу оговорюсь, в рамках 8-ми битных контроллеров настоящей одновременности добится невозможно (только если какое-то действие будет выполняется независимым аппаратным блоком, например - генерация PWM). Но для подавляющего большинства задач - хватает "псевдо-одновременности". Когда контроллер по очереди "хватается" то за одно дело, то за другое. Если на каждом "деле" он не задерживается слишком на долго, то с точки зрения человека это выглядит как "одновременно".
Типичный пример чем может одновременно заниматься контроллер:

  • мигать диодом (периодически переключать его состояние)
  • читать датчик температуры
  • слушать приходящие команды из Serial
  • сообщать о текущем состоянии переменных в Serial
Так как каждое из этих действий выполняется контроллером за микросекунды (миллисекунды в худшем случае), то пробегая раз за разом по этому списку - мы создадим иллюзию одновременности. Главное - не задерживаться на одном месте не долго. Скажем при мигании диодом - не использовать функцию delay()  (она полностью останавливает выполнение скетча на какое-то время).

В принципе, как обходится без delay() - мы уже умеем благодаря примеру [src="blink..."]. Так же в прошлой статье [src....] мы детально разобрали стилистические ошибки этого примера и постарались его улучшить. Так что сделать, по образу и подобию этого примера два одновременных периодических действия - не должно составить проблемы. Просто заводим для каждого действия заводим свою переменную previousTime  (естественно имя чуть-чуть меняем) и оборачиваем действий в if(millis()-previousTime)

Два дела сразу

Скажем мы хотим мигать диодом раз в пол-секунды и, при этом выводить, раз в секунду, в Serial текущие значение millis()  (что-бы даже не видя ардуины физически иметь уверенность что скетч не завис).

Полностью по аналогии со скетчем из предыдущей статьи


/* Blink And Print Without Delay
 2013
 by alxarduino@gmail.com
 http://alxarduino.blogspot.com/2013/09/BlinkAndPrintWithoutDelay.html
 */
 
#define LED_PIN  13      // номер выхода,подключенного к светодиоду
#define  BLINK_INTERVAL  5000UL  // интервал между включение/выключением светодиода (5 секунд)
#define PRINT_INTERVAL 1000UL  // периодичность вывода времени в Serial (1 cсекунда)
#define SERIAL_SPEED 9600 // скорость работы Serial
 
void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(LED_PIN, OUTPUT);      
  
  // задаем скорость работы ком-порта
  Serial.begin(SERIAL_SPEED);
  
  
}
 
void loop()
{
  // мигаем диодом   (периодически переключаем его состояние)
  static unsigned long prevBlinkTime = 0; // время когда последний раз переключали диод
  if(millis() - prevBlinkTime > BLINK_INTERVAL) {
    prevBlinkTime = millis();  // 
    digitalWrite(LED_PIN,!digitalRead(LED_PIN)); 
  }
  
  // периодически выводим millis() в Serial
  static unsigned long prevPrintTime=0;
  if(millis()-prevPrintTime>PRINT_INTERVAL){
    prevPrintTime=millis();
    
    Serial.print("Current time:");
    Serial.println(millis());

  }
  
}

Причесываем код

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

/* Blink And Print Without Delay
 2013
 by alxarduino@gmail.com
 http://alxarduino.blogspot.com/2013/09/BlinkAndPrintWithoutDelay.html
 */
 
#define LED_PIN  13      // номер выхода,подключенного к светодиоду
#define  BLINK_INTERVAL  5000UL  // интервал между включение/выключением светодиода (5 секунд)
#define PRINT_INTERVAL 1000UL  // периодичность вывода времени в Serial (1 cекунда)
#define SERIAL_SPEED 9600 // скорость работы Serial
 
void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(LED_PIN, OUTPUT);      
  
  // задаем скорость работы ком-порта
  Serial.begin(SERIAL_SPEED);
  
  
}
 
void loop()
{
  blinkLed(BLINK_INTERVAL);  // мигаем
  printTime(PRINT_INTERVAL); // выводим время
}

// мигает диодом с периодичностью interval
void blinkLed(unsigned long interval ){
  static unsigned long prevTime = 0; // время когда последний раз переключали диод
  if(millis() - prevTime > interval) {
    prevTime = millis();  // 
    digitalWrite(LED_PIN,!digitalRead(LED_PIN)); 
  }
}

// выводит в Serial время с периодичностью interval
void printTime(unsigned long interval){
  static unsigned long prevTime=0;
  if(millis()-prevTime>interval){
    prevTime=millis();
    
    Serial.print("Current time:");
    Serial.println(millis());

  }
}

Шаблон/Заготовка

Как видите, благодаря тому что мы, с помощью ключевого слово static, "спрятали" объявление prevTime внутрь функции (но при этом сохранили ее способность сохранять значение при выходе из функции подобно глобальной переменной) мы смогли внутри обеих функций (blinkLed и printTime) использовать одно и тоже имя переменной prevTime. Нам теперь не нужно хмурить мозг что-бы каждый раз придумывать новое уникальное имя переменное. Более того, у нас получилась как-бы "универсальная заготовка периодической функции".

// "заготовка/шаблон функции" которая периодически выполняет КАКОЕ-ТО-ДЕЙСТВИЕ
void somePeriodical(unsigned long interval){
  static unsigned long prevTime=0;
  if(millis()-prevTime>interval){
    prevTime=millis();
    
    КАКОЕ-ТО-ДЕЙСТВИЕ;

  }
}

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

А зачем нужен параметр функции?

Так же, если вы заметили, этот шаблон не содержит жестко прописанного интервала времени. Мы его "подаем снаружи" с помощью параметра функции. Это дает нам возможность, примеру, сам интервал мигания положить в какую-то переменную и менять его по мере надобности (ну скажем в зависимости от температуры). 
Или, мигать с разной частотой в зависимости от того нажата кнопка или нет. Примерно так:

void loop(){
  
  if(digitalRead(PIN_BUTTON)) blinkLed(BLINK_INTERVAL)
                        else blinkLed(BLINK_INTERVAL*2); // в два раза медленее
  .....
}

Тут мы мигаем либо с нормальной, либо с половинной частотой в зависимости от того HIGH или LOW уровень сейчас на пине PIN_BUTTON (нажата или нет кнопка)

На сегодня все. В следующий раз рассмотрим попробуем "сделать себе удобно". Посмотрим как можно сделать что-бы ради мелочи (например нам нужно периодически выводить значение какой-то переменной, причем код нужне "на 2 минуты", в отладочных целях, потом его выкинем) не заводить целую специальную функцию, не выписывать "прицеп" из if-а и milis() и т.п.


P.S. Задать вопросы и почитать обсуждение вы можете либо в комментариях ниже, либо на сайте arduino.ru в ветке Еще раз мигаем светодиодом без Delay | Аппаратная платформа Arduino

UPD: Оформил этот прием в виде библиотеки. Про это следующая статья:  Мигаем без delay() с комфортом