Неблокирующая обработка тактовой кнопки для Arduino. Как использовать прерывание таймера «в два клика» в стиле ардуино

в 8:10, , рубрики: arduino, isis proteus, Proteus, timeweb_статьи, байт, Блог компании Timeweb Cloud, гайд, кнопка, код, микроконтроллеры, Программирование, программирование микроконтроллеров, Разработка под Arduino, Си, схема, электроника, Электроника для начинающих
Неблокирующая обработка тактовой кнопки для Arduino. Как использовать прерывание таймера «в два клика» в стиле ардуино - 1

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

Чтобы сохранить общую концепцию, пришлось разобраться с обработкой прерываний таймеров-счетчиков в стиле Arduino. Если вы не сталкивались с этим, и работаете напрямую с регистрами, рекомендую посмотреть, как это выглядит. HAL-драйвер для STM32 в этом плане нервно курит в сторонке.

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

Блокирующие и неблокирующие функции

Прежде, чем приступить к обработке кнопки, имеет смысл разобраться, чем принципиально отличаются блокирующие и не блокирующие функции.

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

image

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

image

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

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

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

Для эксперимента, как и в прошлый раз, я подключу к Arduino одну кнопку, и буду использовать терминал. Кнопка SB1 подключена к цифровому входу 2.

image

Я уже сказал, что неблокирующую функцию обработки кнопки необходимо периодически вызывать. Периодичность вызова функции будет определять интервал времени "TIME_STEP".
Чтобы отличить дребезг контактов от нажатия кнопки, определим минимальное время нажатия "BUTTON_PRESS_TIME".

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

//--------------------------------------------------
//шаг измерения времени нажатия кнопки
#define TIME_STEP 10
//время короткого нажатия кнопки
#define BUTTON_PRESS_TIME 100
//максимально возможное время нажатия
#define MAX_PRESS_DURATION BUTTON_PRESS_TIME

Перечисление "ButtonResult" будет содержать коды, возвращаемые при обработке кнопки. Пока их получилось немного, но дальше добавим еще парочку.

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

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

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

В экземпляре структуры "button" запишу параметры кнопки SB1. Поле "pressingTime" будет использоваться для измерения времени нажатия кнопки, явно сброшу его просто для наглядности.

//--------------------------------------------------
//для хранения параметров кнопки
struct ButtonConfig button = {
  .pin              = 2, 
  .pressIdentifier  = button_SB1_Press, 
  .pressingTime     = 0
};

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

image

Код функции buttonProcessing() под спойлером

//--------------------------------------------------
enum ButtonResult buttonProcessing(uint16_t time){
  //для временного хранения кода нажатия кнопки
  enum ButtonResult temp = buttonNotPress;

  //если кнопка нажата
  if(digitalRead(button.pin) == LOW){
    //считаем время нажатия
    button.pressingTime += time;

    //защита от переполнения
    if(button.pressingTime >= MAX_PRESS_DURATION){
      button.pressingTime = MAX_PRESS_DURATION;
    }
  }
  //если кнопка не нажата
  else{
    //проверяем, сколько времени продолжалось нажатие кнопки
    if(button.pressingTime >= BUTTON_PRESS_TIME){
      temp = button.pressIdentifier;
    }

    //сбрасываем для следующего измерения
    button.pressingTime = 0;
  }

  //возвращаем результат обработки кнопки
  return temp;
}

В функции "setup()", как и полагается для Arduino, произведу настройку периферии контроллера.

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настройка входа для кнопки с подтяжкой
  pinMode(button.pin, INPUT_PULLUP);
  //настройка USART
  Serial.begin(9600);
  //вывод текста
  Serial.println("button test");
}

Фоновая программа в "loop()" будет вызывать функцию "buttonProcessing()" для опроса кнопки. При нажатии кнопки в терминал будет выводиться соответствующее сообщение: «кнопка нажата».

//--------------------------------------------------
//супер цикл
void loop() {
  //для временного хранения кода нажатия кнопки
  enum ButtonResult tempButtonPress;
  
  //опрос кнопки
  tempButtonPress = buttonProcessing(TIME_STEP);

  //обрабатываем результат нажатия кнопки
  if(tempButtonPress == button_SB1_Press){
    Serial.println("button pressed");
  }

  //формируем базовый интервал времени
  delay(TIME_STEP);
}

Так как фоновая программа и обработка кнопки вместе занимают совсем немного машинного времени, интервал между опросами кнопки можно формировать с помощью функции "delay(TIME_STEP)". При решении прикладных задач конечно же лучше использовать функцию "millis()". Но пока не будем заморачиваться.

Кому неохота собирать текст программы по кусочкам, под спойлером привожу программу целиком.

Полный текст программы

//--------------------------------------------------
//шаг измерения времени нажатия кнопки
#define TIME_STEP 10
//время короткого нажатия кнопки
#define BUTTON_PRESS_TIME 100
//максимально возможное время нажатия
#define MAX_PRESS_DURATION BUTTON_PRESS_TIME

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

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

//--------------------------------------------------
//для хранения параметров кнопки
struct ButtonConfig button = {
  .pin              = 2, 
  .pressIdentifier  = button_SB1_Press, 
  .pressingTime     = 0
};

//--------------------------------------------------
enum ButtonResult buttonProcessing(uint16_t time){
  //для временного хранения кода нажатия кнопки
  enum ButtonResult temp = buttonNotPress;

  //если кнопка нажата
  if(digitalRead(button.pin) == LOW){
    //считаем время нажатия
    button.pressingTime += time;

    //защита от переполнения
    if(button.pressingTime >= MAX_PRESS_DURATION){
      button.pressingTime = MAX_PRESS_DURATION;
    }
  }
  //если кнопка не нажата
  else{
    //проверяем, сколько времени продолжалось нажатие кнопки
    if(button.pressingTime >= BUTTON_PRESS_TIME){
      temp = button.pressIdentifier;
    }

    //сбрасываем для следующего измерения
    button.pressingTime = 0;
  }

  //возвращаем результат обработки кнопки
  return temp;
}

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настройка входа для кнопки с подтяжкой
  pinMode(button.pin, INPUT_PULLUP);
  //настройка USART
  Serial.begin(9600);
  //вывод текста
  Serial.println("button test");
}

//--------------------------------------------------
//супер цикл
void loop() {
  //для временного хранения кода нажатия кнопки
  enum ButtonResult tempButtonPress;
  
  //опрос кнопки
  tempButtonPress = buttonProcessing(TIME_STEP);

  //обрабатываем результат нажатия кнопки
  if(tempButtonPress == button_SB1_Press){
    Serial.println("button pressed");
  }

  //формируем базовый интервал времени
  delay(TIME_STEP);
}

Короткое и длинное нажатие

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

Дополню программу необходимыми интервалами времени.

image

//--------------------------------------------------
//шаг измерения времени нажатия кнопки
#define TIME_STEP 10
//время дребезга кнопки
#define BUTTON_PRESS_TIME 50
//время короткого нажатия
#define BUTTON_SHORT_PRESS_TIME 100
//время длинного нажатия
#define BUTTON_LONG_PRESS_TIME 1000
//максимально возможное время нажатия
#define MAX_PRESS_DURATION BUTTON_LONG_PRESS_TIME

Список "ButtonResult" тоже дополним кодами, которые будут информировать программу о коротком и длинном нажатии. Код "button_SB1_Press" будет возвращаться сразу после нажатия, как только наш алгоритм убедиться, что интервал времени превышает дребезг. Остальные коды будут возвращаться после того, как кнопку отпустят.

//--------------------------------------------------
//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress,       //если кнопка не нажата
  button_SB1_Press,     //код нажатия кнопки SB1
  button_SB1_shortPress,//код короткого нажатия
  button_SB1_longPress  //код длинной нажатия
};

Также, из-за расширения функциональности программы, я добавлю поля в структуру "ButtonConfiguration".

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

Поле "clickFlag" представляет собой флаг, с помощью которого будет исключена многократная повторная отправка кода "button_SB1_Press" по нажатию кнопки.

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

struct ButtonConfiguration {
  //код кнопки при нажатии
  enum ButtonResult pressIdentifier;
  //код кнопки при нажатии
  enum ButtonResult pressIdentifierShort;
  //код кнопки при нажатии
  enum ButtonResult pressIdentifierLong;
  //номер входа, к которому подключена кнопка
  uint8_t pin;
  //флаг первого срабатывания кнопки
  bool clickFlag;
  //для измерения длительности нажатия
  uint16_t pressingTime;
};
//--------------------------------------------------
//для хранения параметров кнопки
struct ButtonConfiguration button = {
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = 2,
  .clickFlag            = false,
  .pressingTime         = 0
};

Текст функции "buttonProcessing()" изменился не сильно. В нем добавлены проверки измеренного интервала времени.

image

Полный текст функции buttonProcessing

//--------------------------------------------------
enum ButtonResult buttonProcessing(uint16_t time){
  //для временного хранения кода нажатия кнопки
  enum ButtonResult temp = buttonNotPress;

  //если кнопка нажата
  if(digitalRead(button.pin) == LOW){
    //считаем время нажатия
    button.pressingTime += time;

    //защита от переполнения
    if(button.pressingTime >= MAX_PRESS_DURATION){
      button.pressingTime = MAX_PRESS_DURATION;
    }
    
    //проверка дребезга
    if(button.pressingTime >= BUTTON_PRESS_TIME && button.clickFlag == false){
      temp = button.pressIdentifier;
      button.clickFlag = true;
    }
  }
  //если кнопка не нажата
  else{
    //проверяем, сколько времени продолжалось нажатие кнопки
    if(button.pressingTime >= BUTTON_LONG_PRESS_TIME){
      temp = button.pressIdentifierLong;
    }
    else if(button.pressingTime >= BUTTON_SHORT_PRESS_TIME){
      temp = button.pressIdentifierShort;
    }

    //сбрасываем для следующего измерения
    button.pressingTime = 0;
    button.clickFlag = false;
  }

  //возвращаем результат обработки кнопки
  return temp;
}

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

//обрабатываем результат нажатия кнопки
  if(tempButtonPress == button_SB1_Press){
    Serial.println("button pressed");
  }

  if(tempButtonPress == button_SB1_longPress){
    Serial.println("button long pressed");
  }

  if(tempButtonPress == button_SB1_shortPress){
    Serial.println("button short pressed");
  }

И как всегда, полный проект тут.

//--------------------------------------------------
//шаг измерения времени нажатия кнопки
#define TIME_STEP 10
//время дребезга кнопки
#define BUTTON_PRESS_TIME 50
//время короткого нажатия
#define BUTTON_SHORT_PRESS_TIME 100
//время длинного нажатия
#define BUTTON_LONG_PRESS_TIME 1000
//максимально возможное время нажатия
#define MAX_PRESS_DURATION BUTTON_LONG_PRESS_TIME

//--------------------------------------------------
//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress,       //если кнопка не нажата
  button_SB1_Press,     //код нажатия кнопки SB1
  button_SB1_shortPress,//код короткого нажатия
  button_SB1_longPress  //код длинного нажатия
};

struct ButtonConfiguration {
  //код кнопки при нажатии
  enum ButtonResult pressIdentifier;
  //код кнопки при нажатии
  enum ButtonResult pressIdentifierShort;
  //код кнопки при нажатии
  enum ButtonResult pressIdentifierLong;
  //номер входа, к которому подключена кнопка
  uint8_t pin;
  //флаг первого срабатывания кнопки
  bool clickFlag;
  //для измерения длительности нажатия
  uint16_t pressingTime;
};
//--------------------------------------------------
//для хранения параметров кнопки
struct ButtonConfiguration button = {
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = 2,
  .clickFlag            = false,
  .pressingTime         = 0
};

//--------------------------------------------------
enum ButtonResult buttonProcessing(uint16_t time){
  //для временного хранения кода нажатия кнопки
  enum ButtonResult temp = buttonNotPress;

  //если кнопка нажата
  if(digitalRead(button.pin) == LOW){
    //считаем время нажатия
    button.pressingTime += time;

    //защита от переполнения
    if(button.pressingTime >= MAX_PRESS_DURATION){
      button.pressingTime = MAX_PRESS_DURATION;
    }
    
    //проверка дребезга
    if(button.pressingTime >= BUTTON_PRESS_TIME && button.clickFlag == false){
      temp = button.pressIdentifier;
      button.clickFlag = true;
    }
  }
  //если кнопка не нажата
  else{
    //проверяем, сколько времени продолжалось нажатие кнопки
    if(button.pressingTime >= BUTTON_LONG_PRESS_TIME){
      temp = button.pressIdentifierLong;
    }
    else if(button.pressingTime >= BUTTON_SHORT_PRESS_TIME){
      temp = button.pressIdentifierShort;
    }

    //сбрасываем для следующего измерения
    button.pressingTime = 0;
    button.clickFlag = false;
  }

  //возвращаем результат обработки кнопки
  return temp;
}

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настройка входа для кнопки с подтяжкой
  pinMode(button.pin, INPUT_PULLUP);
  //настройка USART
  Serial.begin(9600);
  //вывод текста
  Serial.println("button test");
}

//--------------------------------------------------
//супер цикл
void loop() {
  //для временного хранения кода нажатия кнопки
  enum ButtonResult tempButtonPress;
  
  //опрос кнопки
  tempButtonPress = buttonProcessing(TIME_STEP);

  //обрабатываем результат нажатия кнопки
  if(tempButtonPress == button_SB1_Press){
    Serial.println("button pressed");
  }

  if(tempButtonPress == button_SB1_longPress){
    Serial.println("button long pressed");
  }

  if(tempButtonPress == button_SB1_shortPress){
    Serial.println("button short pressed");
  }

  //формируем базовый интервал времени
  delay(TIME_STEP);
}

Пишем библиотеку для Arduino в стиле Си

С трепетом вспоминаю, как еще каких-то два десятка лет приходилось работать с COM-портами компьютера на WinAPI. Да чего греха таить, и окна бывало строили. Подключаешь «windows.h», и погнали… А библиотека «winsock.h», которую практически полностью повторили в CodeSYS для промышленных контроллеров… На современных компьютеров уже и COM-портов не встретишь, а привычка писать библиотеки в стиле Си осталась. И вроде понимаешь, что частенько класс написать было бы удобнее, но все равно рука не поднимается это сделать.

Пока наша программа основательно не растолстела, и ее можно прочитать целиком за один раз, самое время вынести обработчик кнопки в отдельную библиотеку. Для этого в папке со скетчем создам два файла: "myNonblockingButton.h" и "myNonblockingButton.cpp". Как только файлы появятся в папке с проектом, они сразу отобразятся на вкладках в Arduino IDE.

Обратите внимание, что расширение второго файла должно быть именно *.cpp. Почему-то Arduino IDE не очень-то жалует файлы с расширением просто *.c.

Заголовочный файл "myNonblockingButton.h" будет представлять собой интерфейс для использования нашей библиотеки. Его необходимо сразу подключить в основной текст программы до основного кода. При подключении файла использую двойные кавычки, т.к. он находится в одной папке с проектом, а не в системном каталоге Arduino IDE.

//--------------------------------------------------
//библиотека для обработки кнопки
#include "myNonblockingButton.h"

В файле "myNonblockingButton.h" обязательно нужно разместить вот такую конструкцию:

#ifndef __myNonblockingButton_h
#define __myNonblockingButton_h

#endif

Это необходимо, чтобы исключить дублирование кода в случае, если файл подключается в нескольких местах нашей программы. А подключать мы его еще раз обязательно будем в файл "myNonblockingButton.cpp". Это нужно будет сделать для расширения области видимости имен, которые будут определены в заголовочном файле.

Теперь перенесу в "myNonblockingButton.h" объявления перечисления "ButtonResult" и структуры "ButtonConfiguration". Определение экземпляра структуры пока оставим на своем месте и не будем переносить. Тем более, что в заголовочных файлах нельзя объявлять ни каких переменных. Да и в принципе, скрывать определение параметров кнопки где-то в недрах библиотек — не лучшая идея.

Следующим этапом я перенесу определение функции "buttonProcessing()" в файл "myNonblockingButton.cpp". И вот тут возникнет две проблемы, из-за которых код придется немного доработать.

Если функцию "buttonProcessing()" перенести в другой файл, то область видимости ее имени более не будет доступна для основного файла программы. Чтобы расширить ее область видимости, в заголовочном файле нашей библиотеки "myNonblockingButton.h" сразу после макроопределений объявим прототип функции.

//--------------------------------------------------
//обработчик кнопки
enum ButtonResult buttonProcessing(uint16_t time);

И теперь возникает следующая проблема. Область видимости структуры "button", которая объявлена в основном файле, теперь недоступна для файла myNonblockingButton.cpp".

Самым правильным, на мой взгляд, в данном случае считаю, передать указатель на структуру с параметрами кнопки в функцию "buttonProcessing()". Именно по этому на меня нахлынули воспоминания про WinAPI.

Добавим еще один входной параметр для функции "buttonProcessing()". Важно, чтобы ее интерфейс был одинаковым для прототипа в *.h — файле и для определения в *.cpp.

enum ButtonResult buttonProcessing(struct ButtonConfiguration* button, uint16_t time);

И, так как, теперь имя "button" внутри тела функции "buttonProcessing()" является указателем, следует обращаться к полям структуры с параметрами кнопки не через оператор прямого доступа ".", а через оператор косвенного доступа "->". Произведу необходимую замену.

Функцию со всеми необходимыми изменениями можно посмотреть тут.

//--------------------------------------------------
//обработчик кнопки
enum ButtonResult buttonProcessing(struct ButtonConfiguration* button, uint16_t time){
  //для временного хранения кода нажатия кнопки
  enum ButtonResult temp = buttonNotPress;

  //если кнопка нажата
  if(digitalRead(button->pin) == LOW){
    //считаем время нажатия
    button->pressingTime += time;

    //защита от переполнения
    if(button->pressingTime >= MAX_PRESS_DURATION){
      button->pressingTime = MAX_PRESS_DURATION;
    }
    
    //проверка дребезга
    if(button->pressingTime >= BUTTON_PRESS_TIME && button->clickFlag == false){
      temp = button->pressIdentifier;
      button->clickFlag = true;
    }
  }
  //если кнопка не нажата
  else{
    //проверяем, сколько времени продолжалось нажатие кнопки
    if(button->pressingTime >= BUTTON_LONG_PRESS_TIME){
      temp = button->pressIdentifierLong;
    }
    else if(button->pressingTime >= BUTTON_SHORT_PRESS_TIME){
      temp = button->pressIdentifierShort;
    }

    //сбрасываем для следующего измерения
    button->pressingTime = 0;
    button->clickFlag = false;
  }

  //возвращаем результат обработки кнопки
  return temp;
}

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

void loop() {
  //для временного хранения кода нажатия кнопки
  enum ButtonResult tempButtonPress;
  
  //опрос кнопки
  tempButtonPress = buttonProcessing(&button,TIME_STEP);

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

//--------------------------------------------------
//настройка входа для кнопки
void buttonInit(struct ButtonConfiguration* button);

Функция "buttonInit()" будет принимать указатель на структуру типа "ButtonConfiguration" для того, чтобы получить из нее номер цифрового порта, к которому подключена кнопка. Определение функции "buttonInit()" напишу в "myNonblockingButton.cpp".

//--------------------------------------------------
//настройка входа для кнопки с подтяжкой
void buttonInit(struct ButtonConfiguration* button){
  pinMode(button->pin, INPUT_PULLUP);
}

Воспользуюсь этой функцией для настройки кнопки. Для этого размещу ее вызов в "setup()". В качестве входного параметра передам адрес структуры "button".

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настройка входа для кнопки с подтяжкой
  buttonInit(&button);
  //настройка USART
  Serial.begin(9600);
  //вывод текста
  Serial.println("button test");
}

Ну вот, основной текст нашей программы изрядно похудел. Но состоит она теперь из трех файлов:
image

1. "countButtonPress.ino" —

посмотреть содержимое файла

//--------------------------------------------------
//библиотека для обработки кнопки
#include "myNonblockingButton.h"

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

//--------------------------------------------------
//для хранения параметров кнопки
struct ButtonConfiguration button = {
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = 2,
  .clickFlag            = false,
  .pressingTime         = 0
};

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настройка входа для кнопки с подтяжкой
  buttonInit(&button);
  //настройка USART
  Serial.begin(9600);
  //вывод текста
  Serial.println("button test");
}

//--------------------------------------------------
//супер цикл
void loop() {
  //для временного хранения кода нажатия кнопки
  enum ButtonResult tempButtonPress;
  
  //опрос кнопки
  tempButtonPress = buttonProcessing(&button,TIME_STEP);

  //обрабатываем результат нажатия кнопки
  if(tempButtonPress == button_SB1_Press){
    Serial.println("button pressed");
  }

  if(tempButtonPress == button_SB1_longPress){
    Serial.println("button long pressed");
  }

  if(tempButtonPress == button_SB1_shortPress){
    Serial.println("button short pressed");
  }

  //формируем базовый интервал времени
  delay(TIME_STEP);
}

2. "myNonblockingButton.h" —

посмотреть содержимое файла

#ifndef __myNonblockingButton_h
#define __myNonblockingButton_h

//--------------------------------------------------
//время дребезга кнопки
#define BUTTON_PRESS_TIME 50
//время короткого нажатия
#define BUTTON_SHORT_PRESS_TIME 100
//время длинного нажатия
#define BUTTON_LONG_PRESS_TIME 1000
//максимально возможное время нажатия
#define MAX_PRESS_DURATION BUTTON_LONG_PRESS_TIME

//--------------------------------------------------
//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress,       //если кнопка не нажата
  button_SB1_Press,     //код нажатия кнопки SB1
  button_SB1_shortPress,//код короткого нажатия
  button_SB1_longPress  //код длинног нажатия
};

struct ButtonConfiguration {
  //код кнопки при нажатии
  enum ButtonResult pressIdentifier;
  //код кнопки при нажатии
  enum ButtonResult pressIdentifierShort;
  //код кнопки при нажатии
  enum ButtonResult pressIdentifierLong;
  //номер входа, к которому подключена кнопка
  uint8_t pin;
  //флаг первого срабатывания кнопки
  bool clickFlag;
  //для измерения длительности нажатия
  uint16_t pressingTime;
};

//--------------------------------------------------
//настройка входа для кнопки
void buttonInit(struct ButtonConfiguration* button);
//обработчик кнопки
enum ButtonResult buttonProcessing(struct ButtonConfiguration* button, uint16_t time);

#endif

3. "myNonblockingButton.cpp" —

посмотреть содержимое файла

//--------------------------------------------------
//потому что надо
#include <Arduino.h>
//подключение библиотеки с нашей кнопкой
#include "myNonblockingButton.h"

//--------------------------------------------------
//настройка входа для кнопки с подтяжкой
void buttonInit(struct ButtonConfiguration* button){
  pinMode(button->pin, INPUT_PULLUP);
}

//--------------------------------------------------
//обработчик кнопки
enum ButtonResult buttonProcessing(struct ButtonConfiguration* button, uint16_t time){
  //для временного хранения кода нажатия кнопки
  enum ButtonResult temp = buttonNotPress;

  //если кнопка нажата
  if(digitalRead(button->pin) == LOW){
    //считаем время нажатия
    button->pressingTime += time;

    //защита от переполнения
    if(button->pressingTime >= MAX_PRESS_DURATION){
      button->pressingTime = MAX_PRESS_DURATION;
    }
    
    //проверка дребезга
    if(button->pressingTime >= BUTTON_PRESS_TIME && button->clickFlag == false){
      temp = button->pressIdentifier;
      button->clickFlag = true;
    }
  }
  //если кнопка не нажата
  else{
    //проверяем, сколько времени продолжалось нажатие кнопки
    if(button->pressingTime >= BUTTON_LONG_PRESS_TIME){
      temp = button->pressIdentifierLong;
    }
    else if(button->pressingTime >= BUTTON_SHORT_PRESS_TIME){
      temp = button->pressIdentifierShort;
    }

    //сбрасываем для следующего измерения
    button->pressingTime = 0;
    button->clickFlag = false;
  }

  //возвращаем результат обработки кнопки
  return temp;
}
}

Синхронная и асинхронная обработка кнопки

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

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

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

Чтобы использовать прерывание от таймера и не выходить за пределы концепции программирования Arduino, пришлось сильно заморочиться. Как по мне, проще разобраться с регистрами AVR-микроконтроллера и настроить таймер на прерывание, чем вникать в библиотеки Arduino.

Я давно не сталкивался с официальным сайтом arduino.cc, и даже расстроился, когда посетил его спустя много лет. Есть ощущение, что систему сильно навязчиво коммерциализировали. Но может мне просто показалось.

В общем, на оф. сайте для работы с таймерами рекомендуют специальную библиотеку "TimerInterrupt.h". Статей по этой библиотеке в русскоязычном сегменте интернета я не нашел. Либо я плохо искал, либо народу просто неохота с этим заморачиваться.

Библиотека "TimerInterrupt.h" написана в стиле С++. Ее можно скачать с помощью встроенного в Arduino IDE менеджера библиотек. Версия библиотеки также зависит от типа платформы. Мне нужна библиотека для Arduino UNO TimmerInterrupt by Khoi Hoang.

image

Статья почему-то опять разрослась, поэтому подробно на этой библиотеке останавливаться не буду. Если вам будет это интересна, я напишу отдельный пост про "TimerInterrupt.h".

Чтобы начать работу с таймерами, добавлю в начало программы несколько макроопределений и подключу библиотеку. Макро имя "USE_TIMER_2 true" необходимо, чтобы использовать таймер-счетчик Т2. Константа "TIMER_INTERVAL_MS" будет определять интервал времени между вызовами обработчика прерывания.

//--------------------------------------------------
//используем таймер Т2
#define USE_TIMER_2 true

//библиотека для работы с прерыванием таймера
#include "TimerInterrupt.h"

//интервал прерываний таймера
#define TIMER_INTERVAL_MS    10

При выборе таймера нужно быть осторожнее, т. к. они используются для реализации некоторых стандартных для Arduino библиотек. Например, таймер Т0 используется для стандартных функций работы со временем, таймер Т1 может быть использован для работы с сервоприводами, и так далее. Т. е. сперва необходимо точно понять, какие таймеры у вас остаются свободными.

Далее, перед функцией "setup()" я объявлю функцию, которая будет связана с обработкой прерывания. По большому счету, имя этой функции может быть любым. Но я воспользуюсь именем из примера в библиотеке "TimerHandler()".

//--------------------------------------------------
//функция для обработки прерывания таймера Т2
void TimerHandler(void)
{
}

Для запуска таймера в функции "setup()" необходимо разместить вызов двух методов объекта "ITimer". Методу "attachInterruptInterval" необходимо передать интервал времени "TIMER_INTERVAL_MS", через который будет вызываться прерывание, а также указатель на функцию "TimerHandler", которая будет обрабатывать это прерывание.

//настройка таймера
  ITimer2.init();
  ITimer2.attachInterruptInterval(TIMER_INTERVAL_MS, TimerHandler);

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

После того, как функция "TimerHandler" связана с прерыванием таймера, можно перенести в нее обработку кнопки из функции "loop()". Из функции "loop()" весь код удалю.

//--------------------------------------------------
//функция для обработки прерывания таймера Т2
void TimerHandler(void)
{ 
  //для временного хранения кода нажатия кнопки
  enum ButtonResult tempButtonPress = buttonProcessing(&button, TIMER_INTERVAL_MS);

  //обрабатываем результат нажатия кнопки
  if(tempButtonPress == button_SB1_Press){
    Serial.println("button pressed");
  }

  if(tempButtonPress == button_SB1_longPress){
    Serial.println("button long pressed");
  }

  if(tempButtonPress == button_SB1_shortPress){
    Serial.println("button short pressed");
  }
}

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

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

Наша библиотека для обработки кнопки осталась без изменений.

А полный текст программы с последними изменениями под спойлером.

//--------------------------------------------------
//используем таймер Т2
#define USE_TIMER_2 true

//библиотека для работы с прерыванием таймера
#include «TimerInterrupt.h»

//интервал прерываний таймера
#define TIMER_INTERVAL_MS 10

//--------------------------------------------------
//библиотека для обработки кнопки
#include «myNonblockingButton.h»

//--------------------------------------------------
//для хранения параметров кнопки
struct ButtonConfiguration button = {
.pressIdentifier = button_SB1_Press,
.pressIdentifierShort = button_SB1_shortPress,
.pressIdentifierLong = button_SB1_longPress,
.pin = 2,
.clickFlag = false,
.pressingTime = 0
};

//--------------------------------------------------
//функция для обработки прерывания таймера Т2
void TimerHandler(void)
{
//для временного хранения кода нажатия кнопки
enum ButtonResult tempButtonPress = buttonProcessing(&button, TIMER_INTERVAL_MS);

//обрабатываем результат нажатия кнопки
if(tempButtonPress == button_SB1_Press){
Serial.println(«button pressed»);
}

if(tempButtonPress == button_SB1_longPress){
Serial.println(«button long pressed»);
}

if(tempButtonPress == button_SB1_shortPress){
Serial.println(«button short pressed»);
}
}

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
//настройка входа для кнопки с подтяжкой
buttonInit(&button);
//настройка USART
Serial.begin(9600);
//вывод текста
Serial.println(«button test in interrupt»);

//настройка таймера
ITimer2.init();
ITimer2.attachInterruptInterval(TIMER_INTERVAL_MS, TimerHandler);
}

//--------------------------------------------------
//супер цикл
void loop() {

}

Пруфы

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

К такому случаю у меня даже нашлась оригинальная ардуинка. Для тестирования решил воспроизвести самые худшие условия. Вместо кнопки использую самую дешевую перемычку папа-папа с алиэкспресса, она скорее всего даже меди не содержит. Перемычку замыкаю на корпус разъема USB.

image

Подключаем несколько кнопок

И в качестве примера использования библиотеки, подключу еще несколько кнопок. Кнопка SB1 останется подключенной ко 2-ому цифровому входу. Добавлю кнопку SB2 на 3-й вход и SB3 на 4-й. Программу продолжу в предыдущем проекте.

image

Объявлю перечисление "ButtonName", в котором будут перечислены имена кнопок. Для удобства работы с циклами добавлю значения "button_start" и "numberOfButtons". Также значение "numberOfButtons" будет удобно для объявления массива с параметрами кнопок. Если в перечисление в дальнейшем будут добавлены новые имена кнопок, то "numberOfButtons" автоматически будет увеличиваться, за счет чего можно написать код, который не нужно будет редактировать.

//--------------------------------------------------
//имена кнопок
enum ButtonName {
  button_start = 0,
  button_SB1 = button_start,
  button_SB2,
  button_SB3,
  numberOfButtons
};

В этот раз экземпляр структуры "ButtonConfiguration" нужно сделать массивом "button[]", который будет хранить параметры всех кнопок. Размер массива будет определяться элементом перечисления "numberOfButtons". Обратите внимание, что я не стал добавлять коды, возвращаемые кнопками при нажатии. Но если вам это необходимо, можно сделать их разными. К примеру, можно возвращать ASCII коды для каждого состояния кнопки. Тут кому что больше нравится, и какие задачи вы перед собой ставите.

//--------------------------------------------------
//для хранения параметров кнопки
struct ButtonConfiguration button[numberOfButtons] = {
[button_SB1] = { 
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = 2,
  .clickFlag            = false,
  .pressingTime         = 0
  },
[button_SB2] = { 
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = 3,
  .clickFlag            = false,
  .pressingTime         = 0
  },
[button_SB3] = { 
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = 4,
  .clickFlag            = false,
  .pressingTime         = 0
  },
};

В функции-обработчике прерывания от таймера добавлю цикл for. В этом цикле будут обрабатываться кнопки. Каждая в свою очередь, по номеру, определяемому счетчиком цикла "num".

//--------------------------------------------------
//функция для обработки прерывания таймера Т2
void TimerHandler(void)
{ 
  for(uint8_t num = button_start; num < numberOfButtons; num++){
    //для временного хранения кода нажатия кнопки
    enum ButtonResult tempButtonPress = buttonProcessing(&button[num], TIMER_INTERVAL_MS);

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

    //обрабатываем результат нажатия кнопки
    if(tempButtonPress == button_SB1_Press){
      Serial.print("pressing the SB");
      Serial.print(num + 1);
      Serial.println(" button");
    }

    if(tempButtonPress == button_SB1_longPress){
      Serial.print("long press button SB");
      Serial.println(num + 1);
    }

    if(tempButtonPress == button_SB1_shortPress){
      Serial.print("short press button SB");
      Serial.println(num + 1);
    }
  }
}

Остается поправить инициализацию кнопки в начале функции "setup()".

    //--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настройка входа для кнопки с подтяжкой
  for(uint8_t num = button_start; num < numberOfButtons; num++)
    buttonInit(&button[num]);

В этот раз тоже обошлось без изменений в самих файлах библиотек, собственно, на то они и библиотеки. Полный текст программы можно посмотреть тут.

посмотреть содержимое файла

//--------------------------------------------------
//используем таймер Т2
#define USE_TIMER_2 true

//библиотека для работы с прерыванием таймера
#include "TimerInterrupt.h"

//интервал прерываний таймера
#define TIMER_INTERVAL_MS    10

//--------------------------------------------------
//библиотека для обработки кнопки
#include "myNonblockingButton.h"

//--------------------------------------------------
//имена кнопок
enum ButtonName {
  button_start = 0,
  button_SB1 = button_start,
  button_SB2,
  button_SB3,
  numberOfButtons
};

//--------------------------------------------------
//для хранения параметров кнопки
struct ButtonConfiguration button[numberOfButtons] = {
[button_SB1] = { 
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = 2,
  .clickFlag            = false,
  .pressingTime         = 0
  },
[button_SB2] = { 
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = 3,
  .clickFlag            = false,
  .pressingTime         = 0
  },
[button_SB3] = { 
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = 4,
  .clickFlag            = false,
  .pressingTime         = 0
  },
};

//--------------------------------------------------
//функция для обработки прерывания таймера Т2
void TimerHandler(void)
{ 
  for(uint8_t num = button_start; num < numberOfButtons; num++){
    //для временного хранения кода нажатия кнопки
    enum ButtonResult tempButtonPress = buttonProcessing(&button[num], TIMER_INTERVAL_MS);
     
    //обрабатываем результат нажатия кнопки
    if(tempButtonPress == button_SB1_Press){
      Serial.print("pressing the SB");
      Serial.print(num + 1);
      Serial.println(" button");
    }

    if(tempButtonPress == button_SB1_longPress){
      Serial.print("long press button SB");
      Serial.println(num + 1);
    }

    if(tempButtonPress == button_SB1_shortPress){
      Serial.print("short press button SB");
      Serial.println(num + 1);
    }
  }
}

//--------------------------------------------------
//настройка периферии микроконтроллера
void setup() {
  //настройка входа для кнопки с подтяжкой
  for(uint8_t num = button_start; num < numberOfButtons; num++)
    buttonInit(&button[num]);
  //настройка USART
  Serial.begin(9600);
  //вывод текста
  Serial.println("button test in interrupt");

  //настройка таймера
  ITimer2.init();
  ITimer2.attachInterruptInterval(TIMER_INTERVAL_MS, TimerHandler);
}

//--------------------------------------------------
//супер цикл
void loop() {

}

Заключение

При увеличении количества кнопок, данный алгоритм обработки будет не самым эффективным. Если многократный вызов функций "buttonInit()" для настройки кнопок можно стерпеть из-за того, что это происходит один раз при старте контроллера и просто немного отодвинет начало выполнения основной программы. А с учетом того, что Arduino стартует с загрузчика, этим вообще можно пренебречь. То многократный вызов функции "buttonProcessing()" может стать проблемой, особенно при обработке кнопок по прерыванию, т.к. увеличивать время работы функций-обработчиков прерываний не желательно.

Алгоритм обработки кнопки можно было бы оптимизировать. Но я считаю это нецелесообразным. Подключать больше 6-ти кнопок параллельно к портам контроллера нет смысла. Большее количество кнопок уже можно объединять в матрицы. А для обработки матрицы нужно писать свой алгоритм.

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

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

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

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

Если вам понравился материал этой статьи, то могу порекомендовать две предыдущие по этой теме:

Блокирующая обработка тактовой кнопки для Arduino.
Тактовая кнопка, как подключить правильно к "+" или "-"

Также в моем блоге вы можете найти другие статьи по программированию Arduino:

Автор:
OldFashionedEngineer

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js