воскресенье, 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() с комфортом

пятница, 30 августа 2013 г.

Мигаем светодиодом без delay()

или всегда ли официальный примеру учат "хорошему".

Обычно это одна из первых проблем с которой сталкивается навичок в микроконтроллерх. Помигал диодом, запустил стандартный скетч blink(), но как только он него возникает желание что-бы контроллер "занимался чем-то еще" его ждет неприятный сюрприз - тут нет многопоточности. Как только он написали где-то что-то типа delay(1000) - обнаруживается что на это строчке "контроллер остановился" и ничего больше не делает (кнопки не опрашивает, датчики не читает, вторым диодом "помигать" не может).
Новичок лезет с этим вопросом на форум и тут же получает ушат поучений: "отказывайтесь от delay()", учитесь работать с millis() и в прочитайте, в конце концов базовые примеры. В частности Мигаем светодиодом без delay()

Приведу его код:
/* Blink without Delay
 2005
 by David A. Mellis
 modified 8 Feb 2010
 by Paul Stoffregen
 */

const int ledPin =  13;      // номер выхода, подключенного к светодиоду
// Variables will change:
int ledState = LOW;             // этой переменной устанавливаем состояние светодиода 
long previousMillis = 0;        // храним время последнего переключения светодиода

long interval = 1000;           // интервал между включение/выключением светодиода (1 секунда)

void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(ledPin, OUTPUT);      
}

void loop()
{
  // здесь будет код, который будет работать постоянно
  // и который не должен останавливаться на время между переключениями свето
  unsigned long currentMillis = millis();
 
  //проверяем не прошел ли нужный интервал, если прошел то
  if(currentMillis - previousMillis > interval) {
    // сохраняем время последнего переключения
    previousMillis = currentMillis;  

    // если светодиод не горит, то зажигаем, и наоборот
    if (ledState == LOW)
      ledState = HIGH;
    else
      ledState = LOW;

    // устанавливаем состояния выхода, чтобы включить или выключить светодиод
    digitalWrite(ledPin, ledState);
  }
}
  



В принципе отправка к этому примеру - вполне правильна. В нем действительно виден стандартный паттерн как нужно выполнять какое-то переодическое действие (или отложенное):
1. Сохраняем время в какую-то переменную
2. В loop() все время смотрим на разницу "текущие-время - сохраненное"
3. Когда эта разница превысила какое-то значение (нужный нам "интервал переодичности")
4. Выполняем какое-то действие (меняем состояние диода, заново запоминаем "стартовое время и т.п.")

С задачей "объяснить идею" - пример справляется. Но, с моей точки зрения, в нем есть несколько методологических ошибок. Написание скетчек в подобно стиле - рано или поздно приведет к проблемам.

Итак, что же тут не так?

1. Не экономно выбираем тип переменных

Переменная ledPin у нас объявлена как тип int. Зачем?  Разве номер пина может быть очень большим числом? Или может быть отрицательным числом? Зачем под его хранение выделять два байта, когда вполне хватит одного. Тип byte может хранить числа от 0 до 255. Для номера пина - этого вполне хватит.

  const byte ledPIN = 13; //  номер выхода, подключенного к светодиоду

Этого будет вполне достаточно.

2. А зачем нам переменная для малых чисел?

А зачем нам тут вообще переменая? (пусть и объявленная как const). Зачем тратить такты процессора на чтение переменной? И расходовать еще один байт?  Воспользуемся директивой препроцессора #define

#define LED_PIN  13 //  номер выхода, подключенного к светодиоду

Тогда еще на этапе компиляции компилятор просто подставить 13-ть везде где в коде используется LED_PIN и не будет выделять отдельных переменных.

3. Тип int опять выбрали как "первое что в голову пришло"?

И опять спотыкаемся на объявлении следующей же переменной. Почему ledState опять int? Кроме того что снова "два байта там где можно один использовать", так еще и "смысловая нагрузка" теряется. Что у нас хранится в переменной? Состояние светодиода. Включен/выключен. Горит/Не горит. Явно же напрашивается тип boolean. По крайней мере до тех пор, пока светодиод у нас может принимать два состояния.

4. И вновь не верный тип. Теперь уже критично

Наверное уже надоел :)  Но объявление следующей переменной мне опять не нравится. Почему previousMillis как long? В нее  мы сохраняем время  возвращаемое функцией millis(). В документации на нее говорится что она возвращает unsigned long  (кстати currentMillis объявили правильно). Значит и все переменные где хранится время должны быть unsigned long.
Эта ошибка с типом - гораздо более опасная. В отличает от предыдущих где тип был выбран "избыточно", в этом случае он выбран "недостаточно". Функция millis() возвращает unsigned long которая может принимать максимальное значение 4,294,967,295 . А в long у нас может поместится 2,147,483,647 . В два раза меньше.  Примерно на 24-тый день в переменной previousMillis из-за этого начнут образовыватся отрицательный значения. И все логика  (currentMillis - previousMillis) - может поломатся.

Конечно мигать диодом 24-дня непрерывно никто не будет. Но если научившись на этом примере, по его образу и подобию сделают, скажем систему полива сада?  Которая через месяц устроит потоп?

Кстати недавно на форуме, как раз и ловили подобную ошибку. Там вместо "unsigned long" подобная переменная была объявлена вообще как int. Повезло в том, что работали не с миллисекундами, на более мелких интервалах. Вместо millis() использовали micros() . И скетч "зависал" через 32-секунды (это число и натолкнуло на необходимость поиска ошибочного int). А если бы был millis() - все бы висло раз в несколько дней. И догадатся "что не так" - было бы гораздо сложней. Грешили бы на "питание/помехи" и т.п.
Вообщем я думаю вы уже поняли, что небрежное обращения с типами, использование "первого попавшегося с которым заработало" - в итоге выливается в часы жизни проведенные за поиском бага.


5. Мелочный придирки

Ну и парочка придирок которые "не ошибки", но можно было-бы компактней написать. Хотя возможно тут автор примера просто старался написать "попроще для новичков". Но раз мы уже "смотрим скетч под лупой.." :)
Переменную previousMillis можно, с помощью ключевого слово static объявить внутри loop(). Не засорять простраство имен глобальных переменных. Полезная привычка.

От переменное ledState - вообще можно отказаться. Текущие состояние пина можно узнать с помощью digitalRead().  Вообщем-то этот "совет" довольно спорен. С переменой ledState - код более читабелен... просто покажу что можно и без нее.

Да и без переменной currentMillis можно обойтись. Просто два раза вызвать функцию millis() . 

6. Наш ответ Чемберлену

Ну вот, а теперь попытаемся переписать этот пример, учтя все "замечания".

 /* Blink without Delay
 2013
 by alxarduino@gmail.com
 http://alxarduino.blogspot.com/2013/08/delay.html
 */

#define LED_PIN  13      // номер выхода,подключенного к светодиоду
#define  INTERVAL  1000UL           // интервал между включение/выключением светодиода (1 секунда)

void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(LED_PIN, OUTPUT);      
}

void loop()
{
  // здесь будет код, который будет работать постоянно
  // и который не должен останавливаться на время между переключениями свето
  
  static unsigned long previousMillis = 0;        // храним время последнего переключения светодиода
 
  //проверяем не прошел ли нужный интервал, если прошел то
  if(millis() - previousMillis > INTERVAL) {
    // сохраняем время последнего переключения
    previousMillis = millis();  
    
    // меняем состояние выхода светодиода на противоположное текущему. 
    // если горит - тушим, не горит - зажигаем.
    digitalWrite(LED_PIN,!digitalRead(LED_PIN));
  }
}

UPD: Больше примеров вы найдете в следующей статье Делаем несколько дел одновременно-переодически

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

воскресенье, 11 августа 2013 г.

Еще одни часы на Ардуине. Часть 2. Выводим время на экран

В прошлой части Еще одни часы на Ардуине. Часть 1. Запускаем часы мы запустили модуль часов DS1307 и научились выводить время в Serial. Но пользоваться такими часами, конечно невозможно. Открывать Serial монитор для что-бы узнать "который час" :)
Так что, сегодня, мы

Подключаем экран:

Мне повезло. В случае Arduino Mega подключение экрана сводится к простому "нахлобучиванию" его на плату (главное, при первом втыкании  не торопится и не погнуть пины). У Меги SDA/SLC не закрываются шилдом. А 5V и GND дополнительно выведено возле D23 и D25. Так что LCD шилд ничуть не мешает подключению RTC шилда.

В случае же UNO - придется чуть-чуть взять в руки паяльник.
Вам потребуется:


Гнезда:

  •  PBS-5 - для выведения вверх SDA/SLC
  • PBS-6 - для выведения земли и питания
  • PBS-7 - что-бы вывести цифровые пины (пока мы их не используем, но что-бы два раза не вставать).

Впаиваем их сюда:


Там специально, производителем, именно для этих целей, оставлены дырочки-площадки.

В результате мы получим что-то такое:

[тут-будет-картинка]

Тут для примера впаяна PBD-5 и в которую воткнут проводок.

Заливаем в дуину такой скетч проверки экрана:
 
#include "LiquidCrystal.h" // библиотека экрана

// select the pins used on the LCD panel
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

void setup(){
   lcd.begin(16, 2); // запускаем библиотеку
   lcd.setCursor(0,0); // курсов в левый верхний угол экрана
   lcd.print("Hello from LCD!!");
}
void loop(){}


Видим на экрнае такое:

[тут-будет-картинка]

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


Который час?

Теперь у нас все готово к выводу времени на экран.  Подключаем RTC модуль (если мы его отключали), берем финальный скетч из  первой части и добавляем в него инициализацию экрана. Строки 1,4,7 из "скетч проверки экрана" прошлого раздела (выделенные строки).

После чего у нас все готово, что-бы написать функцию вывода времени на экран.

Делаем все практически аналогично тому как в прошлой части выводили в Serial. Только функцию называем printTimeToLCD()

 
void printTimeToLCD(){
    byte static prevSecond=0; // тут будем хранить, сколько секунд было при выводе
    
    if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
       lcd.setCursor(0,0); // устанавливаем позицию курсора
       
      lcd.print(RTC.hour); // часы
    
      lcd.print(":"); // разделитель
      lcd.print(RTC.minute);
    
      lcd.print(":"); 
      lcd.print(RTC.second);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

Добавляем вызов этой функции в loop(), заливаем в дуину и... мы сделали свои, пусть и неказистые, но первые электронные часы!!! :) На экране видно текущие время и "меняющие секунды".

А причесать?

Попробуем добавить чуть-чуть "эстетичности". Что мне не нравится в текущем варианте

  1. Хочется выводить время "по центру", а не в углу
  2. Убрать секунды. Мне они не нужны. Пусть лучше мигает двоеточие между часами и минутами (что-бы было видно что "секунды идут").
  3. Привести время к более привычному формату "16:03",  а не как сейчас "16:3"
  4. Если присмотрется в момент смены секунд видно легкое "помаргивание" надписи, она как-бы "пригасает".
Первый пункт - самый простой. Что-бы подвинуть время, нам достаточно сделать

lcd.setCursor(5,0).

Секунды - просто выкидываем :) А разделитель между часами и минутой показываем либо пробелом либо двоеточием. В зависимости от того четная или не четная у нас секунда

lcd.print( (RTC.second %2 )?" ":":");

Что осталось? Дописать "0" перед минутами если они меньше 10-ти

if(RTC.minute<10)lcd.print(0);

Аналогично для часов. Только добавлять впереди будем пробел, а не "0".  Ноль, вперди часов - выглядит некрасиво, а "что-то дописать" - нужно. Что-бы позиция времени не смещалась влево на малых значениях часа.

Наша функция вывода времени приняла вид:

 

void printTimeToLCD(){
    byte static prevSecond=0; // тут будем хранить, сколько секунд было при выводе
    
    if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
       lcd.setCursor(5,0); // устанавливаем позицию курсора
       
      if(RTC.hour<10)lcd.print(" ");
      lcd.print(RTC.hour); // часы
    
      lcd.print( (RTC.second % 2)?" ":":"); // разделитель моргает
      
      if(RTC.minute<10)lcd.print(0); // лидирующий ноль, если нужен
      lcd.print(RTC.minute);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

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

Сегодняшний наш финальный скетч таков:

 

// Author: alxarduino@gmail.com
// Sample Clock App for http://alxarduino.blogspot.com

// Библиотеки необходимые для работы модуля часов
#include "Wire.h"
#include "DS1307new.h"

// библиотека экрана
#include "LiquidCrystal.h" 
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

void setup(){
  Serial.begin(9600);
  if(RTC.isPresent()){ // обнаружен ли модуль?
    Serial.println("RTC Ready"); // все хорошо
  } else {
    Serial.println("Error!!!! RTC Module not found"); // сообщаем о проблеме
    while(1); // и останавливаем скетч
  }
  
  lcd.begin(16, 2); // запускаем библиотеку экрана
}

void loop(){
  RTC.getTime();// получить время от модуля
  printTimeToSerial(); // выводим полученное время в лог
  printTimeToLCD();; // выводи время на экран
  doSerialCommands(); // слушаем и выполняем команды из Serial
}

// Выводит текущие время в Serial
void printTimeToSerial(){

  byte static prevSecond=0; // тут будем хранить, сколько секунд было при прошлом отчете
  
  if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
    Serial.print(RTC.hour); // часы
  
    Serial.print(":"); // разделитель
    Serial.print(RTC.minute);
  
    Serial.print(":"); 
    Serial.println(RTC.second);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

// Выводит текущие время на LCD

void printTimeToLCD(){
    byte static prevSecond=0; // тут будем хранить, сколько секунд было при выводе
    
    if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
       lcd.setCursor(5,0); // устанавливаем позицию курсора
       
      if(RTC.hour<10)lcd.print(" ");
      lcd.print(RTC.hour); // часы
    
      lcd.print( (RTC.second % 2)?" ":":"); // разделитель моргает
      
      if(RTC.minute<10)lcd.print(0); // лидирующий ноль, если нужен
      lcd.print(RTC.minute);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

// устанавливает часы модуля на какое-то заранее определенное время
void setSomeTime(){
  RTC.stopClock();// останавливаем часы
  RTC.fillByHMS(19,15,0); // "подкручиваем стрелки на 19:15:00
  RTC.setTime();// отправляем "подкрученное время" самому модулю
  RTC.startClock(); // и опять запускаем часы
}

// слушает из Serial команды и выполняет их. Каждая команда - один символ.
// доступны команды:
//  s - установить время указанное в функции setSomeTime()
void doSerialCommands(){
  if(Serial.available()){ // что-нибудь пришло?
    char ch=Serial.read(); // читаем что пришло
    
    switch(ch){
      case 's': // команда установки времени
           setSomeTime(); // устанавливаем
           break;
           
       // тут, в будущем, мы можем добавлять дополнительные команды
      default:;
           // на неизвестную команду - ничего не делаем
    };
  }
}

На LCD Это выглядит примерно так:
В В реальности, конечно все симпатичней, использовать фотоаппарат я умею только "в автоматическом режиме".

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

суббота, 10 августа 2013 г.

Еще одни часы на Ардуине. Часть 1. Запускаем часы


Для следующего проекта, попались мне в руки два таких шилда:

Arduino LCD KeyPad Shield (SKU: DFR0009) - Robot Wiki
Текстовый экран 16x2 плюс четыре кнопки с помощью делителя напряжения заведенные на пин A0
И шилд часов реального времени: Real Time Clock Module (DS1307) V1.1 (SKU:DFR0151) - Robot Wiki


Тут могла быть ваша реклама вашего магазина шилдов :)

Руки зачесались...
Понятно, что это 1001 проект  "часы на ардуине" и можно просто взять что-то готовое. Но у меня две цели: поиграться с модулями и показать как "вырастает проект". Правильный, с моей точки зрения, процесс. В противовес тому, как многие новички пытаются "накупить железо", составить 1000 "хотелок" и запустить все это сразу.

Итак, раз мы будем идти мелкими шагами - откладываем пока наш экран в сторону. Для начала освоимся с часами.

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


Подключаем их в соотвествии с wiki-производителя:
Линию DS - можно, пока, не подключать (пунктирная линия). С ней будем разбиратся позже.

Но у меня плата - Arduino Mega. У нее шина I2C (по ней передает данные модуль часов) расположены не на A4,A5, а на D20,D21
Поэтому я подключал SDA->D20 ,  SLC->D21

Если у вас другая плата - можете сверится с этой табличкой:
ПлатаI2C / TWI pins
Uno, EthernetA4 (SDA), A5 (SCL)
Mega256020 (SDA), 21 (SCL)
Leonardo2 (SDA), 3 (SCL)
Due20 (SDA), 21 (SCL)

Щупаем програмно

Что-бы комфортно работать с часами, на закапываясь в даташиты нам потребуется скачать и установить библиотеку для DS1307. Производитель модуля - предоставляет ее, но мне больше понравилась альтернативная. Вот эта: ds1307new - DS1307 RTC Library with NV-RAM support - Google Project Hosting
Скачиваем ее, распаковываем, забрасываем в папку libraries и пишем первый наш код, проверяющий видит ли ардуина наши часы:
   // Библиотеки необходимые для работы модуля часов
#include <Wire.h>
#include <DS1307new.h>

void setup(){
  Serial.begin(9600);
  if(RTC.isPresent()){ // обнаружен ли модуль?
    Serial.println("RTC Ready"); // все хорошо
  } else {
    Serial.println("Error!!!! RTC Module not found"); // сообщаем о проблеме
    while(1); // и останавливаем скетч
  }
}

void loop(){
}

Заливаем, открываем Serial монитор и надпись:


Для перепроверки, отсединяем, скажем, линю SLC. Закрываем, открываем монитор (перезапускаем скетч) и видим что надпись у нас сменилась на "Error!!!! RTC Module not found".
Возвращаем проводок на место и опять видим "RTC Ready". Наш "детектор проблем" - работает.

А который час?

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

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

void loop(){
  RTC.getTime();// получить время от модуля
  printTimeToSerial(); // выводим полученное время
}
 
void printTimeToSerial(){
  Serial.print(RTC.hour); // часы
  
  Serial.print(":"); // разделитель
  Serial.print(RTC.minute);
  
  Serial.print(":"); 
  Serial.println(RTC.second);
}
В логе (сериал мониторе видим) что-то типа:
17:53:35
17:53:35
17:53:35
17:53:35
17:53:35

Много, много раз...
C одной стороны - хорошо. Модуль посылает нам время и мы научились отправлять его в Serial. С другой... он старается делать это как можно чаще. Насколько хватает пропускной способности Serial. А нам - это не нужно :) Столько дублирования. Поэтому давайте отправлять время в Serial только если, скажем секунды, изменились с прошлого вызова функции.
// Выводит текущие время в Serial
void printTimeToSerial(){

  byte static prevSecond=0; // тут будем хранить, сколько секунд было при прошлом отчете
  
  if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
    Serial.print(RTC.hour); // часы
  
    Serial.print(":"); // разделитель
    Serial.print(RTC.minute);
  
    Serial.print(":"); 
    Serial.println(RTC.second);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}


Теперь наш лог, примет такой вид:

RTC Ready
18:0:26
18:0:27
18:0:28
18:0:29

Каждая надпись - появляется раз в секунду. Можно дествительно смотреть как "часы идут".

 Кстати, возможно вы заметили что время выглядит "не очень красиво", вмето привычных 18:00:26, у нас получилось 18:0:26. Но так как работа с Serial у нас "вспомогательная", мы не будет отвлекатся на решение этой проблемы. Но вернемся к ней и будем "наводить красоту", когда время будет показываться уже на LCD экране.


А точнее?

Получать время - это хорошо. Но его же нужно еще уметь устанавливать. В конечном итоге - будем делать это с помощью кнопок, но пока... сделаем временное решение. Самое простейшие: забъем нужное нам время прямо в скетч. И будем его посылать модулю, если из Serial нам пришел символ 's'

Функция установки времени:

void setSomeTime(){
    RTC.stopClock();// останавливаем часы
    RTC.fillByHMs(19,15,0); // "подкручиваем стрелки на 19:15:00
    RTC.setTime();// отправляем "подкрученное время" самому модулю
    RTC.startClock(); // и опять запускаем часы
}

Функция которая слушает команды из Serial

 void doSerialCommands(){
  if(Serial.available()){ // что-нибудь пришло?
    char ch=Serial.read(); // читаем что пришло
    
    switch(ch){
      case 's': // команда устновки времени
           setSomeTime(); // устанавливаем
           break;
           
       // тут, в будущем, мы можем добавлять дополнительные команды
      default:;
           // на неизвестну команду - ничего не делаем
    };
  }
} 

Все. Нам осталось только вставить вызов doSerialCommand() в loop()  для регулярного опроса команда из Serial. После чего наш скетч принимает вид:

// Author: alxarduino@gmail.com
// Sample Clock App for http://alxarduino.blogspot.com

// Библиотеки необходимые для работы модуля часов
#include "Wire.h"
#include "DS1307new.h"

void setup(){
  Serial.begin(9600);
  if(RTC.isPresent()){ // обнаружен ли модуль?
    Serial.println("RTC Ready"); // все хорошо
  } else {
    Serial.println("Error!!!! RTC Module not found"); // сообщаем о проблеме
    while(1); // и останавливаем скетч
  }
}

void loop(){
  RTC.getTime();// получить время от модуля
  printTimeToSerial(); // выводим полученное время
  doSerialCommands(); // слушаем и выполняем команды из Serial
}

// Выводит текущие время в Serial
void printTimeToSerial(){

  byte static prevSecond=0; // тут будем хранить, сколько секунд было при прошлом отчете
  
  if(RTC.second!=prevSecond){ // что-то делаем только если секунды поменялись
    Serial.print(RTC.hour); // часы
  
    Serial.print(":"); // разделитель
    Serial.print(RTC.minute);
  
    Serial.print(":"); 
    Serial.println(RTC.second);
    
    prevSecond=RTC.second; // запомнили когда мы "отчитались"
  }
}

// устанавливает часы модуля на какое-то заранее определенное время
void setSomeTime(){
  RTC.stopClock();// останавливаем часы
  RTC.fillByHMS(19,15,0); // "подкручиваем стрелки на 19:15:00
  RTC.setTime();// отправляем "подкрученное время" самому модулю
  RTC.startClock(); // и опять запускаем часы
}

// слушает из Serial команды и выполняет их. Каждая команда - один символ.
// доступны команды:
//  s - установить время указанное в функции setSomeTime()
void doSerialCommands(){
  if(Serial.available()){ // что-нибудь пришло?
    char ch=Serial.read(); // читаем что пришло
    
    switch(ch){
      case 's': // команда установки времени
           setSomeTime(); // устанавливаем
           break;
           
       // тут, в будущем, мы можем добавлять дополнительные команды
      default:;
           // на неизвестную команду - ничего не делаем
    };
  }
}


Заливаем.Открываем Serial Монитор. Набираем s (в нижнем регистре) и нажимает "отправить"

Итого:

  • Мы подключили модуль часов DS1307. 
  • Убедились что Ардуина "его видит" (и сообщает нам если не видит). 
  • Научились получать от модуля время и отправлять его в Serial.
  • Научились устанавливать время модуля по команде из Serial

В следующей части: мы подключим уже LCD и выведем время на него

воскресенье, 19 мая 2013 г.

Умная Серва. Часть 1

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


Типичной задачей при использовании сервы является

"нужно повернутся в позицию XXX, после чего выполнить какое-то действие".

Предположим у нас стоит задача "серва стоит в 0, нужно ее  повернуть в позицию 90-то градусов и включить светодиод".

Раннее зажигание

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


и пытаемся ее крутить:
#include "Servo.h" // подключаем библиотеку сервы

#define LED_PIN 13  // используем встроенный светодиод
#define SERVO_PIN 9  // подключаем серву на 9-ты pin
Servo myservo;

void setup(){
  pinMode(LED_PIN,OUTPUT); // настраиваем выход светика
  myservo.attach(SERVO_PIN); // подключаем серву
  
  myservo.write(90); // поворачиваем серву
  digitalWrite(LED_PIN,HIGH); // включаем светик
  
}

void loop(){
}


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

Что же делать?  Первое что приходит в голову - нужно как-то узнавать текущие положение сервы и включать светодиод только когда она займет нужную позицию.

К сожалению, для того что-бы узнать физическое положение сервы, нужно дополнительное оборудование (или вносить модификацию в саму серву, или искать дорогую серву обратной связью).
К счастью, если нам не нужна сверх-высокая точность, когда достаточно что-бы светодиод загорелся ПОСЛЕ того как серва займет положение (а не "точно в этот момент").

Для этого мы просто остановим скетч и подождем пока серва прокрутится

...
 myservo.write(90); // поворачиваем серву
 delay(2000); // ждем пока серва повернется
 digitalWrite(LED_PIN,HIGH); // включаем светик
...

Откуда взялась цифра 2000?  Я просто "прикинул" что двух секунд будет достаточно что-бы любая популярная серва успела повернутся на 90-то градусов.

Теперь у нас светодиод загорится не раньше чем серва станет в 90-то градусов.

Кстати сейчас мы можем поправить еще один наш "косяк". Скетч замечательно крутит серву в 90-то градусов, но его работу не удобно наблюдать если серва уже стоит в 90-то. Нужно выключать Ардуину, крутить руками серву в 0 (с риском ее сломать) просто что-бы еще раз увидеть как отрабатывает скетч.

Исправим в это. Будем при старте ее выкручивать в ноль, а уж потом ставить в 90-то и зажигать светодиод. Естественно не забудем дать ей время на "выкрутится в ноль".

void setup(){
  pinMode(LED_PIN,OUTPUT); // настраиваем выход светика
  myservo.attach(SERVO_PIN); // подключаем серву

  // ставим серву в исходную позицию
  myservo.write(0);
  delay(2000); // ждем пока станет в 0
  
  // крутим в 90-то и зажигаем светик
  myservo.write(90); // поворачиваем серву
  delay(2000); // даем время на поворот
  digitalWrite(LED_PIN,HIGH); // включаем светик
  
}


Позднее зажигание

Но теперь у нас нас имеется противоположный эффект - светодиод загорается с опозданием.
Как это побороть?  Да просто. Опытным путем подбираем значение вот этой задержки delay(), после myservo.write . Не слишком маленькое что-бы серва успевала повернутся и не слишком большое, что-бы пауза между "серва перестала крутится" и "светодиод загорелся" - не слишком раздражала.

Умное зажигание.

А что будет если завтра, мы решим поворачивать не на 90, а на, скажем 135-ть? Или таких поворотов нам нужно будет сделать сотню, на разные углы?  Для каждого подбирать время задержки?

Естественно - нет. Мы будем его "высчитывать". Возьмем в руки секундомер,  в предыдущем скетче будем ставить серву не в 90, а в 180-градусов и засечем секундомером сколько времени это занимает. Ну либо, опять-таки подберем delay методом "перелет-недолет" (в следующем посте расскажу как самой Ардуиной можно точнее замерить этот интервал).

Предположим мы замеряли, что полный поворот сервы (180-градусов), занимает 1200 миллисекунд. Отсюда мы можем узнать время поворота на один градус 1200ms/180=6.66ms
Значит поворот на 135-градусов требует задержки 6.66*135=900 ms.

Естественно каждый раз вычислять руками эти задержки - совершенно "не по программески". Напишем вспомогательную функцию, и в будущем вместо myservo.write/delay будем вызвать ее. Пусть она делает все что нам нужно. 
#define DELAY_180 1200 // время, в миллисекундах, за которое серва поворачивается на 180 градусов

// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
void setServo(int newPos){
  // вычисляем нужную задержку
  unsigned long servoDelay=(DELAY_180 /180)*newPos;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
}
Теперь чуть-чуть изменим на setup() на использование нашей новой функции
  void setup(){
  ...

  // ставим серву в исходную позицию
  myservo.write(0);
  delay(DELAY_180); // ждем пока станет в 0
  
  // крутим в 90-то и зажигаем светик
  setServo(90);
  digitalWrite(LED_PIN,HIGH); // включаем светик
  
}

Как видите, теперь в коде с "бизнес логикой" исчез delay(). Тем что-бы про него "не забыть" и его вычислением - занимается вспомогательная функция.
Но в строках 5 и 6-ть, я "по старинке" использую write/delay. Это сделано потому что при включении контроллера я еще не знаю где стоит серва. Следовательно функция setServo - не может вычислить необходимое время поворота. Поэтому я просто дал максимально возможную задержку (где-бы серва не стояла,она успеет провернутся до нуля).

Запоминаем позицию

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

Весь наш код, целиком, теперь выглядит так:
  void setup(){
#include "Servo.h" // подключаем библиотеку сервы

#define LED_PIN 13  // используем встроенный светодиод
#define SERVO_PIN 9  // подключаем серву на 9-ты pin

Servo myservo;


#define DELAY_180 1200 // время, в миллисекундах, за которое серва поворачивается на 180 градусов


void setup(){
  pinMode(LED_PIN,OUTPUT); // настраиваем выход светика
  myservo.attach(SERVO_PIN); // подключаем серву

  init_servo();
  
  run_servo_logic();
  
}

void loop(){
}

void init_servo(){
  myservo.attach(SERVO_PIN); // подключаем серву

  // ставим серву в исходную позицию
  myservo.write(0);
  delay(DELAY_180); // ждем пока станет в 0
}


void run_servo_logic(){
   // крутим в 90-то и зажигаем светик
  setServo(90);
  digitalWrite(LED_PIN,HIGH); // включаем светик
}


// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
void setServo(int newPos){
  // вычисляем нужную задержку
  unsigned long servoDelay=(DELAY_180 /180)*newPos;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
}

Теперь мы можем немного усложнить нашу задачу. Будем выполнять не один поворот. Пусть наша серва должна, по очереди устанавливаться в следующий позиции 90, 45, 135,90, 180
И по достижении каждой позиции включать/выключать светик.

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

void run_servo_logic(){
   // крутим в 90-то и зажигаем светик
  setServo(90);
  toggleLed(); // включаем светик
  
  setServo(45);
  toggleLed(); // выключаем
  
  setServo(135);
  toggleLed(); 
  
  setServo(90);
  toggleLed(); 
  
  setServo(180);
  toggleLed(); 
}


// переключает светик в противоположное состояние
void toggleLed(){
  digitalWrite(LED_PIN,!digitalRead(LED_PIN));
}

Пробуем выполнить, и замечаем, что первый setServo(90) - у нас выполняется нормально, второй setServo(45) - тоже (на самом деле - просто повезло), а вот все остальные - явно ждут дольше чем нужно. Опять наблюдаем задержку между "серва стала в позицию" и "светодиод поменял свое состояние".  Где-то (в функции setServo()) у нас ошибка с вычислением времени задержки.
Внимательно всматриваемся в setServo() и понимаем, что, при вычислении, мы неявно предполагаем что серва, перед поворотом, стоит в нуле. Скажем после  вызова setServo(90) , следует вызов setServo(180). Нужно "довернуть" серву всего на 90-то градусов, а время задержки мы рассчитали как для полного поворота на 180.
Что-бы скорректировать эту ошибку, будем передавать в функцию setServo еще один параметр - текущие положение сервы. И вычислять время поворота исходя из разницы "старого" и"нового положения". При этом, воспользуемся встроенной функцией abs(X), которая возвращает модуль числа. Что-бы при вращении против-часовой стрелки, не получать отрицательные задержки
void run_servo_logic(){
   // крутим в 90-то и зажигаем светик
  setServo(0,90);
  toggleLed(); // включаем светик
  
  setServo(90,45);
  toggleLed(); // выключаем
  
  setServo(45,135);
  toggleLed(); 
  
  setServo(135,90);
  toggleLed(); 
  
  setServo(90,180);
  toggleLed(); 
}


// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
// для этого используют oldPos - старое положение сервы
void setServo(int oldPos,int newPos){
  // вычисляем нужную задержку
  int angleDiff=abs(newPos-oldPos);
  unsigned long servoDelay=(DELAY_180 /180)*angleDiff;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
}

Теперь все работает "как нужно".
Но такое решение "некрасиво". Оно заставляет нас постоянно ,руками указывать прошлое положение сервы. Что с одной стороны - просто неприятная работа, с другой - увеличивает шанс ошибки. Если мы, скажем, переставим местами поворот на 45 и 135, то на нужно будет не забыть поправить прошлое положение в трех местах.

Давайте исправим это, и сделаем что-бы функция setServo - опять принимала только один параметр newPos - новое положение. А "прошлое" - сама запоминала в какую-то глобальную переменную.



byte prevServoPos=0; // сюда будем запоминать, куда мы установили серву. изначально - стоит в нуле.

// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
// для этого используют  глобальную prevServoPos - старое положение сервы
void setServo(byte newPos){
  // вычисляем нужную задержку
  int angleDiff=abs(newPos-prevServoPos);
  unsigned long servoDelay=(DELAY_180 /180)*angleDiff;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
  
  prevServoPos=newPos;// запомнили где теперь стоит серва
}


Теперь мы можем выкинуть из головы заботу о "где сейчас серва" и опять указывать только "куда нужно повернутся" setServo(НОВАЯ_ПОЗИЦИЯ);

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

В итоге наш скетч примет такой вид:

#include "Servo.h" // подключаем библиотеку сервы

#define LED_PIN 13  // используем встроенный светодиод
#define SERVO_PIN 9  // подключаем серву на 9-ты pin
#define DELAY_180 1200 // время, в миллисекундах, за которое серва поворачивается на 180 градусов

Servo myservo;
byte prevServoPos=0; // сюда будем запоминать, куда мы установили серву. изначально - стоит в нуле.

void setup(){
  pinMode(LED_PIN,OUTPUT); // настраиваем выход светика
  myservo.attach(SERVO_PIN); // подключаем серву

  init_servo();
  
  run_servo_logic();
  
}

void loop(){
}

void init_servo(){
  myservo.attach(SERVO_PIN); // подключаем серву

  // ставим серву в исходную позицию
  myservo.write(0);
  delay(DELAY_180); // ждем пока станет в 0
}


void run_servo_logic(){
  byte angles[]={90,45,135,90,180}; // углы в которые нужно ставить серву
  for(byte i=0;i<5;i++){
    setServo(angles[i]); // ставим серву в очередной угол
    toggleLed(); // переключаем состояние светика
  }
}

// переключает светик в противоположное состояние
void toggleLed(){
  digitalWrite(LED_PIN,!digitalRead(LED_PIN));
}

// Блокирующий аналог Servo.write()
// устанавливает myservo в позицию newPos 
// и делает задержку, минимально необходимую для выполнения команды
// для этого используют  глобальную prevServoPos - старое положение сервы
void setServo(byte newPos){
  // вычисляем нужную задержку
  int angleDiff=abs(newPos-prevServoPos);
  unsigned long servoDelay=(DELAY_180 /180)*angleDiff;
  
  // крутим серву
  myservo.write(newPos);
  delay(servoDelay); // применяем вычисленную задержку
  
  prevServoPos=newPos;// запомнили где теперь стоит серва
}
Конечно, это "итоговый" скетч только для данной статьи. Наши игры с сервой - мы еще не закончили. Названию "умная серва" - еще не соотвествуем, но... В следующих статьях мы рассмотрим как наш код опять сделать "не блокирующим" (избавимся от delay()), но при этом сохранить возможность зажигать светик "синхронизировано" с положением сервы. Это нам может потребоваться, если мы захотим, одновременно управлять несколькими сервами, опрашивать, во время вращения, кнопки/датчики и т.п.
Ну и на закуску видео как все это работает. Единственное отличие от кода приведенного выше, для того что-бы на видео было легче уследить за сервой и диодом, после toggleLed(), я добавил delay(1000); Что-бы в каждой позиции серва останавливалась на 1 секунду.