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


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

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

//--------------------------------------------------
//физическое состояние кнопки
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            //нажатие любой кнопки
};

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

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

//--------------------------------------------------
//массив для хранения параметров кнопок
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}
};

//--------------------------------------------------
//обработка текущего состояния кнопок
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;
}

//--------------------------------------------------
//обработка нажатия кнопок
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;
}

//--------------------------------------------------
//настройка периферии микроконтроллера
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_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;
  }
}



Как получить код нескольких одновременно нажатых кнопок


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

Так как в нашем примере используется 4 кнопки, а каждая кнопка может возвращать два состояния: короткое и длинное нажатие. То можно результат обработки такой клавиатуры упаковать в один байт. Каждый бит в этом байте будет обозначать соответствующее состояние для каждой кнопки. Нулевой бит будет кодировать короткое нажатие кнопки SB1. Первый бит — длинное нажатие SB1. Ну и так далее, как показано на рисунке.

image

Чтобы реализовать эту идею, я подправлю перечисление "ButtonResult". Пусть теперь коды кнопок будут соответствовать битовым маскам.

//--------------------------------------------------
//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress        = 0,    //если кнопка не нажата
  button_SB1_shortPress = 1<<0, //код короткого нажатия кнопки SB1
  button_SB1_longPress  = 1<<1, //код длинного нажатия кнопки SB1
  button_SB2_shortPress = 1<<2, //код короткого нажатия кнопки SB2
  button_SB2_longPress  = 1<<3, //код длинного нажатия кнопки SB2
  button_SB3_shortPress = 1<<4, //код короткого нажатия кнопки SB3
  button_SB3_longPress  = 1<<5, //код длинного нажатия кнопки SB3
  button_SB4_shortPress = 1<<6, //код короткого нажатия кнопки SB4
  button_SB4_longPress  = 1<<7, //код длинного нажатия кнопки SB4
  buttonPress           = 0xff  //нажатие любой кнопки
};


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

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


Дальше подправлю одну строчку в теле цикла «for».

//проверяем результат обработки
  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;
  }


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

Предположим, что одновременно были нажаты кнопки SB3 и SB4, и переменная "temp" уже запомнила код короткого нажатия SB3. Тогда в результате логического умножения содержимого переменной «temp» на битовую маску кода короткого нажатия SB4, в переменной "temp" изменится только 6-ой бит. А все остальные биты останутся без изменений.

image

Проверку нажатия кнопок в фоновой программе тоже придется изменить. Больше мне оператор switch не подойдет. Я буду использовать оператор if, в условиях которого буду производить логическое сложение значения, полученного от функции "get_button()" и маски соответствующего кода кнопки. Для короткого нажатия кнопки SB1 код будет выглядеть вот так:

if(temp & button_SB1_shortPress)
      Serial.write("Button SB1 short pressed\r");


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

image

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

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

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

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

//--------------------------------------------------
//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress        = 0,    //если кнопка не нажата
  button_SB1_shortPress = 1<<0, //код короткого нажатия кнопки SB1
  button_SB1_longPress  = 1<<1, //код длинного нажатия кнопки SB1
  button_SB2_shortPress = 1<<2, //код короткого нажатия кнопки SB2
  button_SB2_longPress  = 1<<3, //код длинного нажатия кнопки SB2
  button_SB3_shortPress = 1<<4, //код короткого нажатия кнопки SB3
  button_SB3_longPress  = 1<<5, //код длинного нажатия кнопки SB3
  button_SB4_shortPress = 1<<6, //код короткого нажатия кнопки SB4
  button_SB4_longPress  = 1<<7, //код длинного нажатия кнопки SB4
  buttonPress           = 0xff  //нажатие любой кнопки
};

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

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

//--------------------------------------------------
//массив для хранения параметров кнопок
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}
};

//--------------------------------------------------
//обработка текущего состояния кнопок
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;
}

//обработка нажатия кнопок
uint8_t get_button(void){
  //для хранения кода кнопки
  uint8_t 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;
}

//--------------------------------------------------
//настройка периферии микроконтроллера
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() {
  //обработка нажатия кнопки
  uint8_t temp = get_button();

    if(temp & button_SB1_shortPress)
      Serial.write("Button SB1 short pressed\r");

    if(temp & button_SB2_shortPress)
      Serial.write("Button SB2 short pressed\r");

    if(temp & button_SB3_shortPress)
      Serial.write("Button SB3 short pressed\r");


    if(temp & button_SB4_shortPress)
      Serial.write("Button SB4 short pressed\r");

    if(temp & button_SB1_longPress)
      Serial.write("Button SB1 long pressed\r");

    if(temp & button_SB2_longPress)
      Serial.write("Button SB2 long pressed\r");

    if(temp & button_SB3_longPress)
      Serial.write("Button SB3 long pressed\r");

    if(temp & button_SB4_longPress)
      Serial.write("Button SB4 long pressed\r");
}



Заключение


«Внимательный читатель» может спросить меня: зачем все эти навороты с массивами структур и прочими «кренделями», не проще ли сразу писать классы и использовать прерываний? И это справедливый вопрос.

Отвечу на него так:

1. Прежде чем вникать в прерывания и особенности построения дискретных алгоритмов, все таки стоит прочувствовать на себе все тяготы линейных программ.
2. Классы и в целом объектно-ориентированная парадигма программирования — это достаточно сложно для новичков… и иногда не только для новичков. И, по моему мнению, ООП больше подходит для написание больших проектов на ПК и далее.… хотя, всего каких-то два десятка лет назад некоторые товарищи (стыдливо отвожу взгляд в сторону) ни как не хотели слазить с ассемблера.




Комментарии (41):

  1. ionicman
    /#24971220 / +11

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

    Гораздо правильней делать это с помощью чего-то похожего на конечный автомат.
    И так и делают — одна из самых популярных (но не самых крутых) — это библиотека по работе с кнопками от AlexGyver.

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

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

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

    Это удобней и в разы универсальней.

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

    И самое главное — опрос не мешает выполнятся остальным частям кода.

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

    • OldFashionedEngineer
      /#24971300 / +6

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

    • OldFashionedEngineer
      /#24971310 / +3

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

      • ionicman
        /#24971332 / +1

        Раньше C++ не использовался ввиду того, что давал оверхэд при компиляции для микропррцессоров, сейчас компиляторы его практически не дают - разница может составлять совсем не много около - 0.2-0.5%

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

        Но понятно, что при использовании C++, надо и метрологию его использовать, а не процедуры + вкоряченный класс)

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

        • Деформация и зашоренность сознания сильно ограничивают. К примру, я сам не могу себя заставить на ООП писать под МК. У меня выработались практически "стандартные" для меня подходы, которые позволяют достаточно просто писать код. Я и сам уже анализировал результаты. К примеру, смотрел, как оптимизатор распределяет память для структур, очень не дурно, потери памяти действительно минимальные.

          А что про начинающих говорить? Тут надо с базовым синтаксисом Си совладать, а потом еще ++ изучать. И дополнительно подходы осваивать. Сложно так сходу. Вот многие и останавливаются на Си.

          • ionicman
            /#24971426 / +1

            Ну, надо начать просто - чем больше, тем лучше будет.

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

            Использовать объекты, когда есть что-то повторяющиеся в системе (кнопки, например).

            Использовать ссылки, вместо передачи по значению.

            Ну и возможность выносить класс или набор классов, как отдельный файл с отдельным функционалом - это прямо киллерфича - глобал не загрязняется, а порождая тип, ну например OLED display( PORTA, PIN1) сразу понятно что куда почему + инициализация, если нужна.

            Это вот прямо самое базовое.

            • OldFashionedEngineer
              /#24971448 / +1

              Я когда-то оконные приложения начинал конструировать на WinAPI, меня массивы структур вполне устраивают.

              Когда-то давно вышел пакет WinAVR для микроконтроллеров на основе GCC. Я, на радостях, в мейк файле ткнул ключ G++, и оно тогда заработало. Но вот в отладке оно мне совсем не понравилось. С тех пор как-то скептически относился к классам для микроконтроллеров.

              Хотя понимаю, что для ARM-микроконтроллеров уже самое то, перейти на ООП и использовать RTOS. Но пока задач подходящего масштаба не возникало.

    • OldFashionedEngineer
      /#24971406 / -1

      О "правильности"... а что считать правильно или не правильно? Есть задача, и есть подходящее для этой задачи решение. Зачем все подряд упаковывать в конечные автоматы?!

      • ionicman
        /#24971440 / +2

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

        Задачу можно решить быстро и в лоб, а можно решить хорошо. Блокирующий - это всегда быстро и в лоб, что потом чревато проблемами и переделками.

        Зачем все подряд упаковывать в конечные автоматы?!

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

        • OldFashionedEngineer
          /#24971466 / +2

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

          • ionicman
            /#24971578 / +2

            Но если проект за деньги, то пиши на эти деньги

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

            Переиспользование универсального кода как раз и даёт возможность снизить стоимость разработки.

            • Я с таким проектом сталкивался. Считыватель допиливал лет десять назад под аттракционы. Там ребята вложились, спасибо им, а я чуть пропатчил их проект и тоже заработал.

        • Обратитесь к трудам Анатолия Шалыто. Даже он сам рекомендует использовать конечный автомат там, где это действительно необходимо. Для остального есть switch подходы, можно и с флагами писать, и, в конце концов, писать линейные алгоритмы.

  2. GarryC
    /#24971318 / +2

    Я точно старше 21 года, поэтому "бесконечный цикл" с исходящей из него стрелкой меня несколько удивляет.

    • OldFashionedEngineer
      /#24971328 / +2

      Вас это удивляет, или это затруднительно для понимания?

      Обратитесь к ГОСТ 19.701-90(ИСО 5807-85), там еще и не такое можно найти. В частности "3.2.2.6. Граница цикла". Мы просто привыкли из курса школьной информатики использовать крайне ограниченное количество символов для изображения алгоритмов.

  3. engine9
    /#24971480 / +1

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

    • OldFashionedEngineer
      /#24971500 / +2

      Хороший вопрос, я ждал его! Наберитесь терпения. Я уже готовлю статью, где будет такое решение. Можно будет сделать выводы.

    • Karlson_rwa
      /#24971564 / +1

      Можно, но везде есть своя цена. Проще всего решать дребезг программно.

    • ionicman
      /#24971566 / +2

      Обычный антидребезг, схем много - простейшая кондер 100nf параллельно кнопке.

      • 104u
        /#24973236 / +2

        Можно ещё поставить небольшой резистор от кнопки к кондеру, дабы увеличить её срок службы

      • iShrimp
        /#24973518

        С конденсатором - это уже будет простейший RC-фильтр, временная постоянная которого равна R*C. Зная интервал опроса кнопки t, можно вычислить примерную требуемую ёмкость конденсатора: C = t / R.

    • konstanttin
      /#24972332 / +1

      Есть даже специализированная микросхема MC14490

  4. alexhott
    /#24972036

    Я в комментарии к предыдущей статье пример неблокирующей обработки дребезга приводил.
    Когда кнопок больше 2 и есть лишний аналоговый вход, то проще посадить кнопки все кнопки на одну линию через резисторы, особенно если кнопки вынесены от платы с МК.

    • 104u
      /#24973218 / +1

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

  5. dlinyj
    /#24972224 / +3

    Очень и очень хорошо!

  6. YuryB
    /#24973118 / -2

    https://arduinomaster.ru/datchiki-arduino/ustranenie-drebezg-kontaktov-knopki/

    смотрим "Аппаратный способ подавления дребезга кнопки", всё чем вы занимаетесь это дурота от незнания азов электроники

    • Вы не могли бы подробнее озвучить Ваше мнение?

      • YuryB
        /#24973386 / +1

        ставим 1 конденсатор и решаем проблему дребезга контактов, стандартное решение которому 100 лет. можно даже через один все кнопки пустить. всё остальное скорее велосипеды с рисками по багам или поеданию ресурсов (может конечно и мизерного). если очень хочется то можно всё, но вопрос зачем когда решение это одна деталька на плате?

        • OldFashionedEngineer
          /#24973462 / -1

          А Вы точно читали мою статью? Она разве о дребезге контактов?

        • OldFashionedEngineer
          /#24973540 / +1

          Если бы Вы внимательно прочитали текст, было бы понятно, что статья о программировании кнопки, а не про конденсаторы.

        • sami777
          /#24974690 / +1

          Конденсатор все могут поставить... А без конденсатора слабо?

    • VT100
      /#24973526

      Триггер — Шмитта.

      • sim2q
        /#24973764 / +1

        Для идеалистов - RS-триггер!
        Когда-то начинал в школьные годы с TTL - навесной монтаж, выносная клавиатура на жгуте из МГТФ - работало нормально только с таким решением на микриках :)

      • Mike-M
        /#24974910 / +1

        К слову, входы Arduino/ATmega имеют встроенные триггеры Шмитта.

    • F376
      /#24977978 / +1

      Аппаратное подавление хорошо, но как показал опыт разработки промэлектроники работающей в тяжелых условиях, оно не спасает полностью. Работает пока постоянная времени RC цепочки (элементарного НЧ фильтра, интегратора) больше чем период дребезга у кнопки. Но потом со временем какой-то один из контактов либо повышает сопротивление, либо замерзает, либо загрязняется, либо окисляется, либо намагничивается, либо теряет упругость (даже если контакт - геркон с пятой приёмкой), либо на крыше включается радиостанция и начинает просачивается помеха, либо нажимающий подносит к контактам 50Hz сети. В итоге на вход при том что есть подобранная RC цепочка и ТШ начинает просачиваться сигнал быстрых срабатываний. Контакт вещь такая, не предугадаешь его дребезг, сюрпризы он выкинет обязательно. А уж нажимающего его человека - тем более. Поэтому там где возможно, желательно предусмотреть еще и программную обработку дребезга.

      • Хорошее замечание. Еще, как более частый пример, могу привести энкодеры в автомагнитолах. Там обычно используют RС-цепочки от дребезга. И сколько таких магнитол, у которых со временем энкодер начинает подглючивать?

  7. ibnteo
    /#24973596

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

  8. Vladimir_box
    /#24975000 / +3

    Мегокрутая статья. Даже прошел регистрацию на Хабре, чтобы выразить своё восхищение. Всё крайне подробно. Я не начинающий, но много нового узнал. Теперь жду статью с неблокирующим опросом кнопок. А вообще искал как работать с кнопками через регистры. Нужно ужать код максимально. Ещё раз спасибо)

    • Спасибо за комментарий! Следующая статья к концу недели будет.

  9. OptimumOption
    /#24978476

    Картинку поправьте, у вас там SB5 из воздуха появился :)