Энкодер KY-040 на Arduino: вращение, нажатие, меню
Потенциометр — это угол ручки: у него упор на краях, и меню из двадцати пунктов им не прокрутить. Энкодер — это счётчик щелчков: ручка крутится без упора хоть по кругу весь день, а внутри ещё и кнопка. Любой прибор с настройками — будильник, поливалка с порогом, меню на дисплее — становится удобнее с одной крутёлкой вместо трёх кнопок.
Ниже — пины KY-040, зачем всем трём подтяжка, как серый код квадратуры различает направление, скетч меню на attachInterrupt и проверка в браузере. «Поворотный энкодер KY-040» лежит в палитре «Макетка» нашего симулятора: ручка крутится мышкой, серый код идёт на CLK/DT, кнопка SW нажимается с удержанием.
Энкодер или потенциометр: кто для чего
Потенциометр даёт абсолютный угол на analogRead — минимум кода, и видно позицию сразу после включения. Но диапазон 0-270 градусов с упорами: выбор из 20 пунктов, настройка времени, прокрутка длинного списка — это не про него. Энкодер считает щелчки и не имеет крайних положений; цена вопроса — код: щелчки надо считать самому, и считать аккуратно. Оба компонента лежат в палитре «Макетка» симулятора, так что сравнить их можно без корзины маркетплейса. Для задач «покрутил и отпустил» (яркость, громкость) потенциометр проще; для меню и ввода значений энкодер сильнее. Поиграть с потенциометром можно в симуляторе потенциометра — разница чувствуется за минуту.
Пины KY-040: CLK, DT и SW
| Пин | Что несёт | Куда на Uno | Как читается |
|---|---|---|---|
| CLK | фронт на каждый шаг квадратуры | D2 | attachInterrupt, режим CHANGE |
| DT | фаза — говорит, в какую сторону крутят | D3 | digitalRead |
| SW | кнопка под ручкой | D4 | digitalRead при INPUT_PULLUP |
| VCC, GND | питание 3,3-5 В | 5V и GND | — |
Подтяжка обязательна для всех трёх линий: внутри модуля стоят обычные механические контакты на землю, и без INPUT_PULLUP пины висят в воздухе, ловя помехи от рук и проводов. При INPUT_PULLUP покой — это HIGH, а замыкание на землю даёт LOW: нажатый SW читается как 0.
Серый код квадратуры: почему два провода, а не один
Один контакт с импульсами не различает направление: щелчок вправо и влево выглядят одинаково. Поэтому в энкодере две линии, сдвинутые на четверть цикла: на один щелчок приходится четыре фазы пары CLK/DT — (0,0), затем (1,0), затем (1,1), затем (0,1). Крутя по часовой, CLK щёлкает раньше DT; против часовой — наоборот. Плата смотрит, какая линия изменилась первой, и знает сторону. В симуляторе фазы честные: на каждый щелчок CLK и DT проходят весь цикл, и каждый фронт дёргает ваше прерывание.
ISR на CHANGE и антидребезг SW
Вращение ловим прерыванием, а не опросом: loop() занят экраном или поливалкой, и щелчки не должны теряться. На Uno внешние прерывания живут на D2 и D3, поэтому CLK — на D2: attachInterrupt(digitalPinToInterrupt(2), isr, CHANGE). В ISR живут только короткие операции: digitalRead и счётчик; volatile обязателен, Serial и delay там запрещены. Полный разбор — в уроке про прерывания и уроке про энкодер на прерываниях. Кнопка SW — обычная механика с дребезгом 5-10 мс, и лечится она как любая кнопка: проверка фронта в loop(), при необходимости пауза или библиотека. Четыре рабочих способа собраны в уроке про антидребезг; в скетче ниже — простейший, через сравнение с прошлым состоянием.
Скетч: меню с позицией 0-24
В симуляторе карточка «Энкодер KY-040» собирается сама, как только скетч вешает attachInterrupt с режимом CHANGE: ручка-крутёлка, кнопки влево-вправо, SW с удержанием и слайдер позиции 0-24. Одна честная деталь: в симуляторе щелчок прикладывается пачкой фаз за один кадр, и ISR читает линии уже после щелчка — поэтому все щелчки считаются в одну сторону, позиция меняется ровно на единицу за щелчок. На живой плате фазы разнесены на микросекунды, и тот же код различает направление как в учебнике.
const byte PIN_CLK = 2; // CLK — пин прерывания на Unoconst byte PIN_DT = 3; // DT — фаза направленияconst byte PIN_SW = 4; // SW — кнопка под ручкой volatile int shagi = 0; // приращение из ISR: фронтов на щелчок — два void isr() { if (digitalRead(PIN_CLK) == digitalRead(PIN_DT)) shagi--; else shagi++;} void setup() { pinMode(PIN_CLK, INPUT_PULLUP); pinMode(PIN_DT, INPUT_PULLUP); pinMode(PIN_SW, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PIN_CLK), isr, CHANGE); Serial.begin(9600);} int poz = 0; // позиция меню: 0..24, кольцомbool swByla = HIGH; // прошлое состояние SW — ловим фронт нажатия void loop() { if (shagi != 0) { noInterrupts(); // копия счётчика под защитой int s = shagi; shagi = 0; interrupts(); poz = (poz + s / 2 + 25) % 25; // два фронта на щелчок, кольцо 0-24 Serial.print("poz="); Serial.println(poz); } int fazaDt = digitalRead(PIN_DT); // DT читаем до SW: порядок важен для симулятора bool sw = digitalRead(PIN_SW); if (sw == LOW && swByla == HIGH) { // нажали: фронт, а не удержание Serial.print("vybran punkt "); Serial.println(poz); } swByla = sw; delay(5);}Что вы увидите в мониторе порта: poz меняется на единицу за щелчок ручки, а по нажатию SW печатается выбранный пункт. Дальше пункт меню — это switch: экран, порог поливалки, режим гирлянды. Для подтверждения выбора пикните пассивным зуммером, а паузы между проверками держите на millis() — почему delay() в меню вреден, разобрано в лендинге millis() против delay().