Кнопка #
Кнопка является простейшим устройством, при помощи которого можно управлять ходом программы на микроконтроллере, но физически она выполняет очень простую функцию: замыкает и размыкает контакт. Кнопки бывают нескольких типов:
Фиксация:
- С фиксацией - после отпускания остаётся нажатой
- Без фиксации - после отпускания отключается обратно
Поведение:
- Нормально разомкнутая (Normal Open, NO) - при нажатии замыкает контакты
- Нормально замкнутая (Normal Closed, NC) - при нажатии размыкает контакты
- Тактовые кнопки (слева) - замыкают или размыкают контакт. У обычных тактовых кнопок ноги соединены вдоль через корпус. Встречаются в большинстве электронных приборов с кнопками, на которые нажимает человек
- Микровыключатели или "микрики" (справа) обычно имеют три контакта, общий COM, нормально открытый NO и нормально закрытый NC. При отпущенной кнопке замкнута цепь COM-NC, при нажатой замыкается COM-NO. Часто встречаются там, где кнопка нажимается частью механизма (концевик станка, принтера, дверца микроволновки)
Рекомендуется изучить урок по работе с цифровыми входами
Подключение #
Подключим обычную 6 мм тактовую кнопку как open-drain вот таким образом:
В программе этот пин подтянем к питанию, соответственно нажатие кнопки можно будет обработать следующим образом, инвертировав для удобства:
void setup() {
Serial.begin(115200);
pinMode(3, INPUT_PULLUP);
}
void loop() {
bool state = !digitalRead(3);
// 1 - нажата, 0 - отпущена
Serial.println(state);
delay(100);
}
Нажатие и отпускание #
Чтобы разделить нажатие и отпускание на два раздельных события, используем флаг (машина состояний с двумя состояниями):
void setup() {
Serial.begin(115200);
pinMode(3, INPUT_PULLUP);
}
void loop() {
static bool pState = false;
bool state = !digitalRead(3);
if (pState != state) { // состояние изменилось
pState = state; // запомнить новое
if (state) Serial.println("Кнопка нажата");
else Serial.println("Кнопка отпущена");
}
}
Дребезг контактов #
Кнопка не идеальна и контакт замыкается не сразу, какое-то время он механически "дребезжит" - внутри кнопки находится металлическая платинка, которая колеблется при нажатии и отпускании. Прогоняя данный алгоритм, система опрашивает кнопку и условия приблизительно за 6 мкс, то есть кнопка опрашивается около 166'666 раз в секунду! Этого достаточно, чтобы получить несколько тысяч срабатываний:
Нажатие кнопки. При отпускании получится аналогичная картина
Кнопка нажата
Кнопка отпущена
Кнопка нажата
Кнопка отпущена
Кнопка нажата
Для однозначного определения состояния кнопки дребезг нужно погасить - debounce.
Аппаратный дебаунс #
Дребезг можно погасить аппаратно - при помощи RC фильтра, образованного резистором и конденсатором:
Сигнал будет выглядеть примерно так:
Программный дебаунс #
Гашение дребезга можно сделать и программно - при помощи "таймера на миллис":
#define BTN_DEB 50 // тай-маут смены состояния, мс
void setup() {
Serial.begin(115200);
pinMode(3, INPUT_PULLUP);
}
void loop() {
static bool pState = false;
static uint32_t tmr;
bool state = !digitalRead(3);
// состояние изменилось и вышел таймер
if (pState != state && millis() - tmr >= BTN_DEB) {
tmr = millis(); // сбросить таймер
pState = state; // запомнить состояние
if (state) Serial.println("Кнопка нажата");
else Serial.println("Кнопка отпущена");
}
}
В обоих случаях дребезг контактов должен пропасть - кнопка будет выдавать два чётких события при нажатии и отпускании.
Удержание #
Добавив ещё одну конструкцию таймера можно получить событие удержания и импульсного удержания - кнопка удерживается и "сигналит" событиями с заданным периодом:
#define BTN_DEB 50 // тай-маут смены состояния, мс
#define BTN_HOLD 500 // тай-маут удержания, мс
void setup() {
Serial.begin(115200);
pinMode(3, INPUT_PULLUP);
}
void loop() {
static bool pState = false;
static uint32_t tmr;
bool state = !digitalRead(3);
if (pState != state && millis() - tmr >= BTN_DEB) {
tmr = millis();
pState = state;
if (state) Serial.println("Кнопка нажата");
else Serial.println("Кнопка отпущена");
}
// кнопка удерживается дольше 500 мс
if (pState && millis() - tmr >= 500) {
tmr = millis(); // сброс таймера
Serial.println("Кнопка удержана");
}
}
При нажатии и удержании кнопки данный код сначала выведет "Кнопка нажата"
, затем начнёт выводить "Кнопка удержана"
с периодом 500 мс - примерно так и реализовано изменение "настройки" при клике и удержании в большинстве цифровых устройств.
Однократное удержание #
Если нужен однократный сигнал на удержание - можно ввести флаг:
#define BTN_DEB 50 // тай-маут смены состояния, мс
#define BTN_HOLD 700 // тай-маут удержания, мс
void setup() {
Serial.begin(115200);
pinMode(3, INPUT_PULLUP);
}
void loop() {
static bool pState = false;
static bool hold = false; // флаг удержания
static uint32_t tmr;
bool state = !digitalRead(3);
if (pState != state && millis() - tmr >= BTN_DEB) {
tmr = millis();
pState = state;
hold = false; // сброс флага удержания
if (state) Serial.println("Кнопка нажата");
else Serial.println("Кнопка отпущена");
}
// кнопка удерживается дольше 500 мс
if (pState && !hold && millis() - tmr >= 500) {
hold = true; // флаг удержания
Serial.println("Кнопка удержана");
}
}
Класс кнопки #
Если нужно обрабатывать больше одной кнопки - функциональный подход становится неудобным - начинает дублироваться код и переменные, обработку кнопки пора переносить в отдельный класс и выносить в файл. Например - так:
#include "Button.h"
// кнопки подключены к пинам 3 и 4 и GND
Button btn1(3);
Button btn2(4);
void setup() {
Serial.begin(115200);
}
void loop() {
if (btn1.click()) Serial.println("click 1");
if (btn1.hold()) Serial.println("hold 1");
if (btn2.click()) Serial.println("click 2");
if (btn2.hold()) Serial.println("hold 2");
}
#pragma once
#include <Arduino.h>
#define BTN_DEB 50 // тай-маут смены состояния, мс
#define BTN_HOLD 700 // тай-маут удержания, мс
class Button {
public:
Button(uint8_t pin) : _pin(pin) {
pinMode(pin, INPUT_PULLUP);
}
bool click() {
bool state = !digitalRead(_pin);
if (_pState != state && millis() - _tmr >= BTN_DEB) {
_pState = state;
_hold = false;
_tmr = millis();
if (state) return true;
}
return false;
}
bool hold() {
if (_pState && !_hold && millis() - _tmr >= BTN_HOLD) {
_hold = true;
return true;
}
return false;
}
private:
uint8_t _pin;
bool _pState;
bool _hold;
uint32_t _tmr;
};
События #
Можно переписать класс в событийно-ориентированном стиле, добавив подключение обработчика - всю логику вынесем в "тикер":
#include "Button.h"
// кнопки подключены к пинам 3 и 4
Button btn1(3);
Button btn2(4);
void click1() {
Serial.println("click 1");
}
void hold1() {
Serial.println("hold 1");
}
void setup() {
Serial.begin(115200);
btn1.onClick(click1);
btn1.onHold(hold1);
btn2.onClick([]() {
Serial.println("click 2");
});
btn2.onHold([]() {
Serial.println("hold 2");
});
}
void loop() {
btn1.tick();
btn2.tick();
}
#pragma once
#include <Arduino.h>
#define BTN_DEB 50 // тай-маут смены состояния, мс
#define BTN_HOLD 700 // тай-маут удержания, мс
class Button {
typedef void (*ButtonCallback)();
public:
Button(uint8_t pin) : _pin(pin) {
pinMode(pin, INPUT_PULLUP);
}
void onClick(ButtonCallback cb) {
_click_cb = cb;
}
void onHold(ButtonCallback cb) {
_hold_cb = cb;
}
void tick() {
bool state = !digitalRead(_pin);
if (_pState != state && millis() - _tmr >= BTN_DEB) {
_pState = state;
_hold = false;
_tmr = millis();
if (state && _click_cb) _click_cb();
}
if (_pState && !_hold && millis() - _tmr >= BTN_HOLD) {
_hold = true;
if (_hold_cb) _hold_cb();
}
}
private:
uint8_t _pin;
bool _pState;
bool _hold;
uint32_t _tmr;
ButtonCallback _click_cb = nullptr;
ButtonCallback _hold_cb = nullptr;
};
Библиотека #
Велосипеды стоит изобретать только в обучающих целях - всё уже изобретено до нас, например - моя библиотека EncButton - очень мощная библиотека для работы с кнопкой (а также энкодером и энкодером с кнопкой), имеет огромное количество сценариев использования и событий от кнопки, см. полную документацию по ссылке. Самый простой пример:
#include <EncButton.h>
Button btn(3);
void setup() {
Serial.begin(115200);
}
void loop() {
btn.tick();
if (btn.click()) Serial.println("click");
if (btn.hold()) Serial.println("hold");
if (btn.step()) Serial.println("step");
}