Arduino

Тактовые кнопки и потенциометр: дебаунсинг и считывание аналоговых значений

Введение

В заключительной статье первой части цикла разберём два самых базовых, но фундаментальных компонента почти любого Arduino-проекта — простые тактовые кнопки (4 штуки в комплекте) и потенциометр (переменный резистор), обычно используемый как ручка плавной настройки какого-либо параметра. Несмотря на простоту, именно дребезг контактов кнопок — источник одной из самых частых проблем у начинающих, и правильная обработка дебаунса важна для надёжности любой прошивки, использующей механические кнопки.

Концепция

Механический контакт кнопки при нажатии не переключается мгновенно и чисто — происходит несколько очень быстрых, паразитных переключений состояния (дребезг) в течение нескольких миллисекунд, прежде чем контакт окончательно «устаканивается» в новом состоянии. Без программной обработки дребезга один физический акт нажатия кнопки может быть интерпретирован прошивкой как несколько отдельных, быстро следующих друг за другом нажатий. Потенциометр, в отличие от кнопки, не требует дебаунса — это плавно изменяющееся аналоговое значение, считываемое тем же analogRead(), что и оси джойстика.

Пример кода

// firmware/src/main.cpp — корректный неблокирующий дебаунс четырёх кнопок и потенциометра
#define BUTTON_COUNT 4
const byte buttonPins[BUTTON_COUNT] = {2, 3, 4, 5};
#define POTENTIOMETER_PIN A0

bool lastRawState[BUTTON_COUNT];
bool debouncedState[BUTTON_COUNT];
unsigned long lastChangeTime[BUTTON_COUNT];
const unsigned long DEBOUNCE_DELAY = 30; // миллисекунды устойчивости перед признанием состояния валидным

void setup()
{
    Serial.begin(9600);
    for (int i = 0; i < BUTTON_COUNT; i++) {
        pinMode(buttonPins[i], INPUT_PULLUP);
        lastRawState[i] = HIGH;
        debouncedState[i] = HIGH;
        lastChangeTime[i] = 0;
    }
    delay(100);
    Serial.println("READY");
}

void loop()
{
    for (int i = 0; i < BUTTON_COUNT; i++) {
        const bool rawState = digitalRead(buttonPins[i]);

        if (rawState != lastRawState[i]) {
            lastChangeTime[i] = millis(); // фиксируем момент любого замеченного изменения
            lastRawState[i] = rawState;
        }

        // Состояние признаётся стабильным только если оно не менялось дольше DEBOUNCE_DELAY
        if (millis() - lastChangeTime[i] > DEBOUNCE_DELAY && debouncedState[i] != rawState) {
            debouncedState[i] = rawState;
            Serial.print("BUTTON:");
            Serial.print(i);
            Serial.println(debouncedState[i] == LOW ? ":PRESSED" : ":RELEASED");
        }
    }

    static unsigned long lastPotSend = 0;
    if (millis() - lastPotSend >= 200) {
        const int potValue = analogRead(POTENTIOMETER_PIN);
        Serial.print("POT:");
        Serial.println(map(potValue, 0, 1023, 0, 100)); // нормализация к процентам для удобства
        lastPotSend = millis();
    }
}

Пояснения к коду

Алгоритм дебаунса основан на отслеживании времени последнего замеченного изменения сырого состояния (lastChangeTime) для каждой кнопки независимо — каждый раз, когда считанное сырое значение отличается от предыдущего считанного сырого значения, время сбрасывается; и только если состояние остаётся неизменным дольше DEBOUNCE_DELAY (30 мс), оно признаётся достоверным, стабильным состоянием и сравнивается с уже зафиксированным debouncedState для определения, действительно ли произошло значимое изменение, о котором стоит сообщить через Serial. Этот подход полностью неблокирующий, в отличие от примитивного delay()-based дебаунса, и корректно работает для нескольких кнопок одновременно, независимо обрабатывая дребезг каждой.

Потенциометр считывается отдельно от логики кнопок, с собственным неблокирующим интервалом отправки (200 мс) — поскольку это плавно меняющееся аналоговое значение без какого-либо «дребезга», дебаунс ему не нужен, единственное, что регулируется — частота отправки показаний через Serial, аналогично частоте отправки данных любого другого аналогового датчика из предыдущих статей цикла.

Подводные камни

  • Слишком короткий DEBOUNCE_DELAY (заметно меньше 20-30 мс) не полностью устраняет дребезг для некоторых, особенно изношенных или дешёвых кнопок, тогда как слишком долгий (более 50 мс) может сделать реакцию на нажатие ощутимо медленной, заметной пользователю как небольшая задержка — оптимальное значение зависит от конкретного физического экземпляра кнопок и требует небольшого эмпирического подбора для конкретного проекта.
  • Независимая обработка дребезга для каждой кнопки в массиве через отдельные элементы lastChangeTime[i]/debouncedState[i] обязательна — попытка использовать общие, разделяемые между всеми кнопками переменные дебаунса (что было бы ошибкой при поспешном написании кода) привела бы к некорректной обработке, когда дребезг одной кнопки влиял бы на распознавание состояния другой, физически независимой кнопки.
  • INPUT_PULLUP для кнопки означает, что в нормальном, ненажатом состоянии пин читается как HIGH, а нажатие (замыкание на землю) даёт LOW — это инвертированная относительно интуитивного ожидания логика (нажатие = LOW, а не HIGH), и код, написанный без учёта этой инверсии (или для кнопки, подключённой по противоположной схеме с внешним резистором, без INPUT_PULLUP), будет интерпретировать нажатия ровно наоборот.
  • Потенциометр, как и любой аналоговый компонент, подвержен небольшому электрическому шуму в считываемых значениях — даже без физического движения ручки потенциометра последовательные вызовы analogRead() могут отличаться на одну-две единицы из-за шума АЦП; для большинства применений (как в примере, с округлением до процентов через map()) эта погрешность незаметна, но для проектов, критичных к точности (например, точная настройка узкого диапазона), может потребоваться программное усреднение нескольких последовательных считываний.