В своей прошлой статье я писал про блокирующие способы обработки кнопки. Реакция была, наверное, больше положительная, чем отрицательная. Хотя минусов мне поднасовали… Но, тем не менее, я решил продолжить. И в этой статье я хочу поделиться с вами своим опытом по неблокирующей обработке кнопок на 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:

Экономим выводы для Arduino. Управление сдвиговым регистром 74HC595 по одному проводу

Комментарии (22)


  1. gleb_l
    14.12.2022 12:02
    +4

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

    А вот в оригинальном Мониторе (читай - BIOS‘е) Радио-86РК обработка клавиатуры (втч неблокирующая) была сделана вообще без прерываний, и при этом поддерживался чисто программный (!) звук подтверждения нажатия клавиш, антидребезг, и ускоренный повтор при долгом нажатии. Никаких аппаратных средств измерения времени (кроме программных циклов) в нем тоже не было. И опрос матрицы тоже был чисто программный. Если разобрать, как это было сделано тогда - окажется любопытно.


    1. OldFashionedEngineer Автор
      14.12.2022 12:56

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


    1. Andy_Big
      14.12.2022 13:31

      Если разобрать, как это было сделано тогда - окажется любопытно.

      Ну разве что ради любопытства :)


      1. OldFashionedEngineer Автор
        14.12.2022 14:38
        +3

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


  1. Andy_Big
    14.12.2022 13:30
    +8

    Позволю себе несколько замечаний.

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

    2. Структура текущего состояния кнопок мне кажется излишне большой. Ниже приведу свой вариант с пояснениями.

    3. "В обработчике прерывания, вместо вывода в терминал сообщений о нажатии кнопки, вы можете разместить полезный код, который будет реагировать на нажатие кнопки." - ну зачем же учить начинающих плохому? В прерывании код должен быть как можно более коротким. В данном случае - только оценка текущего состояния кнопок и все, никаких реакций на их нажатие, тем более никаких медленных блокирующих выводов в UART. Это очень, очень вредная рекомендация.

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

    enum
    {
      	KB_FREE,		// отсутствие событий у кнопки
      	KB_WORKED,		// последнее событие кнопки было обработано
    	KB_PREPRESSED,	// есть сигнал нажатия, идет выжидание антибребезга
    	KB_SPRESSED,	// подтверждено нажатие кнопки после антидребезга
    	KB_LPRESSED,	// кнопка остается нажатой длительное время
    	KB_SRELEASED,	// кнопка отпущена после короткого времени нажатия
    	KB_LRELEASED,	// кнопка отпущена после долгого нажатия
    } KBD_STATES;
    
    typedef struct
    {
    	KBD_STATES state;
    	uint8_t	time;
    } KEYBOARD;
    

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

    Алгоритм обработки каждой кнопки в цикле в прерывании таймера таков:

    for (uint8_t kb = 0; kb < keys_count; kb++)
    {
      	switch (Keys[kb].state)  	// проверяем текущий статус кнопки
    	{
      	  	// если событие кнопки было обработано и сейчас кнопка отпущена,
      	  	// то возвращаем ее статус в свободный
    		case KB_WORKED:
      			if (!KeyRead(kb))  	// если на входе нет сигнала нажатой кнопки
    			{
    				Keys[0].state = KB_FREE;
    			}
    			break;
      	  	// если кнопка в свободном состоянии, или была отпущена после
      	  	// любого нажатия, и в данный момент есть сигнал ее нажатия,
      	  	// то начинаем обработку антидребезга
    		case KB_FREE:
    		case KB_SRELEASED:
    		case KB_LRELEASED:
    			if (KeyRead(kb))
    			{
    				Keys[kb].state = KB_PREPRESSED;
    				Keys[kb].time = 0;  	// обнуляем счетчик времени
    			}
    			break;
      	  	// если кнопка в состоянии обработки антидребезга и есть сигнал
    		// нажатия - инкрементируем счетчик и проверяем не достиг ли он времени
    		// проверки антидребезга, если достиг, то присваиваем кнопке статус
    		// короткого нажатия.
    		// Если сигнала нажатия нет, значит проверка антидребезга не прошла,
    		// обнуляем счетчик времени и возвращаем статус кнопки в свободный.
    		case KB_PREPRESSED:
    			if (KeyRead(kb))
    			{
    				if (Keys[kb].time > 6)	// время обработки антидребезга вышло
    					Keys[kb].state = KB_SPRESSED;
    				else
    					Keys[kb].time++;
    			}
    			else
    			{
    				Keys[kb].state = KB_FREE;
    				Keys[kb].time = 0;
    			}
    			break;
    		// если кнопка в состоянии короткого нажатия и есть сигнал нажатия -
    		// инкрементируем счетчик времени и проверяем не достиг ли он времени
    		// долгого нажатия, если достиг, то переводим статус кнопки в долгое
    		// нажатие.
    		// Если нет сигнала нажатия - значит кнопка была отпущена, ставим
    		// ей статус "отпущена после короткого нажатия".
    		case KB_SPRESSED:
    			if (KeyRead(kb))
    			{
    				if (Keys[kb].time > 150)
    				{
    					Keys[kb].state = KB_LPRESSED;
    				}
    				else
    					Keys[kb].time++;
    			}
    			else
    			{
    				Keys[kb].state = KB_SRELEASED;
    				Keys[kb].time = 0;
    			}
    			break;
    		// если кнопка в состоянии долгого нажатия и есть сигнал нажатия -
    		// инкрементируем счетчик времени (это просто для легкой организации
    		// автоповтора нажатий).
    		// Если нет сигнала нажатия - значит кнопка была отпущена, ставим
    		// ей статус "отпущена после долгого нажатия".
    		case KB_LPRESSED:
    			if (!KeyRead(kb))
    			{
    				Keys[kb].state = KB_LRELEASED;
    				Keys[kb].time = 0;
    			}
    			else
    				Keys[kb].time++;
    			break;
    	}
    

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

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

    По счетчику времени: нет нужды считать в нем миллисекунды или любые другие реальные временные величины. Достаточно считать вызовы прерывания таймера - они все равно следуют с определенным промежутком времени. Поэтому достаточно 1-байтового счетчика, который посчитает, скажем, 5 вызовов (с периодом 20 миллисекунд) вместо 100 миллисекунд для антидребезга. Или 50 вызовов вместо 1000 миллисекунд для долгого нажатия.


    1. OldFashionedEngineer Автор
      14.12.2022 14:56

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

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

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

      Когда пишешь код, всегда надо искать компромиссы. Или производительность, или удобство. Когда речь идет о программировании Arduino, то о производительности речь не идет. Концепция предполагает удобство программирования без оглядки на ресурсы контроллера.

      О недостатках и достоинствах моего примера я написал в заключении в статье. Код ориентирован на те случаи, когда кнопок немного.

      Если говорить об использовании ресурсов микроконтроллера, просто посмотрите, как реализованы некоторые библиотеки Arduino. И почитайте статьи по программированию Arduino в интернете. Это дикая смесь ООП и обычного функционального программирования в стиле Си.

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


      1. Andy_Big
        14.12.2022 17:34
        +1

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

        Честно говоря, из текста это совершенно не очевидно, по крайней мере для меня :)

        Концепция предполагает удобство программирования без оглядки на ресурсы контроллера.

        Речь ведь не о производительности, а о базовых принципах программирования для микроконтроллера. В прерываниях должен быть код, выполняющийся за минимально возможное время, что в 8-битной Ардуине на 8 МГц, что в 32-битном ARM-е на 400 МГц :)

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

        Это отдельный вид искусства антипрограммирования, согласен. Значит тем более не нужно прививать начинающим эти антипаттерны :)


        1. OldFashionedEngineer Автор
          14.12.2022 17:55

          Вы смотрели, как реализована обработка прерываний в hal-драйвере? Там перекладных прилично. 20 лет назад надо было оптимизировать код для экономии ресурсов. Сейчас это не сильно актуально.


          1. Andy_Big
            14.12.2022 18:11
            +1

            Смотрел и щупал, поэтому не использую его в своих проектах. HAL уже вплотную приблизился к ардуино-стилю :)

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


            1. OldFashionedEngineer Автор
              14.12.2022 19:52

              Давайте разделять цели и задачи. То, о чем Вы говорите, для меня понятно, можете не продолжать. Я начинал программировать ещё до того, как ардуино появилась. Статья нацелена на тех, кто только начал. USART в прерывание для данного случая - это хороший приём для того, чтобы увидеть работоспособность кода с прерываниями.

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


  1. Gumanoid
    14.12.2022 13:50
    +1

    В «A Guide to Debouncing» описан прикольный алгоритм:

    // Service routine called by a timer interrupt
    bool_t DebounceSwitch2()
    {
        static uint16_t State = 0; // Current debounce status
        State = (State << 1) | !RawKeyPressed() | 0xe000;
        if (State == 0xf000)
            return TRUE;
        return FALSE;
    }


    1. Kotofay
      14.12.2022 14:31

      Зависит от частоты таймера.

      Если таймер часто вызывается то дребезг пролезет.


    1. gleb_l
      15.12.2022 18:34

      О, вот это круто! ФНЧ на программном сдвиговом регистре ;)


    1. OldFashionedEngineer Автор
      15.12.2022 19:34

      Какой в этом смысл? Для 8-ми битного контроллера сдвинуть 16-ти битную переменную и дважды наложить маску, ни чуть не проще, чем счетчик увеличить на единицу и проверку выполнить.


      1. Gumanoid
        15.12.2022 21:43

        Не очень понял с каким вариантом идёт сравнение. Если использовать счетчик нажатий, то надо сравнить что его значение совпадает со значением 12 тиков назад. Выглядит сложнее чем один инкремент и проверка.


        1. OldFashionedEngineer Автор
          15.12.2022 22:00

          Я про вариант, предложенный в статье.


          1. Gumanoid
            15.12.2022 22:23

            Там в несколько раз больше операций чем инкремент и проверка.


            1. OldFashionedEngineer Автор
              15.12.2022 23:01

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


  1. AVKinc
    14.12.2022 15:39
    +1

    Раз в 300мс вызываем функцию которая проверяет нажата ли кнопка, если нажата ставим флаг нажатия, если нет -то флаг снимаем. Этим мы сразу убираем дребезг. В программе тот кому надо проверяет флаг нажатия,поле обработки сбрасывает его. 10 строк кода. Очень удобно и позволяет мгновенно изменить управление хоть на ИК-приемник. Он будет также поднимать флаги.


    1. OldFashionedEngineer Автор
      14.12.2022 15:50
      +1

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


      1. AVKinc
        14.12.2022 17:27

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


        1. OldFashionedEngineer Автор
          14.12.2022 17:58
          +1

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