Пока еще свежи воспоминания о подключении кнопки к Arduino, дарованные нам предыдущей статьей-уроком, продолжим развивать эту тему. Все дело в том, что при обсуждении работы с кнопками нельзя не упомянуть об одном эффекте под названием дребезг контактов. Этому и будет посвящен сегодняшний пост, а точнее борьбе с этим самым дребезгом, поскольку явление это является нежелательным. Плавно перемещаемся к сути вопроса.
Теоретическая часть.
Начнем по отличнейшей традиции с разбора сути происходящих физических явлений, а в практической части статьи перейдем к конкретным действиям. Пусть кнопка подключена так же, как и в предыдущем уроке:
Собственно, в данном случае все довольно просто - как и все в нашем мире, кнопка, к сожалению, не идеальна, не идеальны и поверхности контактов, не идеальны и материалы. И одним из проявлений этой неидеальности как раз и является дребезг контактов. На практике представляет из себя следующее:
То есть, проще говоря, в идеальном случае кнопка должна работать так:
- кнопка не нажата - имеем высокий уровень, то есть 5 В
- нажимаем кнопку - уровень моментально становится низким и остается в этом состоянии, пока кнопка нажата
- отпускаем кнопку - также молниеносно сигнал возвращается к стабильно высокому уровню
Все четко и понятно, на то случай и идеальный. В реальности же при каждом изменении состояния кнопки (нажатии/отпускании) возникает тот самый дребезг контактов. Что означает, что на протяжении некоторого времени уровень сигнала довольно быстро и случайным образом меняется. Время этих переходных процессов невелико, тем не менее, очевидно, что из этого вытекает куча проблем.
Вот, к примеру, допустим, что нам нужно подсчитать количество нажатий, пользователь однократно нажал кнопку, а программа насчитала несколько (а в реальности может и несколько сотен), так как уровень по факту изменился некоторое количество раз. И таких примеров можно придумать кучу, поэтому данный нежелательный эффект необходимо должным образом искоренить.
Но для начала посмотрим врагу в глаза, то есть воспроизведем дребезг контактов и воочию убедимся в его наличии. Я возьму Arduino Uno, но особой разницы, какую именно плату из семейства Ардуино использовать, нет. Повторим еще раз схему:
На макетной же плате можно осуществить коммутацию так:
Создаем простенький проект, в котором выведем текущее состояние кнопки в Serial Monitor:
// Кнопка подключена к D3 int buttonPin = 3; void setup() { // D3 работает в качестве входа pinMode(buttonPin, INPUT); Serial.begin(115200); } void loop() { int currentButtonState = digitalRead(buttonPin); Serial.print("Button state: "); Serial.println(currentButtonState); }
Что мы увидим в результате? Все просто - поскольку мы выводим значения непосредственно из функции loop()
, то результатом работы скетча будут быстро бегущие цифры. Допустим, кнопка не нажата, на входе логическая единица, соответственно это и увидим:
Суть же и смысл эксперимента в следующем. Нажимаем кнопку, значение изменилось на "0" (на входе низкий уровень напряжения). Все, казалось бы, верно, но вот если рассмотреть значения непосредственно в момент нажатия, то увидим как раз-таки нежелательные случайные скачки между "1" и "0", вызванные дребезгом контактов.
Практическая часть.
Ну что же, первопричину дребезга выяснили, на практике воспроизвели, дело за малым - осталось проблему решить 👍 И здесь сходу можно выделить несколько самых распространенных вариантов, начнем с аппаратного.
Аппаратное уничтожение дребезга контактов.
Аппаратный вариант не требует внесения изменений в программу, то есть в скетч, потому так и называется. В данном случае простейшее решение заключается в добавлении в схему нескольких компонентов, а точнее двух - короля фильтрации конденсатора и резистора.
Выглядит все это дело так, здесь также присутствует подтягивающий резистор R_2 :
И на макетной плате:
Конденсатор C_1 в сочетании с резистором R_1 образуют RC-цепочку (представляющую из себя фильтр нижних частот). Здесь, по ссылке, можно почитать подробнее о нюансах, связанных с RC-цепочками. А, поскольку сегодня у нас на повестке тема "Arduino", то погружаться в детали, а также дублировать их из упомянутой статьи я не буду, не люблю мешанину 🙂 Сейчас можно кратко выделить тот факт, что "традиционные" номиналы в данном случае имеют следующие значения:
- емкость конденсатора: 100 нФ (0.1 мкФ)
- сопротивление резистора: 1 КОм - 10 КОм
Все эти значения вытекают из того, что постоянная времени RC-цепи должна превышать время успокоения дребезга кнопки. Так, все, не углубляюсь в эту тему, переходим к программному способу устранения дребезга, только еще раз взглянем на модернизированную схему подключения компонентов к Arduino Uno:
Таким образом, получили классическое и наиболее простое решение проблемы с наименьшими затратами на компоненты. В большинстве случаев этого будет достаточно.
Программное уничтожение дребезга контактов.
Здесь самый базовый вариант заключается в том, чтобы после изменения состояния кнопки выждать паузу, достаточную для окончания всех дребезговых процессов, сейчас на практике все будет абсолютно понятно. Расписываю максимально подробно, пусть в ущерб оптимальности, но во имя большей прозрачности:
// Кнопка подключена к D3 int buttonPin = 3; // Текущее состояние кнопки int currentButtonState = HIGH; // Предыдущее состояние кнопки int previousButtonState = HIGH; // Величина задержки constexpr int DEBOUNCE_DELAY_MS = 25; void setup() { // D3 работает в качестве входа pinMode(buttonPin, INPUT); Serial.begin(115200); } void loop() { // Считываем состояние входа D3 int currentButtonState = digitalRead(buttonPin); // Если считанное значение отличается от сохраненного (состояние кнопки изменилось) if (currentButtonState != previousButtonState) { // Банальная задержка для того, чтобы миновать переходные процессы/дребезг контактов delay(DEBOUNCE_DELAY_MS); // Теперь в теории дребезг миновал, спокойно считываем состояние currentButtonState = digitalRead(buttonPin); } // Текущее значение становится предыдущим previousButtonState = currentButtonState; // Далее программа продолжается, оперируем с currentButtonState Serial.println(currentButtonState); }
В данном случае текущее значение будет храниться в переменной currentButtonState
, а предыдущее - в previousButtonState
. Считываем значение, если оно не равно сохраненному в previousButtonState
, значит по всей видимости состояние кнопки изменилось (было нажатие, к примеру). В течение времени, которое задается в переменной DEBOUNCE_DELAY_MS
(значение в миллисекундах), просто ожидаем, намертво остановившись на вызове delay()
. За это время предполагаем, что переходные процессы завершились, уровень на входе стабилен, можно спокойно считать повторно значение в переменную и с ним уже работать дальше:
currentButtonState = digitalRead(buttonPin);
Решение простое до безобразия и до безобразия же неоптимальное, прямо скажем - скверное ) Причина проста - пока микроконтроллер мог заниматься полезной деятельностью, он бессмысленно висит в функции ожидания , то есть на вызове delay(DEBOUNCE_DELAY_MS)
. Да, во многих случаях это сработает, да, проблем не вызовет, но все-таки решение так себе как минимум. Поэтому рассмотрим еще один вариант - не сильно сложнее, не особо напряжный, но при этом гораздо оптимальнее и правильнее. Итак, смотрим код, потом подробно пройдемся по нему поэтапно:
// Кнопка подключена к D3 int buttonPin = 3; // Результирующее состояние кнопки int finalButtonState = HIGH; // Текущее состояние кнопки int currentButtonState = HIGH; // Предыдущее состояние кнопки int previousButtonState = HIGH; // Счетчик миллисекунд int debounceTimeMs = 0; // Величина задержки const int DEBOUNCE_DELAY_MS = 50; void setup() { // D3 работает в качестве входа pinMode(buttonPin, INPUT); Serial.begin(115200); } void loop() { // Считываем состояние входа D3 int currentButtonState = digitalRead(buttonPin); // Если считанное значение отличается от сохраненного (состояние кнопки изменилось) if (currentButtonState != previousButtonState) { // Сохраняем текущую временную отметку debounceTimeMs = millis(); } // Если с момента обновления debounceTimeMs прошло более, чем DEBOUNCE_DELAY_MS мс if ((millis() - debounceTimeMs) >= DEBOUNCE_DELAY_MS) { // Обновляем результирующее значение, с которым дальше и работаем finalButtonState = digitalRead(buttonPin); } // Обновляем значение previousButtonState = currentButtonState; // Далее программа продолжается, оперируем с finalButtonState Serial.println(finalButtonState); }
В данном случае у нас уже три переменные для разных состояний кнопки:
currentButtonState
- текущее состояниеpreviousButtonState
- предыдущееfinalButtonState
- результирующее, то которое мы и стремимся получить и очистить от всяческих нежелательных явлений в виде дребезга в данном случае
Итак, по-прежнему в самом начале функции loop()
анализируем текущее состояние цифрового входа D3:
int currentButtonState = digitalRead(buttonPin);
Проверяем, изменилось ли эта величина относительно предыдущей, и, если изменение имело место, то в переменную debounceTimeMs
сохраняем текущее количество миллисекунд с момента запуска платы:
if (currentButtonState != previousButtonState) { // Сохраняем текущую временную отметку debounceTimeMs = millis(); }
Для получения этого значения используем стандартную функцию Arduino - millis()
, которая возвращает количество миллисекунд с момента старта выполнения программы. Отлично, двигаемся дальше... А дальше мы только проверяем, прошло ли заданное время (DEBOUNCE_DELAY_MS
) с момента, когда было обновлено значение debounceTimeMs
:
if ((millis() - debounceTimeMs) >= DEBOUNCE_DELAY_MS)
Если условие выполнено - обновляем целевое результирующее значение, которое теперь избавлено от всякого дребезга и прочей нечисти.
Идея метода здесь проста, по факту мы получаем следующее: значение debounceTimeMs
будет обновляться каждый раз, когда изменилось состояние на входе. Это может быть вызвано как дребезгом, помехами, шумами и т. п., так и собственно нажатием. В любом случае обновляем значение переменной, сохраняя в него текущее количество миллисекунд. А finalButtonState
мы обновим тогда и только тогда, когда в течение DEBOUNCE_DELAY_MS
(50 мс в данном примере) изменений на входе не было. То есть напряжение устаканилось и стабилизировалось, значит мы можем считывать состояние входа и использовать для наших нужд. Вот и все, если механизм будет не до конца ясен, пишите в комментарии, я нарисую временную диаграмму протекающих процессов 👍
Метод также не избавлен от недостатков, но здесь уже нет простаивания на delay()
. Микроконтроллер занимается своей работой, проверяя при каждом заходе в loop()
, прошло ли необходимое время или еще нет. Поэтому данное решение проблемы гораздо предпочтительнее первого, в чем мы и убедились наглядно. А вообще, в целом, конкретная реализация может отличаться в разных примерах, неизменной остается концепция - переждать нежелательные процессы, при этом позволив контроллеру заниматься другими задачами.
Так, ну и на этом заканчиваем, пожалуй, на сегодня, до скорого 🤝