Введение
В заключительной статье первой части цикла разберём два самых базовых, но фундаментальных компонента почти любого 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()) эта погрешность незаметна, но для проектов, критичных к точности (например, точная настройка узкого диапазона), может потребоваться программное усреднение нескольких последовательных считываний.