View Categories

Кнопка

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

В наборе 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);
}

Полезные страницы #

5 2 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

6 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх