Аквариумный термостат собирался с энкодером, а приехал с одной кнопкой в щитке — отверстие просверлено, туда уже втулка. Меню переделал под одну кнопку: короткое нажатие листает три пункта по кругу, удержание — вход в выбранный. Три пункта — цель температуры, гистерезис и ночной режим — живут в таком меню прекрасно, а провод экономится один: вторая кнопка понадобилась бы ещё два провода и второе отверстие.
Меню на двух кнопках вверх-вниз и вложенные уровни — в уроке про меню на кнопках и уроке про вложенное меню, крутилка энкодером — в уроке про энкодер, широкий экран — в уроке про меню 20x4. Здесь угол — одна кнопка на всё: круг из пунктов, стрелка-курсор и различение короткого нажатия от долгого на millis.
Фронт, круг, долгое нажатие
Кнопку на D2 я держу на INPUT_PULLUP: разомкнута — HIGH, нажата — LOW. Листание считает фронт — переход HIGH в LOW, а не уровень: уровень при удержании сработает десятки раз, и пункты пролетят за одно нажатие. К фронту прилипает антидребезг: между фронтами держу паузу 30 мс по millis, иначе механический контакт подарит по два-три пункта за клик.
Долгое нажатие — тот же фронт, но с секундомером: запоминаем время прижатия и если через 800 мс кнопка всё ещё в LOW — это «вход», отмечаем выбранный пункт и больше листания на этом нажатии не ждём. Отпустили раньше порога — короткое, листаем. Три пункта ходят по кругу через (idx + 1) % 3: после третьего снова первый, и никакие крайние условия не нужны. Курсор — стрелка в колонке 0; в скетче символ «>», на живой плате его место занимает глиф из слота, как в уроке про три глифа.
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); const char* PUNKTY[3] = { "Tcela 24-30C ", "Gisterez 0.5-2C ", "Noch on/off "}; int idx = 0; // активный пунктbool sel = false; // пункт отмечен долгим нажатиемbool lastBtn = HIGH;unsigned long tPress = 0;bool longDone = false; void draw() { lcd.setCursor(0, 0); lcd.print("Menu "); lcd.print(idx + 1); lcd.print("/3 "); lcd.setCursor(0, 1); lcd.print(sel ? "*" : ">"); // на плате - глиф стрелки из слота lcd.print(PUNKTY[idx]);} void setup() { lcd.init(); lcd.backlight(); pinMode(2, INPUT_PULLUP); draw();} void loop() { bool now = digitalRead(2); // фронт HIGH->LOW с антидребезгом 30 мс if (lastBtn == HIGH && now == LOW && millis() - tPress > 30) { tPress = millis(); longDone = false; } // удержание дольше 800 мс - вход в пункт if (now == LOW && !longDone && millis() - tPress > 800) { longDone = true; sel = true; draw(); } // отпускание: короткое нажатие - листаем по кругу if (lastBtn == LOW && now == HIGH) { if (!longDone) { sel = false; idx = (idx + 1) % 3; draw(); } lastBtn = now; } lastBtn = now;}Разбор. Каркас — три события на одной кнопке: фронт ставит секундомер, удержание сверх 800 мс отмечает выбор и блокирует листание до отпускания, отпускание без долготы листает. Обратите внимание на порядок: проверка удержания стоит раньше проверки отпускания — иначе короткое нажатие успеет сработать как листание на границе. Флаг longDone — страховка от двойного срабатывания: долгое нажатие отмечено один раз, отпускание его просто отпускает.
Строки пунктов я выровнял пробелами до 16 символов: перерисовка между пунктами затирает хвосты, и отдельная очистка строки не нужна — тот же приём, что с фикс-шириной процентов в баре. Вход sel здесь только подсвечивает пункт звёздочкой: редактирование значений той же кнопкой — тема вложенного меню из урока выше по списку, там же и выход из редактирования. Для трёх пунктов-переключателей и выбора режима этого меню хватает на годы эксплуатации.
- Tcela 24-30C — цель термостата, шаг 0,5 °C, держит нерест рыбам
- Gisterez 0.5-2C — зазор включения реле против щёлканья каждые две минуты
- Noch on/off — приглушение подсветки и смещение цели на ночь
- Короткое нажатие — следующий пункт по кругу, после третьего — снова первый
- Удержание 800 мс — вход: пункт отмечен звёздочкой до следующего листания
- 1. Почему пункт меню листают по фронту нажатия, а не по уровню LOW?
- 2. Как отличить короткое нажатие от долгого в этом меню?
- Одна кнопка на всё: короткое нажатие — следующий пункт, удержание 800 мс — вход
- Фронт HIGH->LOW плюс антидребезг 30 мс против пролёта и двойных срабатываний
- Круг из трёх пунктов: idx = (idx + 1) % 3, крайние условия не нужны
- Курсор в колонке 0; на плате — глиф стрелки из CGRAM, в симе — символ >
- Строки пунктов выровнены пробелами до 16 — перерисовка затирает хвосты сама
Частые вопросы по теме урока
- Почему пункт меню листают по фронту нажатия, а не по уровню LOW?
- Пока кнопка зажата, условие now == LOW верно всё время — меню прокрутит десятки пунктов за одно нажатие. Фронт HIGH->LOW фиксирует одно нажатие, антидребезг отсекает повторные. Уровень срабатывает десятки раз за удержание — пункты пролетают
- Как отличить короткое нажатие от долгого в этом меню?
- При фронте запоминается tPress; удержание сверх порога — вход, отпускание раньше — листание. Флаг longDone не даёт одному нажатию сработать дважды. Засечь время фронта и сравнить удержание с порогом 800 мс