На пульте моего фильтра для аквариума было три кнопки и пять настроек: обороты насоса, температура, подсветка, время кормления, порог тревоги. Кнопки справлялись, но руки превращались в пинг-понг: листаешь — тыкаешь — листаешь — тыкаешь, а к пятой настройке я путался, какая кнопка что значит в этот раз. Энкодер KY-040 вычищает этот пинг-понг одной ручкой: крутишь — курсор ходит, нажал — выбрал, и всё это не глядя на подписи. Расплата одна: ручка не показывает положение, и первые минуты на новом приборе глаз ищет отметку «ноль» — через неделю привыкаешь и перестаёшь замечать. У энкодерных меню есть и своя классика: типовой проект на Arduino Project Hub крутит ручкой три переменные аналоговых выходов — вращение меняет значение выделенного пункта, нажатие переключает режим. Мы пойдём тем же путём, но поверх меню из прошлого урока, где каркас уже собран на трёх кнопках.
У KY-040 три вывода, и это не аналоговая ручка, а счётчик щелчков. CLK даёт фронт на каждый щелчок — у этой модели 20 щелчков на оборот; DT — второй контакт квадратуры, чей уровень в момент фронта говорит направление: HIGH — по часовой, LOW — против. SW — обычная кнопка под ручкой, замыкает на землю. Подтяжки на модуле стоят, по 10 кОм на линию, поэтому скетчу хватает pinMode с INPUT для CLK и DT и INPUT_PULLUP для SW — на случай модуля без обвязки. Если вы пропустили навигацию по меню кнопками, начните с неё — там собран каркас кадра, который здесь предполагается готовым.
Схема кода меняется по существу в одном месте: CLK уходит в прерывание по фронту RISING, DT читается внутри обработчика одним digitalRead, а SW опрашивается в loop с окном 40 мс, как обычная кнопка. Почему CLK именно в прерывание: при бодром кручении щелчки идут пачкой до двадцати в секунду, и loop с перерисовкой на борту глотает фронты — курсор теряет шаги именно тогда, когда вы крутите быстрее всего. Зажимы границ в обработчике — тоже не случайность: ручка не умеет «стоять на краю», щелчки в сторону края продолжают сыпаться, и без зажимов счётчик уезжал бы в минус.
CLK, DT, SW: кто за что отвечает
Adafruit_SSD1306 display(128, 64, &Wire, -1); const int CLK = 2;const int DT = 3;const int SW = 4; String punkty[3];int kurzor = 0;int vybor = -1;int lastKurz = -1;int lastSw = HIGH;unsigned long tSw = 0; void tick() { // обработчик щелчка: только счёт и зажимы, никакой отрисовки if (digitalRead(DT) == HIGH) { if (kurzor < 2) { kurzor++; } } else { if (kurzor > 0) { kurzor--; } }} void risujMenu() { display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); for (int i = 0; i < 3; i++) { display.setCursor(12, i * 8); display.print(punkty[i]); if (i == kurzor) { display.setCursor(0, i * 8); display.print(">"); } } display.setCursor(0, 56); if (vybor < 0) { display.print("Krutite ruchku; SW - vybor"); } else { display.print("VYBOR: "); display.print(punkty[vybor]); } display.display();} void setup() { pinMode(CLK, INPUT); pinMode(DT, INPUT); pinMode(SW, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(CLK), tick, RISING); Serial.begin(9600); punkty[0] = "Start ventilyatora"; punkty[1] = "Podsvetka: vikl"; punkty[2] = "Sbros nastroyek"; risujMenu();} void loop() { if (kurzor != lastKurz) { lastKurz = kurzor; risujMenu(); Serial.print("kurzor="); Serial.println(kurzor); } int sw = digitalRead(SW); if (sw != lastSw && millis() - tSw > 40) { tSw = millis(); lastSw = sw; if (sw == LOW) { vybor = kurzor; risujMenu(); Serial.print("vybor="); Serial.println(vybor); } } delay(5);}В обработчике прерывания только две операции и зажимы границ — ни print, ни перерисовки: loop замечает, что kurzor уехал, и рисует кадр сам. Разделение «прерывание считает, loop рисует» мы уже разбирали на адресной ленте, и здесь оно то же самое: перерисовка на I2C занимает миллисекунды, и внутри ISR она проглотила бы следующие щелчки. Про сами прерывания, приоритеты и digitalPinToInterrupt — в уроке про attachInterrupt.
SW опрашиваю именно в loop, а не в прерывании: нажатие — событие человеческого темпа с удержанием в полсекунды, и окно 40 мс из прошлого урока закрывает его дребезг без отдельного кода. Вторая деталь в loop — перерисовка по факту: кадр уходит на стекло только когда сменились kurzor или vybor, а не каждые 50 миллисекунд. Экран не мигает, шина не гоняет лишний трафик; про то, как часто вообще можно перерисовывать кадр без мерцания, — в уроке про анимацию без мерцания. Цена вопроса измеримая: полный кадр 128×64 — это килобайт по шине, на быстрой шине 400 кГц он уезжает около тридцати миллисекунд, на стандартных 100 кГц — втрое-вчетверо дольше. Слать такой кадр после каждого щелчка в пачке — значит выстраивать очередь из щелчков; одна перерисовка на пачку держит отклик живым.
Кнопки против ручки
| Критерий | Три кнопки | Энкодер KY-040 |
|---|---|---|
| Пины | 3 цифровых | 3 цифровых: CLK, DT, SW |
| Прокрутка списка | щелчок за щелчком, по пункту | непрерывно, до 20 щелчков на оборот |
| Выбор пункта | отдельная кнопка | нажатие на саму ручку (SW) |
| Ввод значения 0-100 | долгое тыкание по кнопкам | щелчок равен шагу, оборот — сотне |
| Лучшее применение | 2-3 настройки, простой прибор | 5 и больше настроек, меню с подэкранами |
Подключение на макетке — пять проводов: GND, плюс питания, CLK на D2, DT на D3 и SW на D4. Распиновка на модуле подписана, но не всегда в том порядке, как на картинке магазина: сверяйтесь с надписями на плате, у KY-040 они встречаются в двух вариантах. Ток крошечный — подтяжки берут миллиамперы, ручке хватает питания от пина 5 В. И нюанс: у части партий модуля подтяжки стоят только на CLK и DT, а SW идёт без них, поэтому INPUT_PULLUP на SW в скетче стоит всегда — это дешевле, чем гадать по партии. Механика у модуля приятная: каждая ступень щёлкает с фиксацией, палец чувствует шаг, и уже по звуку понятно, что счёт идёт. CLK обязан сидеть на D2: на Uno прерывания принимают только D2 и D3, а DT и SW свободно переезжают на другие пины правкой констант. Кадр, как всегда у Adafruit, собирается в буфере и уходит одним display() — цену этой механики считали раньше; в меню она виднее всего, потому что кадры здесь редкие и точечные.
В симуляторе и на плате вы поймаете одну и ту же вещь: ручка не спрашивает, где верх и где низ. Кнопки — это карта «вверх-вниз», которую надо держать в голове; поворот — жест без карты, как качелька громкости в машине. Отсюда и главная эргономика: в приборе с подэкранами одна рука держит корпус, вторая крутит, и никакой акробатики пальцами. Скорость тоже за ручкой: список из десяти пунктов кнопками — десять нажатий, ручкой — пол-оборота, а 20 щелчков на оборот бодрое запястье делает за секунду.
Чего у ручки нет — памяти о положении. Энкодер считает щелчки, а не угол: выключили питание, включили — kurzor стартует с нуля, хоть стрелка физически и стоит посередине. Кнопочное меню забывает позицию с тем же успехом, так что здесь паритет; а вот для значений вроде уставки температуры ручка выигрывает всухую — щелчок равен градусу, значение правится без всякой клавиатуры. Чтобы значения и позиции переживали выключение, их выносят в энергонезависимую память: с первого байта это разобрано в уроке про чтение и запись EEPROM.
Что попробовать в симуляторе, чтобы почувствовать разницу с кнопками: крутите ручку быстро — курсор бежит по списку без повторных нажатий, и каждый щелчок напечатан в мониторе строкой kurzor=, потерянных шагов нет. Остановитесь ровно на краю списка: зажим сработает, и лишние щелчки не уводят курсор в пустоту. На макетке к этому добавится звон контактов: если курсор начнёт прыгать через пункт, рецепт уже лежит в рамке выше — окно в обработчике или конденсатор вдоль CLK. И не потеряйте статусную строку: ручка без неё слепа, половина вопросов «а выбралось ли» снимается одной строкой VYBOR в нижней строке кадра.
Отдельный случай — щелчок во время перерисовки. Обработчик прерывания срабатывает всегда, счёт идёт, а loop после отрисовки берёт последнее значение: курсор оказывается там, куда докрутили, ни один шаг не пропал. Именно поэтому счёт живёт в переменной, а не в кадре: экран — витрина, а не склад. С кнопками такой гонки почти нет — нажатия редкие, — а ручкой вы будете крутить во время отрисовки постоянно, и каркас обязан переживать это без потерь.
- 1. Что подсказывает направление вращения при схеме «фронт CLK + чтение DT»?
- 2. Сколько щелчков на полный оборот у KY-040?
- 3. Почему CLK читается в прерывании, а не в loop?
- 4. Курсор прыгает через пункт на реальной плате, но не в симуляторе. Виновник?
- 5. Где в скетче обрабатывается нажатие SW?
- KY-040: CLK — фронт на щелчок, DT — направление, SW — кнопка под ручкой; 20 щелчков на оборот, подтяжки 10 кОм на модуле
- CLK — в прерывание по RISING, DT — одним digitalRead в обработчике; в ISR только счёт и зажимы
- SW опрашивается в loop с окном 40 мс: человеческий темп не требует прерываний
- Перерисовка по факту смены kurzor или vybor: экран не мигает, шина не гоняет лишний трафик
- Кнопки честны для 2-3 настроек; пять и больше параметров, ввод значений — территория ручки
Частые вопросы по теме урока
- Что подсказывает направление вращения при схеме «фронт CLK + чтение DT»?
- Внутри обработчика одним digitalRead(DT): HIGH — по часовой, LOW — против. Сам CLK только сообщает, что щелчок случился. Уровень на DT в момент фронта
- Сколько щелчков на полный оборот у KY-040?
- Двадцать щелчков на оборот: при вводе значения шагом в единицу один оборот — это диапазон в двадцать шагов. 20
- Почему CLK читается в прерывании, а не в loop?
- Перерисовка на I2C занимает миллисекунды: пачка щелчков до двадцати в секунду теряет шаги. ISR делает только счёт. При бодром кручении loop с перерисовкой глотает фронты
- Курсор прыгает через пункт на реальной плате, но не в симуляторе. Виновник?
- Контакты CLK механические: один щелчок даёт два фронта. Лечение — окно в ISR или конденсатор 100 нФ вдоль CLK. Дребезг механических контактов ручки
- Где в скетче обрабатывается нажатие SW?
- SW — обычная кнопка человеческого темпа: фронт с окном 40 мс в loop. Прерывание ей не нужно. В loop, по фронту с окном 40 мс