воскресенье, 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 как в оригинальном туториале и смотрите на встроенный светодиод.

Что дальше?

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


12 комментариев:

  1. Красивый код "наше все". :-)
    С нетерпением ждем продолжения (и исправления мелких опечаток в коде, вроде такой: if(valmax)max=val; а то вдруг не поймут)
    И по кнопкам: (шилд судя по проверке - 1.1)
    RIGHT=0
    UP=145
    DOWN=329
    LEFT=505
    SELECT=741

    ОтветитьУдалить
  2. Использовал ваш код.
    Адаптировал его с обработкой короткого и длительного нажатия - и получилось!
    Спасибо за ваш труд.

    ОтветитьУдалить
    Ответы
    1. Здравствуйте, можете пожалуйста показать как Вы различали длинные и короткие нажатия? заранее спасибо!

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

    ОтветитьУдалить
  4. if(valmax)max=val;
    TEST_KEY_ANALOG.ino: In function 'void readSamples()':
    TEST_KEY_ANALOG:65: error: expected `)' before 'if'
    где ошибка,не понимаю, как трактовать

    ОтветитьУдалить
  5. 8К у них там похоже на схеме пропущено, посмотрим что ответят: http://www.dfrobot.com/index.php?route=product/product&keyword=DFR0009&category_id=0&description=1&model=1&product_id=51#comment-2030842201

    ОтветитьУдалить
    Ответы
    1. точнее, оно там неявно набегает :)

      Удалить
  6. Библиотека готова, или проект заброшен?

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