Блокирующая обработка тактовой кнопки для Arduino. Настолько полный гайд, что ты устанешь его читать

image

В одной из своих прошлых статей я писал про подключение тактовой тактильной кнопки. И, казалось бы, такой простой вопрос, вызвал «бурю» в комментариях. Публика разделилась на два лагеря: на тех, кто все знает, но обычно молчит; и тех, кто не знает, и стесняется спросить. А я так и не понял, к какому лагерю отношусь!

Поиски в интернете по запросу «программирование кнопки для Arduino» выдает весьма противоречивый контент. Где-то код очень крутой, но из-за скудного описания не понятный. А где-то код очень простой, и от того не понятно, что с ним можно делать.

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

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

Дисклеймер. Текст рассчитан на публику, которая только начинает познавать азы программирования контроллеров Arduino. Запрещено к просмотру людям с ослабленной психикой или психикой с долголетней инженерной выдержкой и беременным женщинам (это так, на всякий случай, а то мало ли… британские ученые пока до исследований на эту тему не добрались). Материал предназначен исключительно для просмотра лицам, не достигшим 21 года.

Вступление


Для экспериментов я буду использовать плату Arduino Uno. Программировать буду в Arduino IDE и для отладки использую ISIS Proteus 8.6. Это практически стандартный набор для начинающих любителей микроконтроллеров.

Функции для обработки кнопки, как и любые другие функции, могут быть блокирующими и неблокирующими.

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

О подключении кнопки к Arduino вы можете почитать в одной из моих прошлых статей. А теперь погнали программировать.

Блокирующий обработчик кнопки


Рассмотрим самый простой способ обработки кнопки, который можно использовать при реализации линейных алгоритмов. Для этого предлагаю собрать простую схему. Подключите кнопку между цифровым входом 2 платы Arduino Uno и общим проводом GND, как показано на рисунке. Для отладки кода и демонстрации его работоспособности я буду использовать терминал. При моделировании в Proteus для этого нужно добавить виртуальную модель терминала. При физических экспериментах можно воспользоваться терминалом в Arduino IDE.

image

Для примера я напишу программу, которая будет выводить в терминал надпись «кнопка нажата» или «кнопка не нажата» при соответствующих событиях. Обобщенный алгоритм этой программы можно изобразить следующим образом.

image

Для начала, в функции »setup ()» настраиваю порт для управления кнопки и запускаю USART для передачи данных со скоростью 9600bps. Номер цифрового порта Arduino, к которому подключена кнопка, для удобства программирования назову »BUTTON_INPUT».

//--------------------------------------------------
//линия, к которой подключена кнопка
#define BUTTON_INPUT  2

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настраиваем порт для обработки кнопки
  //вход с подтяжкой к плюсу питания
  pinMode(BUTTON_INPUT, INPUT_PULLUP);

  //настройки для передачи данных в терминал 
  Serial.begin(9600);
}

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

image

Текст программы для обработки кнопки я размещу в теле функции »loop ()». Эта функция вызывается фактически непрерывно и может считаться бесконечным циклом программы. Надписи для вывода в терминал я написал на английском, чтобы не морочить голову с настройками терминала и подбором кодировок шрифтов.

//супер цикл
void loop() {
  //если кнопка нажата
  if(digitalRead(BUTTON_INPUT) == LOW){
    Serial.print("button pressed\r");
  }
  //если кнопка не нажата
  else{
    Serial.print("button not pressed\r");
  }
}

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

Обработка отдельных нажатий кнопки


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

image

Для реализации данного алгоритма обработки кнопки я модифицирую текст программы в функции »loop ()» следующим образом:

//супер цикл
void loop() {
  //проверка нажатия кнопки
  if(digitalRead(BUTTON_INPUT) == LOW){
    //один раз выводим текст
    Serial.print("button pressed\r");
    //ничего не делаем, пока кнопка нажата
    while(digitalRead(BUTTON_INPUT) == LOW);
  }
}

При выполнении полученного примера программного кода в Proteus, текст в терминал действительно будет выводиться однократно при каждом отдельном нажатии на кнопку. Но вот, если попробовать выполнить программу в «реальном» железе, то при каждом нажатии кнопки в терминал может выскочить одновременно несколько строк текста.

Защита от дребезга контактов


Дело в том, что у реальной кнопки с сухими контактами замыкание и размыкание контактной группы ни когда не происходит мгновенно. Нажатие на кнопку или ее отпускание всегда будет сопровождаться многократными кратковременными замыканиями. Этот эффект называют »дребезг контактов». А так как микроконтроллер обладает достаточно высокой производительностью, то программа успевает несколько раз сработать и определяет дребезг, как отдельные нажатия кнопки.

image

Самым простым способом избавиться от дребезга контактов в нашей программе является использование задержки времени. Задержка времени вводится сразу после первой фиксации замыкания кнопки. Длительность задержки подбирается таким образом, чтобы за ее время все переходные процессы гарантированно завершились. После чего кнопка опрашивается повторно. На практике я обычно использую задержку в 50 мс  — 100 мс.

image

Макроопределение »#define BUTTON_PROTECTION 50» лучше разместить в самом начале программы. Величину задержки можно подобрать по своему вкусу.

//величина задержки для защиты от дребезга кнопки
#define BUTTON_PROTECTION 50

//--------------------------------------------------
//супер цикл
void loop() {
  //проверка нажатия кнопки
  if(digitalRead(BUTTON_INPUT) == LOW){
    //пауза для защиты от дребезга
    delay(BUTTON_PROTECTION);  
    //повторный опрос кнопки
    if(digitalRead(BUTTON_INPUT) == LOW){
      //один раз выводим текст
      Serial.print("button pressed\r");
      //ничего не делаем, пока кнопка нажата
      while(digitalRead(BUTTON_INPUT) == LOW);
    }
  }
}

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

image

Если кнопка не была нажата, а произошел случайный дребезг ее контактов, то после выполнения задержки времени второй оператор if не сработает. А значит, обработка нажатия кнопки не произойдет.

image

Пишем функцию для обработки кнопки


Для удобства использования полученного кода, я размещу его в функцию »get_button ()». Функция будет возвращать значение »buttonPress» при нажатии кнопки, и »buttonNotPress» — если кнопка не нажата.

//--------------------------------------------------
//линия, к которой подключена кнопка
#define BUTTON_INPUT  2

//величина задержки для защиты от дребезга кнопки
#define BUTTON_PROTECTION 50

//физическое состояние кнопки
enum ButtonResult {buttonNotPress, buttonPress};

//--------------------------------------------------
//обработка кнопки
enum ButtonResult get_button(void){
  if(digitalRead(BUTTON_INPUT) == LOW){
    //пауза для защиты от дребезга
    delay(BUTTON_PROTECTION);  
    //повторный опрос кнопки
    if(digitalRead(BUTTON_INPUT) == LOW){
      //ничего не делаем, пока кнопка нажата
      while(digitalRead(BUTTON_INPUT) == LOW);
      //сообщаем, что кнопка нажата
      return buttonPress;
    }
  }

  //сообщаем, что кнопку не нажимали
  return buttonNotPress;
}

Для проверки работоспособности функции можно написать следующий код, который будет выводить в терминал сообщение «кнопка нажата».

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настраиваем порт для обработки кнопки
  //вход с подтяжкой к плюсу питания
  pinMode(BUTTON_INPUT, INPUT_PULLUP);

  //настройки для передачи данных в терминал 
  Serial.begin(9600);
}

//--------------------------------------------------
//супер цикл
void loop() {
  //обработка нажатия кнопки
  switch(get_button()){
    case buttonPress:
      Serial.write("Button pressed\r");
    break;

    case buttonNotPress:break;
  }
}

Особенность работы функции »get_button ()» заключается в том, что теперь она обрабатывается не по нажатию, а когда ее отпустили. На самом деле такое часто бывает. К примеру, кнопка блокировки экрана на большинстве смартфонов работает именно так. «Крестик» в верхнем углу вашего браузера тоже закрывает окно, когда вы отпустите кнопку мыши, а ни когда нажмете на нее.

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

Обработка длинного и короткого нажатия кнопки


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

В общем-то, длинное и короткое нажатие кнопок принципиально отличается временем, в течение которого кнопка зажата. И наш алгоритм должен это время учитывать. Обратите внимание на графики. Фактически, наша программа должна различать три интервала времени: время дребезга, время короткого нажатия и время длинного нажатия. Для удобства использования такой кнопки, следует сделать разницу между этими интервалами побольше.

image

Алгоритм работы функции »get_button ()» нужно изменить. Для измерения времени нажатий кнопки я использую цикл while, в котором с периодичностью в 10 мс будет увеличиваться переменная-счетчик.

Дискретность измерения времени нажатия кнопки в 10 мс я считаю оптимально. Это дает достаточную точность измеренного интервала. Если увеличить задержку, то это приведет к дополнительной потере времени в момент отпускания кнопки. Ну, а чаще измерение проводить наверное ни к чему. В общем, я обычно обрабатываю нажатие кнопки с интервалом в 10 мс, и этого всегда было достаточно.

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

image

В тексте программы я допишу необходимые константы для обозначения интервалов времени и добавлю коды, которые будет возвращать функция »get_button ()».

//--------------------------------------------------
//линия, к которой подключена кнопка
#define BUTTON_INPUT  2

//шаг измерения времени нажатия кнопки
#define TIME_STEP 10

//время длинного нажатия кнопки
#define BUTTON_LONG_PRESS_TIME 500

//время короткого нажатия кнопки
#define BUTTON_SHORT_PRESS_TIME 100

//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress,   //код если кнопка не нажата
  buttonShortPress, //код короткого нажатия
  buttonLongPress   //код длинного нажатия
};

Далее перепишу функцию «get_button ()».

//--------------------------------------------------
//обработка кнопки
enum ButtonResult get_button(void){
  //для измерения времени нажатия 
  uint16_t buttonPressTime = 0;

  //проверка нажатия кнопки
  while(digitalRead(BUTTON_INPUT) == LOW){
    //шаг по шкале времени
    delay(TIME_STEP);  
    //считаем время
    buttonPressTime += TIME_STEP;
    //это нужно, чтоб счетчик не переполнился, если кто-то уснет на кнопке
    if(buttonPressTime > BUTTON_LONG_PRESS_TIME)
      buttonPressTime = BUTTON_LONG_PRESS_TIME;
  }

  //проверяем длинное нажатие кнопки
  if(buttonPressTime >= BUTTON_LONG_PRESS_TIME)
    return buttonLongPress;

  //проверяем короткое нажатие кнопки
  if(buttonPressTime >= BUTTON_SHORT_PRESS_TIME)
    return buttonShortPress;

  //сообщаем, что кнопку не нажимали
  return buttonNotPress;
}

Для тестирования полученного кода также перепишу фоновую программу. Теперь она будет выводить в терминал сообщения о длинном или коротком нажатии кнопки.

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настраиваем порт для обработки кнопки
  //вход с подтяжкой к плюсу питания
  pinMode(BUTTON_INPUT, INPUT_PULLUP);

  //настройки для передачи данных в терминал 
  Serial.begin(9600);
}

//--------------------------------------------------
//супер цикл
void loop() {
  //обработка нажатия кнопки
  switch(get_button()){
    case buttonShortPress:
      Serial.write("Button short pressed\r");
    break;

    case buttonLongPress:
      Serial.write("Button long pressed\r");
    break;

    case buttonNotPress:break;
  }
}

Обработка нескольких кнопок


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

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

Для удобства, верхнюю кнопку назовем SB1 (вход 3), а нижнюю — SB2 (вход 2).

image

Как и в предыдущем случае, я допишу макрос для входа кнопки. И подправлю имя предыдущего макроса в соответствии с новыми позиционными обозначениями на схеме. А также верну макрос для задержки времени при защите от дребезга.

//--------------------------------------------------
//линии, к которым подключены кнопки
#define BUTTON_INPUT_SB1  3
#define BUTTON_INPUT_SB2  2

//величина задержки для защиты от дребезга кнопки
#define BUTTON_PROTECTION 50

Далее исправлю перечисление »ButtonResult», добавлю в него коды для нажатий первой и второй кнопки.

//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress,   //если кнопка не нажата
  button_SB1_Press, //если кнопка SB1 нажата
  button_SB2_Press  //если кнопка SB2 нажата
};

Теперь в функции »setup ()» настрою выходы Arduino на ввод с внутренним подтягивающим резистором для обеих кнопок, используя новые макро-имена.

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настраиваем порт для обработки кнопки
  //вход с подтяжкой к плюсу питания
  pinMode(BUTTON_INPUT_SB1, INPUT_PULLUP);
  pinMode(BUTTON_INPUT_SB2, INPUT_PULLUP);

  //настройки для передачи данных в терминал 
  Serial.begin(9600);
}

Самым простым и очевидным для обработки двух кнопок будет продублировать код из первого примера в функции »get_button ()», и подправить в нем новые имена кнопок.

//--------------------------------------------------
//обработка кнопки
enum ButtonResult get_button(void){
  //проверка нажатия кнопки SB1
  if(digitalRead(BUTTON_INPUT_SB1) == LOW){
    //пауза для защиты от дребезга
    delay(BUTTON_PROTECTION);  
    //повторный опрос кнопки
    if(digitalRead(BUTTON_INPUT_SB1) == LOW){
      //ничего не делаем, пока кнопка нажата
      while(digitalRead(BUTTON_INPUT_SB1) == LOW);
      //сообщаем, что кнопка нажата
      return button_SB1_Press;
    }
  }

//проверка нажатия кнопки SB2
  if(digitalRead(BUTTON_INPUT_SB2) == LOW){
    //пауза для защиты от дребезга
    delay(BUTTON_PROTECTION);  
    //повторный опрос кнопки
    if(digitalRead(BUTTON_INPUT_SB2) == LOW){
      //ничего не делаем, пока кнопка нажата
      while(digitalRead(BUTTON_INPUT_SB2) == LOW);
      //сообщаем, что кнопка нажата
      return button_SB2_Press;
    }
  }

  //сообщаем, что кнопку не нажимали
  return buttonNotPress;
}

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

//--------------------------------------------------
//супер цикл
void loop() {
  //обработка нажатия кнопки
  switch(get_button()){
    case button_SB1_Press:
      Serial.write("Button SB1 pressed\r");
    break;

    case button_SB2_Press:
      Serial.write("Button SB2 pressed\r");
    break;

    case buttonNotPress:break;
  }
}

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

Проблема с обработкой одновременного нажатия нескольких кнопок


Но все таки, если вам каким-то магическим образом удастся нажать обе кнопки совершенно синхронно, то их обработка будет выполнена в порядке приоритета, обусловленного порядком выполнения операторов if. Также этот алгоритм не заметит нажатие кнопки SB2, если оно будет менее продолжительным, чем нажатие кнопки SB1.

image

В случае, если кнопки были нажаты одновременно, и нажатие кнопки SB2 происходило дольше, то возникнет неоднозначная ситуация. Обработка кнопки SB1 произойдет корректно при первом вызове функции »get_button ()». А при следующем вызове функции, алгоритм примет «хвост» графика за отдельное нажатие кнопки SB2. И если этот «хвост» окажется короче паузы для защиты от дребезга, то функция не заметит нажатия кнопки SB2. Чтобы функция заметила нажатие кнопки SB2, ее нужно отпустить значительно позже, чем SB1.

image

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

Увеличиваем количество кнопок


А как быть, если нужно еще увеличить количество кнопок? Еще больше дублировать код? Пожалуй, это не самая лучшая идея. Программа станет громоздкой, и ее сложно будет даже читать. Попробуем решить эту задачу. Для этого дополним схему еще двумя кнопками, пусть теперь их будет четыре.

image

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

image

Давайте оптимизируем этот алгоритм с помощью цикла. Счетчик в цикле будет последовательно перебирать номера кнопок. На основе этого номера будем подставлять в алгоритм номера портов и подходящие им идентификаторы. Использование цикла позволило улучшить читаемость алгоритма и значительно сократило его длину за счет исключения дублирующегося кода.

image

Первым делом я дополню перечисление »ButtonResult» кодами для новых кнопок. Эти коды будет возвращать функция »get_button ()».

//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress,   //если кнопка не нажата
  button_SB1_Press, //если кнопка SB1 нажата
  button_SB2_Press, //если кнопка SB2 нажата
  button_SB3_Press, //если кнопка SB3 нажата
  button_SB4_Press  //если кнопка SB4 нажата
};

Для хранения параметров кнопок я создам некое подобие таблицы. Для этого определю структуру »ButtonConfig», и в ее полях будут записаны номера входов и идентификаторы для каждой кнопки.

//для описания параметров кнопки
struct ButtonConfig {
  //номер входа, к которому подключена кнопка
  uint8_t pin;
  //код кнопки при нажатии
  enum ButtonResult identifier;
};

Для удобства обращения к кнопкам, я перечислю их имена в соответствии с позиционными обозначениями на схеме. Это, конечно, не обязательно, но мне так привычнее. Имена »ButtonNamesStart» и »NumberOfButtons» будет удобно использовать для работы в циклах.

//перечисление имен кнопок
enum ButtonNames {
  ButtonNamesStart = 0,
  SB1 = ButtonNamesStart, SB2, SB3, SB4,
  NumberOfButtons
};

Теперь я объявлю массив из структур »ButtonConfig», отдельные элементы которого будут хранить данные по каждой кнопке. При инициализации я явно указываю индексы элементов массива и поля структуры, которые заполняю. Так удобнее, не нужно запоминать последовательность, в которой объявлены поля структуры, и порядок, в котором будут храниться кнопки в массиве. В итоге, инициализация массива структур напоминает обычную таблицу данных, где элементы массива — это строки, а поля таблицы — это столбцы.

//массив для хранения параметров кнопок
struct ButtonConfig button[NumberOfButtons] = {
  [SB1] = {.pin = 4, .identifier = button_SB1_Press},
  [SB2] = {.pin = 5, .identifier = button_SB2_Press},
  [SB3] = {.pin = 3, .identifier = button_SB3_Press},
  [SB4] = {.pin = 2, .identifier = button_SB4_Press}
};

Когда параметры кнопок определены, выполню инициализацию портов в функции »setup ()». Цикл for поочередно выбирает номера портов, к которым подключены кнопки, из массива »button».

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настраиваем порты для обработки кнопок
  //вход с подтяжкой к плюсу питания
  for(uint8_t num = ButtonNamesStart; num < NumberOfButtons; ++num){
    pinMode(button[num].pin, INPUT_PULLUP);
  }

  //настройки для передачи данных в терминал 
  Serial.begin(9600);
}

Остается дописать функцию «get_button».

//--------------------------------------------------
//обработка кнопки
enum ButtonResult get_button(void){
  //поочереди перебираем параметры кнопок  
  for(uint8_t num = ButtonNamesStart; num < NumberOfButtons; ++num){
    //проверка кнопки по текущему индексу
    if(digitalRead(button[num].pin) == LOW){
      //пауза для защиты от дребезга
      delay(BUTTON_PROTECTION);  
      //повторный опрос кнопки
      if(digitalRead(button[num].pin) == LOW){
        //ничего не делаем, пока кнопка нажата
        while(digitalRead(button[num].pin) == LOW);
        //сообщаем, что кнопка нажата
        return button[num].identifier;
      }
    }
  }
  //сообщаем, что кнопку не нажимали
  return buttonNotPress;
}

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

//--------------------------------------------------
//супер цикл
void loop() {
  //обработка нажатия кнопки
  switch(get_button()){
    case button_SB1_Press:
      Serial.write("Button SB1 pressed\r");
    break;

    case button_SB2_Press:
      Serial.write("Button SB2 pressed\r");
    break;

    case button_SB3_Press:
      Serial.write("Button SB3 pressed\r");
    break;

    case button_SB4_Press:
      Serial.write("Button SB4 pressed\r");
    break;

    case buttonNotPress:break;
  }
}

На всякий случай полный текст программы я спрятал под спойлер. Вы можете скопировать его в Arduino IDE и поэкспериментировать с ним.

Полный текст программы
//величина задержки для защиты от дребезга кнопки
#define BUTTON_PROTECTION 50

//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress,   //если кнопка не нажата
  button_SB1_Press, //если кнопка SB1 нажата
  button_SB2_Press, //если кнопка SB2 нажата
  button_SB3_Press, //если кнопка SB3 нажата
  button_SB4_Press  //если кнопка SB4 нажата
};

//--------------------------------------------------
//для описания параметров кнопки
struct ButtonConfig {
  //номер входа, к которому подключена кнопка
  uint8_t pin;
  //код кнопки при нажатии
  enum ButtonResult identifier;
};

//перечисление имен кнопок
enum ButtonNames {
  ButtonNamesStart = 0,
  SB1 = ButtonNamesStart, SB2, SB3, SB4,
  NumberOfButtons
};

//--------------------------------------------------
//массив для хранения параметров кнопок
struct ButtonConfig button[NumberOfButtons] = {
  [SB1] = {.pin = 4, .identifier = button_SB1_Press},
  [SB2] = {.pin = 5, .identifier = button_SB2_Press},
  [SB3] = {.pin = 3, .identifier = button_SB3_Press},
  [SB4] = {.pin = 2, .identifier = button_SB4_Press}
};

//--------------------------------------------------
//обработка кнопки
enum ButtonResult get_button(void){
  //поочереди перебираем параметры кнопок  
  for(uint8_t num = ButtonNamesStart; num < NumberOfButtons; ++num){
    //проверка кнопки по текущему индексу
    if(digitalRead(button[num].pin) == LOW){
      //пауза для защиты от дребезга
      delay(BUTTON_PROTECTION);  
      //повторный опрос кнопки
      if(digitalRead(button[num].pin) == LOW){
        //ничего не делаем, пока кнопка нажата
        while(digitalRead(button[num].pin) == LOW);
        //сообщаем, что кнопка нажата
        return button[num].identifier;
      }
    }
  }
  //сообщаем, что кнопку не нажимали
  return buttonNotPress;
}

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настраиваем порты для обработки кнопок
  //вход с подтяжкой к плюсу питания
  for(uint8_t num = ButtonNamesStart; num < NumberOfButtons; ++num){
    pinMode(button[num].pin, INPUT_PULLUP);
  }

  //настройки для передачи данных в терминал 
  Serial.begin(9600);
  Serial.write("Button test\r");
}

//--------------------------------------------------
//супер цикл
void loop() {
  //обработка нажатия кнопки
  switch(get_button()){
    case button_SB1_Press:
      Serial.write("Button SB1 pressed\r");
    break;

    case button_SB2_Press:
      Serial.write("Button SB2 pressed\r");
    break;

    case button_SB3_Press:
      Serial.write("Button SB3 pressed\r");
    break;

    case button_SB4_Press:
      Serial.write("Button SB4 pressed\r");
    break;

    case buttonNotPress:break;
  }
}

Обработка нескольких кнопок с разной длительностью нажатия


Повторить тот же «финт ушами» с дублированием кода для обработки каждой кнопки, в данном случае не получится. Если использовать цикл while, как в прошлом примере обработки кнопки с разной длительностью нажатия, при нескольких одновременно нажатых кнопках результат работы кода будет крайне неудовлетворительным.

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

image

Моя программа будет отслеживать нажатия любой комбинации кнопок, измерять длительность нажатия каждой кнопки отдельно с дискретным шагом в 10 мс. А затем, в соответствии с заложенным приоритетом по возрастанию номера кнопки, определит, код какой из нажатых кнопок возвращать.

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

//--------------------------------------------------
//шаг измерения времени нажатия кнопки
#define TIME_STEP 10

//время длинного нажатия кнопки
#define BUTTON_LONG_PRESS_TIME 500

//время короткого нажатия кнопки
#define BUTTON_SHORT_PRESS_TIME 100

Количество кнопок в схеме увеличилось, каждая кнопка теперь может иметь два состояния: короткое и длинное нажатие. Чтобы можно было все это в программе различить, дополню перечисление состояний «ButtonResult » новыми кодами.

//--------------------------------------------------
//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress,        //если кнопка не нажата
  button_SB1_shortPress, //код короткого нажатия кнопки SB1
  button_SB1_longPress,  //код длинного нажатия кнопки SB1
  button_SB2_shortPress, //код короткого нажатия кнопки SB2
  button_SB2_longPress,  //код длинного нажатия кнопки SB2
  button_SB3_shortPress, //код короткого нажатия кнопки SB3
  button_SB3_longPress,  //код длинного нажатия кнопки SB3
  button_SB4_shortPress, //код короткого нажатия кнопки SB4
  button_SB4_longPress,  //код длинного нажатия кнопки SB4
  buttonPress            //нажатие любой кнопки
};

Перечисление »ButtonNames» имен кнопок остается без изменений из прошлого примера.

//--------------------------------------------------
//перечисление имен кнопок
enum ButtonNames {
  ButtonNamesStart = 0,
  SB1 = ButtonNamesStart, SB2, SB3, SB4,
  NumberOfButtons
};

А вот структура »ButtonConfig» с описанием параметров кнопок изменилась. В ней добавились поля »shortPressIdentifier» и »longPressIdentifier» для хранения кодов нажатия, поле »result» будет использовано для промежуточного хранения результата нажатия кнопки, поле »pressingTime» будет использовано для измерения времени нажатия кнопки.

//--------------------------------------------------
//для описания параметров кнопки
struct ButtonConfig {
  //номер входа, к которому подключена кнопка
  uint8_t pin;
  //код кнопки при нажатии
  enum ButtonResult shortPressIdentifier;
  enum ButtonResult longPressIdentifier;
  //для промежуточного хранения результата
  enum ButtonResult result;
  //для измерения длительности нажатия
  uint16_t pressingTime;
};

Далее я заполняю массив »button» для каждой кнопки. На этом этапе важно не перепутать номера цифровых портов. И это является главным преимуществом. Если нужно переназначить кнопки, допустим их порядок изменился при трассировке платы, или еще по каким-то причинам, то не нужно лезть глубоко в код, достаточно переставить цифры при инициализации одного массива.

//--------------------------------------------------
//массив для хранения параметров кнопок
struct ButtonConfig button[NumberOfButtons] = {
  [SB1] = {.pin = 4, .shortPressIdentifier = button_SB1_shortPress, .longPressIdentifier = button_SB1_longPress},
  [SB2] = {.pin = 5, .shortPressIdentifier = button_SB2_shortPress, .longPressIdentifier = button_SB2_longPress},
  [SB3] = {.pin = 3, .shortPressIdentifier = button_SB3_shortPress, .longPressIdentifier = button_SB3_longPress},
  [SB4] = {.pin = 2, .shortPressIdentifier = button_SB4_shortPress, .longPressIdentifier = button_SB4_longPress}
};

Следующий код может показаться сложным, но это на первый взгляд. Присмотритесь к нему повнимательнее. В отличие от многих примеров для Arduino, где используют односложные имена типа: x, y, z, a, d, c и тому подобное, я люблю длинные и емкие имена. Мне так проще. Не нужно их запоминать, достаточно помнить к чему относится имя. К примеру, для кнопок все имена начинаются со слова «button», для интервалов времени — со слова «time», и так далее. При наборе кода я пользуюсь функциями автоматического ввода, нажимаю комбинацию клавиш на клавиатуре, потом просто читаю имена в списке и выбираю подходящее. И итоговый код удобно читать, с хорошо подобранными именами он практически не нуждается в комментариях.

Чтобы не мучиться с do-while и не терять лишнюю задержку в 10 мс при каждом вызове обработчика клавиатуры, я разделил код на две функции. Функция »get_button ()», как и прежде, будет основной, ее будем использовать при необходимости опросить кнопки. А функция »checkingButtonStatus ()» будет оценивать состояние кнопок только в текущий момент времени. Функция »get_button ()» будет вызывать функцию »checkingButtonStatus ()» с интервалом в 10 мс, пока хотя бы одна кнопка остается нажатой.

//--------------------------------------------------
//обработка текущего состояния кнопок
enum ButtonResult checkingButtonStatus(void){
  //проверка, была ли нажата хоть одна кнопка
  enum ButtonResult check = buttonNotPress;

  //последовательно обрабатываем все кнопки
  for(uint8_t num = ButtonNamesStart; num < NumberOfButtons; ++num){
    //если кнопка нажата,  
    if(digitalRead(button[num].pin) == LOW){
      //увеличиваем счетчик времени ее нажатия
      button[num].pressingTime += TIME_STEP;
      
      //чтобы счетчик не переполнился, если кнопка залипнет
      if(button[num].pressingTime >= BUTTON_LONG_PRESS_TIME)
        button[num].pressingTime = BUTTON_LONG_PRESS_TIME;
      
      //запоминаем, что кнопка нажималась
      check = buttonPress;
    }
    //если не нажата, проверяем измеренное время
    else{
      //проверяем на длинное нажатие
      if(button[num].pressingTime >= BUTTON_LONG_PRESS_TIME)
        button[num].result = button[num].longPressIdentifier;
      //проверяем короткое нажатие
      else if(button[num].pressingTime >= BUTTON_SHORT_PRESS_TIME)
        button[num].result = button[num].shortPressIdentifier;
      
      //сбрасываем время
      button[num].pressingTime = 0;
    }
  }

  //сообщаем, была ли хоть одна кнопка нажата
  return check;
}

Переменная »check» используется как флаг, показывающий было нажатие хотя бы одной кнопки, или нет. Если при обработки кнопок ни одна не была нажата, то значение переменной останется равным »buttonNotPress», в противном случае в нее будет записано значение »buttonPress».

Цикл »for (uint8_t num = ButtonNamesStart; num < NumberOfButtons; ++num)» поочередно перебирает все кнопки с помощью переменной-счетчика »num».

В теле цикла оператор »if (digitalRead (button[num].pin) == LOW)» проверяет состояние кнопок. И если кнопка нажата, то ее счетчик времени »button[num].pressingTime» увеличивается на величину базового интервала 10 мс.

Если кнопку отпустили, то происходит оценка интервала, накопленного в »button[num].pressingTime». Результат проверки записывается в »button[num].result», чтобы потом вернуть код наиболее приоритетной кнопки. После чего »button[num].pressingTime» сбрасывается для следующего измерения.

После того, как все кнопки проверены, функция »checkingButtonStatus ()» возвращает свой результат:»buttonNotPress» — если нажатий не было, или »buttonPress» — если хотя бы одна кнопка нажата.

Теперь посмотрим, что у меня получилось с функцией »get_button ()».

//обработка нажатия кнопок
enum ButtonResult get_button(void){
  //для хранения кода кнопки
  enum ButtonResult temp = buttonNotPress;

  //пока хоть одна кнопка нажата, измеряем время
  while(checkingButtonStatus() == buttonPress){
    delay(TIME_STEP);
  }

  //проверяем результат обработки
  for(uint8_t num = ButtonNamesStart; num < NumberOfButtons; ++num){
    //если было нажатие кнопки, запоминаем его
    if(button[num].result != buttonNotPress)
      temp = button[num].result;

    //сбрасываем результаты одработки
    button[num].result = buttonNotPress;
    button[num].pressingTime = 0;
  }
  
  //возвращаем код нажатия
  return temp;
}

Переменная »temp» используется для того, чтобы определить наиболее приоритетный код нажатой кнопки при условии, что кнопка была нажата.

Далее, цикл »while (checkingButtonStatus () == buttonPress)» вызывает функцию »checkingButtonStatus ()», пока хотя бы одна кнопка остается нажатой.

Когда не остается ни одной нажатой кнопки, цикл »for (uint8_t num = ButtonNamesStart; num < NumberOfButtons; ++num)» проверяет результат обработки для каждой кнопки. Если кнопка нажата, то ее код помещается в переменную »temp», а содержимое »button[num].result» и »button[num].pressingTime» сбрасываются, чтобы не помешать обработке кнопок в следующий раз.

Таким образом получается, что приоритет обработки кнопки определяется ее порядковым номером. При одновременном нажатии нескольких кнопок, функция »get_button ()» вернет код той кнопки, порядковый номер которой больше остальных.

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

Функция loop () для тестирования нового кода.
//--------------------------------------------------
//супер цикл
void loop() {
  //обработка нажатия кнопки
  switch(get_button()){
    case button_SB1_shortPress:
      Serial.write("Button SB1 short pressed\r");
    break;

    case button_SB2_shortPress:
      Serial.write("Button SB2 short pressed\r");
    break;

    case button_SB3_shortPress:
      Serial.write("Button SB3 short pressed\r");
    break;

    case button_SB4_shortPress:
      Serial.write("Button SB4 short pressed\r");
    break;

    case button_SB1_longPress:
      Serial.write("Button SB1 long pressed\r");
    break;

    case button_SB2_longPress:
      Serial.write("Button SB2 long pressed\r");
    break;

    case button_SB3_longPress:
      Serial.write("Button SB3 long pressed\r");
    break;

    case button_SB4_longPress:
      Serial.write("Button SB4 long pressed\r");
    break;

    case buttonNotPress:break;
  }
}

Если вам интересно протестировать получившуюся программу, можете скопировать ее код целиком.

Полный текст программы
//--------------------------------------------------
//шаг измерения времени нажатия кнопки
#define TIME_STEP 10

//время длинного нажатия кнопки
#define BUTTON_LONG_PRESS_TIME 500
    
            

© Habrahabr.ru