Дребезг контактов и его подавление (debounce)

Ты нажал кнопку один раз. А Arduino насчитала пять нажатий. Добро пожаловать в мир дребезга — и его укрощения.

Дребезг (bounce) — это когда металлические контакты кнопки при нажатии несколько миллисекунд «звенят», быстро замыкаясь и размыкаясь. Для нас это одно нажатие, для Arduino — десяток.

Сейчас сделаем счётчик нажатий, который считает честно — одно нажатие = +1, без призрачных срабатываний. Это классическая задача, отделяющая новичка от уверенного пользователя.

Что происходит физически

  Идеал (как мы думаем):   ____|‾‾‾‾‾‾‾‾‾‾|____

  Реальность (дребезг):    ____|‾|_|‾|_|‾‾‾‾‾|____
                               ^^^^^^^
                          контакты "звенят" ~1-20 мс

Если просто считать каждое изменение с LOW на HIGH, ты насчитаешь все эти «звоны». Решение: после первого изменения подождать ~30 мс и убедиться, что сигнал устаканился.

Debounce без delay: конечный автомат

Мы не используем delay (он бы заморозил всю программу). Вместо этого запоминаем время последнего изменения через millis() — счётчик миллисекунд с момента включения платы.

const int BUTTON = 2;
int lastReading = HIGH;
int stableState = HIGH;
unsigned long lastChange = 0;
const unsigned long DEBOUNCE_MS = 30;
int count = 0;

void setup() {
  pinMode(BUTTON, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  int reading = digitalRead(BUTTON);
  if (reading != lastReading) {
    lastChange = millis();           // сигнал дёрнулся - засекаем время
  }
  if (millis() - lastChange > DEBOUNCE_MS) {
    if (reading != stableState) {    // прошло 30 мс, состояние новое
      stableState = reading;
      if (stableState == LOW) {      // именно нажатие
        count++;
        Serial.println(count);
      }
    }
  }
  lastReading = reading;
}

Как работает под капотом

millis() возвращает число миллисекунд с момента старта (тип unsigned long — большое число). Идея debounce: каждый раз, когда сигнал меняется, мы «обнуляем таймер». Состояние считается настоящим только если оно держалось дольше 30 мс без изменений. Звон длится единицы миллисекунд, поэтому он не успевает «пережить» порог — мы его игнорируем.

Вот та же логика как чистая модель на Python — конечный автомат debounce, прогоняем через «зашумлённый» сигнал:

# Та же логика на Python: debounce как конечный автомат
DEBOUNCE = 30
def debounce(samples):
    # samples: список (время_мс, сырое_значение), 0=нажата,1=отпущена
    stable = 1; last = 1; last_change = 0; presses = 0
    for t, raw in samples:
        if raw != last:
            last_change = t          # сигнал дёрнулся
        if t - last_change > DEBOUNCE:
            if raw != stable:
                stable = raw
                if stable == 0:      # подтверждённое нажатие
                    presses += 1
        last = raw
    return presses

# Дребезг: звон в начале, потом устойчивое нажатие на 50мс
signal = [(0,1),(2,0),(4,1),(6,0),(8,1),(10,0),(60,0),(120,1)]
print("Насчитано нажатий:", debounce(signal))   # должно быть 1

Запусти — несмотря на пять «дёрганий» в начале, автомат насчитает ровно одно нажатие. Магия порога времени.

Частые ошибки

  • Считают каждое изменение без задержки — счётчик прыгает на 5–10 за нажатие.
  • Используют int для millis(). Нужен unsigned long, иначе через ~32 секунды переполнение.
  • Ставят delay(30) вместо millis-логики — работает, но замораживает остальную программу.

Best practices

  • Порог 20–50 мс — золотая середина: дребезг гасится, а реакция остаётся быстрой.
  • Для нескольких кнопок есть библиотека Bounce2 — она делает то же самое в одну строку.
  • Всегда храни время в unsigned long и сравнивай через вычитание millis() - lastChange.

Аппаратный способ и когда он нужен

Дребезг можно гасить не только кодом, но и железом — добавив к кнопке конденсатор. Конденсатор — это крошечный «бак» для заряда: он не даёт напряжению меняться мгновенно, поэтому быстрые «звоны» сглаживаются ещё до того, как доберутся до пина. Такой RC-фильтр (резистор + конденсатор) применяют, когда важна максимальная надёжность или когда сигнал читает не программа, а другая микросхема. Но для большинства любительских проектов программный debounce проще и гибче: ничего не паять, порог легко поменять в коде.

Запомни главную мысль шире самой кнопки: реальные сигналы грязные. Кнопки дребезжат, датчики шумят, провода ловят наводки. Зрелый подход к электронике — всегда закладывать, что вход «врёт», и фильтровать его: debounce для кнопок, усреднение для датчиков, проверка диапазона для чисел. Этот образ мышления отличает работающее устройство от того, что «иногда глючит, и непонятно почему».

Итоги

Дребезг — это физический «звон» контактов. Победа — в логике: засекаем millis() при каждом изменении и доверяем состоянию, только если оно продержалось дольше порога. Это первый твой конечный автомат. Дальше перейдём от «вкл/выкл» к измерению напряжения — аналоговому вводу.

Проверьте себя
1. Что такое дребезг контактов кнопки?
AПрограммная ошибка
BФизический «звон» контактов при нажатии в течение нескольких мс
CСлишком высокое напряжение
DПерегрев пина
2. Какой тип нужен для хранения значения millis()?
Aint
Bunsigned long
Cbool
Dchar