Кнопка – самый базовый элемент ввода данных в микроконтроллер, используется для управления электронным устройством. Функционально кнопка замыкает контакт при нажатии, что может быть использовано для передачи через неё электрического сигнала на пин МК - он читает значение пина и таким образом понимает, нажата кнопка или нет.
В наборе GyverKIT | START | IOT | EXTRA | |
---|---|---|---|---|
![]() |
Кнопка 12 мм | ✔ | ✔ | |
![]() |
Кнопка модуль | ✔ | ✔ |
Типы кнопок #
Фиксация:
- Без фиксации (momentary) - после отпускания отключается обратно
- С фиксацией (latching) - после отпускания остаётся нажатой
Поведение:
- Нормально разомкнутая (Normal Open, NO) - при нажатии замыкает контакты
- Нормально замкнутая (Normal Closed, NC) - при нажатии размыкает контакты
Тактовая #
Самая простая "стандартная" кнопка, используется повсеместно. Без фиксации, нормально разомкнутая. Бывает разных размеров, стандарт 6x6 мм:
Большая #
К большим тактовым кнопкам (12x12 мм) есть специальные колпачки (есть в наборе GyverKIT). Большие кнопки не очень приспособлены для подключения на макетной плате: у них очень широкие изогнутые ножки, которые туго входят в отверстия. Можно выпрямить их, а затем скрутить на 90 градусов при помощи пинцета. Так они не будут излишне растягивать контакты платы. Вторую пару ножек можно отогнуть или откусить кусачками:
Микровыключатель #
"Микрик" обычно имеет три контакта, общий COM, нормально открытый NO и нормально закрытый NC. При отпущенной кнопке замкнута цепь COM-NC, при нажатой замыкается COM-NO. Часто встречаются там, где кнопка нажимается частью механизма (концевик станка, принтера, дверца микроволновки)
Подключение к модулям #
Кнопка может быть использована для управления модулями с логическим входом: реле, транзистор и другим подобным. Рассмотрим подключение при питании от сетевого адаптера:
Подключение к Arduino #
Рекомендуется изучить урок по работе с цифровыми входами
Кнопка замыкает два контакта, поэтому пин МК должен быть подтянут к противоположному полюсу питания резистором ~10 кОм:
В Arduino мы можем подключить кнопку к GND и использовать внутреннюю подтяжку пина INPUT_PULLUP
:
void setup() {
Serial.begin(115200);
pinMode(3, INPUT_PULLUP);
}
void loop() {
bool state = !digitalRead(3);
// 1 - нажата, 0 - отпущена
Serial.println(state);
delay(100);
}
Модуль #
Модуль подключается к питанию и цифровому пину. На плате пин кнопки подтянут к GND резистором:
- При нажатии кнопка выдаёт сигнал
HIGH
- Режим работы пина должен быть
INPUT
, а не с подтяжкой к питанию, как у обычной кнопки!
void setup() {
Serial.begin(115200);
pinMode(3, INPUT);
}
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 фильтра, образованного резистором и конденсатором:
В наборе GyverKIT | START | IOT | EXTRA | |
---|---|---|---|---|
![]() |
Кнопка 12 мм | ✔ | ✔ | |
![]() |
Резистор 10 кОм | ✔ | ✔ | |
![]() |
Керам. конденсатор | ✔ |
Сигнал будет выглядеть примерно так:
Программный дебаунс 1 #
Гашение дребезга можно сделать и программно - при помощи таймера на миллис. Первый вариант - ловим первое изменение сигнала пина и игнорируем остальные, пока идёт таймаут:
#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("Кнопка отпущена");
}
}
Этот вариант мгновенно реагирует на нажатие и отпускание, но может пропустить "помеху" - короткий скачок напряжения.
Программный дебаунс 2 #
Более интересный вариант - ФНЧ (фильтр низких частот) - виртуальное состояние pState
не изменяется, пока не выйдет таймаут при стабильном реальном состоянии. Переменная таймера также используется в качестве флага первого изменения (значение 0
):
#include <Arduino.h>
#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) {
if (!tmr) tmr = millis(); // первое изменение
else if (millis() - tmr >= BTN_DEB) { // вышел тайм-аут
pState = state; // запомнить состояние
if (state) Serial.println("Кнопка нажата");
else Serial.println("Кнопка отпущена");
}
} else tmr = 0; // сброс
}
Этот вариант получает изменение не сразу, но позволяет игнорировать помехи в проводе.
В обоих случаях дребезг контактов должен пропасть - кнопка будет выдавать два чётких события при нажатии и отпускании.
Удержание #
Добавив ещё одну конструкцию таймера можно получить событие удержания и импульсного удержания - кнопка удерживается и "сигналит" событиями с заданным периодом:
#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() {
btn1.tick();
if (btn1.click()) Serial.println("click 1");
if (btn1.hold()) Serial.println("hold 1");
btn2.tick();
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);
}
void tick() {
bool state = !digitalRead(_pin);
_clickF = 0;
_holdF = 0;
if (_pState != state && millis() - _tmr >= BTN_DEB) {
_pState = state;
_hold = false;
_tmr = millis();
if (state) _clickF = true;
}
if (_pState && !_hold && millis() - _tmr >= BTN_HOLD) {
_hold = true;
_holdF = true;
}
}
bool click() {
return _clickF;
}
bool hold() {
return _holdF;
}
private:
uint32_t _tmr = 0;
uint8_t _pin;
bool _pState = 0;
bool _hold = 0;
bool _clickF = 0;
bool _holdF = 0;
};
События #
Можно переписать класс в событийно-ориентированном стиле, добавив подключение обработчиков. В этом случае не придётся делать манипуляции с флагами, но придётся хранить обработчики:
#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:
uint32_t _tmr = 0;
ButtonCallback _click_cb = nullptr;
ButtonCallback _hold_cb = nullptr;
uint8_t _pin;
bool _pState = 0;
bool _hold = 0;
};
Подробная обработка #
В уроке про конечный автомат есть ещё один вариант обработки действий с кнопки через большой конечный автомат с обработкой клика, удержания, импульсного режима и отпускания отдельно из всех режимов.
Библиотеки #
Для удобной работы с кнопками можно использовать библиотеки, например EncButton, её можно установить/обновить из встроенного менеджера библиотек Arduino по названию EncButton. Краткая документация находится по ссылке выше, базовые примеры есть в самой библиотеке.
Для работы библиотеки нужно вызывать метод tick()
в loop()
и опрашивать нужные события. Их там много - смотрите документацию.
Демо
#include <EncButton.h>
// подключаем кнопку на D3
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");
}
Кнопка управляет светодиодом
/*
Кнопка на D3. Переключаем светодиод по клику
*/
#include <EncButton.h>
// подключаем кнопку на D3
Button btn1(3);
void setup() {
// пин 13 как выход (для мигания светодиодом)
pinMode(13, OUTPUT);
}
void loop() {
// опрос кнопки происходит здесь
btn1.tick();
// клик по кнопке - переключить светодиод на 13 пине
if (btn1.click()) digitalWrite(13, !digitalRead(13));
}
Две кнопки, разные действия
/*
Кнопки на D2 и D3. Выполняем всякие действия
по нажатию и удержанию
*/
#include <EncButton.h>
// подключаем кнопки на пины D2 и D3
Button btn1(2);
Button btn2(3);
void setup() {
// открываем порт для связи
Serial.begin(115200);
// пин 13 как выход (для мигания светодиодом)
pinMode(13, OUTPUT);
}
void loop() {
// опрос кнопок происходит здесь
btn1.tick();
btn2.tick();
// клик по первой кнопке - вывод текста
if (btn1.click()) Serial.println("btn 1 click!");
// удержание первой кнопки - вывод текста
if (btn1.held()) Serial.println("btn 1 holded!");
// клик по второй кнопке - переключить светодиод на 13 пине
if (btn2.click()) digitalWrite(13, !digitalRead(13));
}
Две кнопки меняют значение переменной
/*
Кнопки на D2 и D3. Меняем значение переменной
*/
#include <EncButton.h>
// подключаем кнопки на пины D2 и D3
Button btn1(2);
Button btn2(3);
// эту переменную будем менять
int value = 0;
void setup() {
// открываем порт для связи
Serial.begin(115200);
}
void loop() {
// опрос кнопок происходит здесь
btn1.tick();
btn2.tick();
// клик по кнопке - меняем значение и выводим в порт
if (btn1.click()) {
value += 10;
Serial.println(value);
}
if (btn2.click()) {
value -= 10;
Serial.println(value);
}
}
Кнопка управляет яркостью светодиода
/*
Кнопка на D3. Управляем яркостью светодиода
на 13 пине. Используется программный ШИМ
*/
#include <EncButton.h>
// подключаем кнопку на D3
Button btn1(3);
void setup() {
// пин 13 как выход (для мигания светодиодом)
pinMode(13, OUTPUT);
}
int bright = 0; // храним яркость
int speed = 5; // скорость яркости
void loop() {
// опрос кнопки происходит здесь
btn1.tick();
// шим на 13 пине
softPWM(13, bright);
// клик по кнопке - переключить направление яркости
if (btn1.press()) speed = -speed;
// удержание - импульсное изменение яркости
if (btn1.step()) bright = constrain(bright + speed, 0, 255);
}
// софт шим
void softPWM(byte pin, byte val) {
static byte count;
count++;
if (count == 0 && val != 0) digitalWrite(pin, 1);
if (count == val) digitalWrite(pin, 0);
}
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками
