View Categories

Опрос кнопки

Кнопка #

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

Фиксация:

  • С фиксацией - после отпускания остаётся нажатой
  • Без фиксации - после отпускания отключается обратно

Поведение:

  • Нормально разомкнутая (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");
}
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

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