Пины кончились к третьему датчику. Под параллельный LCD уходят шесть линий, под энкодер — три, и кнопкам меню уже некуда втыкаться. Классический ответ — LCD Keypad Shield: плата-шляпа, которая надевается сверху и несёт экран плюс пять кнопок, все на одном аналоговом входе. У меня такой шилд был первым интерфейсом, который заработал за вечер, — и первым интерфейсом, у которого не нажималась одна кнопка из пяти.
Кнопка не нажималась, потому что её не было: было число на A0, которое я сравнивал с табличным через ==. Клоны шилда собирают из резисторов с допуском 5%, и реальные отсчёты уезжают от справочных на десятки единиц. Разбор ниже: как лестница резисторов превращает пять кнопок в пять чисел, почему равенство не работает и что происходит в момент отпускания кнопки.
План такой: схема и физика, калибровка своего экземпляра, аналоговый дебаунс и меню из пяти кнопок — всё в одном уроке, с кодом, который я гонял на реальном шилде.
Отдельный бонус шилда — развод уже готов: экран, подстроечник контраста и лестница кнопок сидят на плате. Макетный эквивалент — час пайки и паутина проводов; шилд — тридцать секунд и ровные ряды. Для первого интерфейса устройства я считаю это честным путём: сначала работающий интерфейс на готовой плате, потом, если проект живёт, свой развод с ровно теми кнопками, которые реально нужны.
Лестница резисторов: пять кнопок — пять чисел
Механика — та же физика делителя, что в уроке про расчёт делителя напряжения, только вместо щупа — кнопки. Вход A0 подтянут к 5 В резистором 2 кОм; ниже — цепочка из 330 Ом, 620 Ом, 1 кОм и 3,3 кОм, и каждая кнопка замыкает вход на соответствующий узел этой цепочки. RIGHT закорачивает вход на землю совсем — отсчёт ноль; каждая следующая кнопка добавляет сопротивление снизу, и напряжение на входе растёт: UP порядка 131, DOWN порядка 306, LEFT порядка 478, SELECT порядка 719. Никто не нажат — вход подтянут вверх, около 1023.
Порядок резисторов не случаен: RIGHT стоит внизу лестницы, ближе всех к земле — под кнопкой ноль добавленного сопротивления, отсюда и ноль на входе. SELECT — вверху, с максимальной добавкой, потому и самое высокое напряжение из зон. Кнопки у моих экземпляров тугие, с щелчком; дребезг у них тот же, что у обычных тактовых, пять-десять миллисекунд, но на аналоговом входе он виден как прыжки числа между зонами — сравните печать сырого отсчёта при медленном и резком нажатии.
Числа — паспорт классического шилда DFR0009. Клоны живут своей жизнью: 5% резисторы дают разброс в десятки единиц, плюс шум АЦП в пару пунктов сверху. Поэтому табличные значения — ориентир, а не константы кода. Мой порядок такой: скетч печатает сырой отсчёт по каждой кнопке, я записываю свои пять чисел и беру порогами середины между соседними уровнями. Зазоры между уровнями — порядка 170-300 единиц, так что середина помещается с запасом в обе стороны.
Сырой отсчёт — единственное, что стоит печатать при калибровке: сами значения-пороги вынесете в код после. Как работает сам analogRead и откуда берётся шкала 0-1023 — разобрано в уроке про потенциометр и аналоговый вход; здесь вход тот же, просто вместо ручки — пальцы по кнопкам. Заодно прикиньте дрейф: нажмите одну и ту же кнопку десять раз и посмотрите разброс отсчёта — у живого шилда он в пределах пары-тройки пунктов, и это лучшая проверка контактов, чем взгляд на плату.
Таблица ниже — моя рабочая шпаргалка; колонку порогов заполняйте по своей калибровке, не по моей.
Отдельный грабёж времени — доверять порогам из чужой статьи на этапе запуска. Моя первая прошивка шилда начиналась с чужих констант, и кнопка LEFT лежала ровно на границе: в зоне 478-600 она читалась то LEFT, то SELECT в зависимости от температуры в комнате. Десять минут калибровки сняли вопрос навсегда; теперь в начале каждого проекта с шилдом — калибровка как ритуал, и только потом меню.
| Зона | Паспорт DFR0009 | Пороги моей калибровки |
|---|---|---|
| RIGHT | около 0 | ниже 70 |
| UP и DOWN | 131 и 306 | 70-220 и 220-390 |
| LEFT и SELECT | 478 и 719 | 390-600 и 600-850 |
| Никто не нажат | около 1023 | выше 850 |
#include <LiquidCrystal.h> LiquidCrystal lcd(8, 9, 4, 5, 6, 7); // раскладка классического шилда const int ladder = A0; // лестница кнопок шилда int zona = 5; // 5 - никого не нажато int zonaIzmeri(int v) { if (v < 70) { return 0; } // RIGHT if (v < 220) { return 1; } // UP if (v < 390) { return 2; } // DOWN if (v < 600) { return 3; } // LEFT if (v < 850) { return 4; } // SELECT return 5;} void setup() { Serial.begin(9600); lcd.begin(16, 2); lcd.setCursor(0, 0); lcd.print("KALIBRKA KNOPOK "); lcd.setCursor(0, 1); lcd.print("SMOTRI SERIAL "); Serial.println("ZHIMITE KNOPOKI PO OHEREDI");} void loop() { int v = analogRead(ladder); int z = zonaIzmeri(v); if (z != zona) { // печать на смене зоны: сырой поток zona = z; Serial.print("A0="); Serial.print(v); if (z == 0) { Serial.println(" RIGHT"); } if (z == 1) { Serial.println(" UP"); } if (z == 2) { Serial.println(" DOWN"); } if (z == 3) { Serial.println(" LEFT"); } if (z == 4) { Serial.println(" SELECT"); } if (z == 5) { Serial.println(" PUSTO"); } } delay(20);}Отдельная история — синяя кнопка в углу шилда. Она не пятая кнопка меню: RESET соединён с одноимённой ножкой платы, и нажатие перезагружает Arduino целиком. Первый месяц я по привычке жал её, чтобы выйти из экрана, и дивился, отчего счётчики обнуляются, а Serial переподключается. Клоны порой дребезжат этим контактом при надевании шилда — плата перезагружается в момент установки. Если кнопка мешает, её отгибают или выпаивают; я ограничился привычкой не трогать её вовсе и вынес сброс меню на обычную тактовую на D2. Заодно это ответ на вопрос, который приходит в почте регулярно: почему у шилда только четыре кнопки меню — потому что пятая занята перезагрузкой платы, а не моим упрямством.
Аналоговый дебаунс: зона держится, значит нажали
Цифровой кнопке хватает фронта и окна в 20-30 мс — мы это проходили на счётчике с антидребезгом. Аналоговой нужен ещё один слой: в момент замыкания и отпускания контакт дребезжит, и отсчёт за эти мгновения пробегает чужие зоны — с 1023 на 719 по пути можно успеть прочитать и 478, и 306. Лечение — подтверждение: сканирую вход раз в 20 мс и считаю нажатием только зону, продержавшуюся три-четыре чтения подряд, то есть 60-80 миллисекунд.
Второй слой — фронт по зоне: после подтверждения срабатывает переход из одной устойчивой зоны в другую, и только он. Кнопка не долбит меню десять раз, пока её держишь, — повтор приходит лишь от нового нажатия. Автомат повтора при удержании — отдельная надстройка, как в меню с кнопками вверх и вниз, если она вообще нужна проекту.
У подтверждения есть цена: реакция меню запаздывает на 60-80 миллисекунд — рука этого не замечает. Зато в поле, где на шильдике пыль, а контакты немолодые, подтверждение спасает от фантомов. Я видел меню, которое листало само от вибрации конвейера: там подтверждение ужесточили до пяти чтений и потребовали между зонами проход через пустую зону — около 1023. Сто миллисекунд на нажатие, зато ноль случайных входов в режим правки.
Одновременные нажатия лестница различить не сумеет: вход покажет узел ближе к земле, то есть RIGHT всегда перекрывает соседей. Это свойство схемы, а не дефект: два пальца на UP и RIGHT дают честный ноль. Планируйте интерфейс так, чтобы одновременность ничего не ломала — в меню она и не нужна.
Бюджет пинов шилда стоит знать наизусть: экран ест D4-D7 под обмен байтами, RS на D8, EN на D9, у части ревизий D10 — подсветка, A0 — кнопки. Остаются D2, D3, D11-D13 и A1-A5, а D0-D1 лучше не трогать — это Serial. С этой раскладкой уже не страшно добавлять зуммер и датчики; многострочные интерфейсы на освободившемся месте — тема урока про меню 20x4, а полное дерево экранов — урока про вложенное меню.
Для меню из пяти кнопок базового набора хватает: UP и DOWN двигают курсор, SELECT входит и подтверждает, LEFT выходит, RIGHT остаётся под своё — у меня он обычно резерв под быстрый вызов: закороченный на землю вход читается стабильнее остальных зон, ложных срабатываний у него меньше всех, а кнопка выхода — самое частое действие в меню.
#include <LiquidCrystal.h> LiquidCrystal lcd(8, 9, 4, 5, 6, 7); // раскладка классического шилда const int ladder = A0; // лестница кнопок шилдаconst int btn = 2; // внешняя кнопка сброса менюconst int led = 12; // лампа режима правки int item = 0;int edit = 0;int znach = 40; // значение в режиме правкиint zona = 5; // последняя зона: 5 - пустоint confirm = 0;int acted = 5; // зона, по которой уже сработалиunsigned long tick = 0;int prevBtn = HIGH; int zonaIzmeri(int v) { if (v < 70) { return 0; } // RIGHT if (v < 220) { return 1; } // UP if (v < 390) { return 2; } // DOWN if (v < 600) { return 3; } // LEFT if (v < 850) { return 4; } // SELECT return 5;} void printItem(int i) { if (i == 0) { lcd.print("START"); } if (i == 1) { lcd.print("POROG"); } if (i == 2) { lcd.print("SVET "); }} void printMenu() { lcd.setCursor(0, 0); lcd.print("MENYU "); lcd.print(item + 1); lcd.print("/3 "); lcd.setCursor(0, 1); lcd.print("> "); printItem(item); lcd.print(" ");} void printEdit() { lcd.setCursor(0, 0); lcd.print("VHOD: POROG "); lcd.setCursor(0, 1); lcd.print("> ZNACH "); lcd.print(znach); lcd.print(" ");} void knopika(int z) { if (edit == 0) { if (z == 1 && item > 0) { item = item - 1; printMenu(); } if (z == 2 && item < 2) { item = item + 1; printMenu(); } if (z == 4) { edit = 1; digitalWrite(led, HIGH); printEdit(); } } else { if (z == 1) { znach = znach - 10; if (znach < 30) { znach = 50; } printEdit(); } if (z == 2) { znach = znach + 10; if (znach > 50) { znach = 30; } printEdit(); } if (z == 3) { edit = 0; digitalWrite(led, LOW); printMenu(); Serial.println("BEZ ZAPISI"); } if (z == 4) { edit = 0; digitalWrite(led, LOW); Serial.print("ZAPISANO "); Serial.println(znach); printMenu(); } }} void setup() { Serial.begin(9600); pinMode(btn, INPUT_PULLUP); pinMode(led, OUTPUT); lcd.begin(16, 2); printMenu(); Serial.println("SHILD GO");} void loop() { int nowBtn = digitalRead(btn); if (prevBtn == HIGH && nowBtn == LOW) { item = 0; edit = 0; digitalWrite(led, LOW); printMenu(); Serial.println("RESET MENYU"); } prevBtn = nowBtn; unsigned long now = millis(); if (now - tick >= 20) { // скан лестницы раз в 20 мс tick = now; int v = analogRead(ladder); int z = zonaIzmeri(v); if (z == zona) { if (confirm < 4) { confirm = confirm + 1; } } else { zona = z; confirm = 0; if (z != 5) { Serial.print("A0="); Serial.print(v); Serial.print(" ZONA "); Serial.println(z); } } if (z == 5 && confirm >= 3) { acted = 5; } // отпускание подтвердилось if (confirm == 3 && z != 5 && z != acted) { // зона держится 60 мс acted = z; knopika(z); } }}- 1. Что читает analogRead(A0) шилда, пока ни одна кнопка не нажата?
- 2. Почему сравнение v == 131 ломается на клонах шилда?
- 3. Одновременно зажаты LEFT и RIGHT. Что прочитает лестница?
- 4. Как получают пороги для своего шилда?
- 5. Зачем нужно подтверждение тремя-четырьмя чтениями подряд?
- Лестница резисторов делит 5 В: паспортные отсчёты 0, 131, 306, 478, 719, пусто — около 1023
- Пороги — середины между уровнями своей калибровки; == с табличным числом спорит с допуском 5%
- Подтверждение тремя-четырьмя чтениями раз в 20 мс режет перескоки зон при дребезге и отпускании
- Одновременные нажатия различить нельзя: побеждает узел ближе к земле — RIGHT
- Шилд занимает D4-D7, D8, D9, часть ревизий D10 и A0: остальную периферию планируйте заранее
Частые вопросы по теме урока
- Что читает analogRead(A0) шилда, пока ни одна кнопка не нажата?
- Резистор 2 кОм тянет вход к питанию, и пустая зона — это около 1023. Ноль даёт только RIGHT, закорачивающий вход на землю. Около 1023 — вход подтянут к 5 В
- Почему сравнение v == 131 ломается на клонах шилда?
- Паспортные значения сняты с конкретной партии. Клоны с 5% резисторами дают разброс в десятки единиц, поэтому работают пороги-диапазоны из своей калибровки. Резисторы с допуском 5% уводят отсчёт на десятки единиц
- Одновременно зажаты LEFT и RIGHT. Что прочитает лестница?
- RIGHT закорачивает вход на землю напрямую, перекрывая всю цепочку. Одновременность лестница различить не может — это свойство схемы. Ближний к земле узел — RIGHT, около нуля
- Как получают пороги для своего шилда?
- Печатают сырые отсчёты каждой кнопки, записывают пять чисел и берут середины зазоров — там запас в 170-300 единиц на разброс и шум. Берут середины между уровнями своей калибровки
- Зачем нужно подтверждение тремя-четырьмя чтениями подряд?
- В момент замыкания и отпускания отсчёт пробегает чужие зоны. Подтверждение в 60-80 мс отсекает эти мгновенные выбросы. Оно режет перескоки по чужим зонам при дребезге и отпускании