Что такое энкодер
Роторный энкодер — ручка-крутилка как на магнитоле: крутится в обе стороны без конца, щёлкает по фиксированным позициям, часто имеет встроенную кнопку (нажали — щёлк). KY-040 — популярный модуль: энкодер + подтяжки + кнопка, 5 выводов.
Принцип: при повороте два сигнальных контакта (CLK и DT) выдают прямоугольные импульсы со сдвигом фазы. По тому, какой канал «поднялся» первым — определяем направление. Кнопка — отдельный вывод SW.
Подключение KY-040
KY-040 + ── 5V
KY-040 GND ── GND
KY-040 CLK ── Arduino D2 (прерывание!)
KY-040 DT ── Arduino D3 (прерывание)
KY-040 SW ── Arduino D4 (кнопка)
CLK и DT лучше сажать на пины с прерываниями (2 и 3 на Uno) — тогда поворот ловится мгновенно, даже когда loop занят. Подтяжки на модуле уже есть — пины настраиваем как обычные INPUT.
Чтение без прерываний — простой опрос
const int CLK = 2, DT = 3;
int lastClk;
void setup() {
pinMode(CLK, INPUT);
pinMode(DT, INPUT);
lastClk = digitalRead(CLK);
Serial.begin(9600);
}
void loop() {
int clk = digitalRead(CLK);
if (clk != lastClk) { // произошёл фронт
if (digitalRead(DT) != clk) {
Serial.println("по часовой");
} else {
Serial.println("против часовой");
}
}
lastClk = clk;
}
Работает, но теряет быстрые повороты — если крутануть резко, импульсы пропадут между опросами.
Чтение по прерываниям — надёжно
const int CLK = 2, DT = 3;
volatile int counter = 0;
volatile bool changed = false;
void isrClk() {
if (digitalRead(CLK) == digitalRead(DT)) counter--;
else counter++;
changed = true;
}
void setup() {
pinMode(CLK, INPUT);
pinMode(DT, INPUT);
attachInterrupt(digitalPinToInterrupt(CLK), isrClk, CHANGE);
Serial.begin(9600);
}
void loop() {
if (changed) {
noInterrupts();
int c = counter;
changed = false;
interrupts();
Serial.println(c);
}
}
Прерывание по CHANGE на CLK срабатывает на каждом фронте. counter — volatile (изменяется в прерывании), чтение — под noInterrupts, чтобы не поймать полуслово.
Каждый щелчок KY-040 даёт 4 перехода — делите на 4 или фильтруйте, чтобы «один щелчок = один шаг»:
int steps = counter / 4; // полный щелчок = 4 перехода
Кнопка энкодера
const int SW = 4;
pinMode(SW, INPUT_PULLUP);
if (digitalRead(SW) == LOW) { // нажата
// обработка — с антидребезгом
}
Практическое применение: регулятор громкости
int volume = 50; // 0..100
void loop() {
if (changed) {
int delta = counter; counter = 0; changed = false;
volume = constrain(volume + delta / 4, 0, 100);
Serial.println(volume);
}
}
Меню на энкодере
Классический сценарий — навигация по пунктам:
const char* items[] = {"Настройки", "Яркость", "Таймер", "Выход"};
const int N = 4;
int pos = 0;
void loop() {
if (changed) {
pos += counter / 4; counter = 0; changed = false;
pos = (pos % N + N) % N; // циклически
showItem(pos);
}
if (digitalRead(SW) == LOW) { // выбор
delay(30); // антидребезг
if (digitalRead(SW) == LOW) enterItem(pos);
}
}
Типичные проблемы
- Пропуски шагов: опрос без прерываний не успевает — переходите на
attachInterrupt. - Один щелчок = 4 шага: делите на 4 или фильтруйте по полному циклу.
- Счётчик гуляет при отпускании: у дешёвых энкодеров дребезг контактов — добавьте аппаратный фильтр (RC на CLK/DT) или программный дебаунс 2–5 мс.
- Направление перепутано: поменяйте CLK и DT местами или знак счётчика.
- Кнопка читает наоборот:
INPUT_PULLUP— нажатие это LOW.
Итог
Энкодер — отличный ввод для меню и регулировок: CLK/DT на прерывания, кнопка на INPUT_PULLUP, делить на 4 перехода за щелчок, volatile для счётчика и антидребезг для кнопки. С этим строят регуляторы громкости, настройки температуры, навигацию по экрану — привычный «крутилочный» интерфейс.
Полный проект: энкодер как регулятор
const int CLK = 2, DT = 3, SW = 4;
volatile int counter = 0;
int value = 0;
bool lastSw = HIGH;
void isr() {
if (digitalRead(CLK) == digitalRead(DT)) counter--;
else counter++;
}
void setup() {
pinMode(CLK, INPUT);
pinMode(DT, INPUT);
pinMode(SW, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(CLK), isr, CHANGE);
Serial.begin(9600);
}
void loop() {
noInterrupts();
int delta = counter;
counter = 0;
interrupts();
value = constrain(value + delta / 4, 0, 100); // 4 перехода за щелчок
if (delta) Serial.println(value);
bool sw = digitalRead(SW);
if (sw == LOW && lastSw == HIGH) { // нажали
Serial.println("клик");
}
lastSw = sw;
}
Меню на энкодере
const char* items[] = {"Назад", "Яркость", "Температура", "Режим"};
int pos = 0;
void loop() {
if (delta) {
pos += delta / 4;
pos = (pos % 4 + 4) % 4;
lcd.clear();
lcd.print(items[pos]);
}
if (clicked()) enterItem(pos);
}
Энкодер с точной позицией
Для задач «крутилка знает свою позицию» (например, вал управления двигателем) берут энкодеры с Z-каналом (индексный импульс на оборот) или абсолютные энкодеры — I2C/SPI интерфейс, выдают угол напрямую.
Проблемы и решения
- Шагов больше чем щелчков: один щелчок = 4 перехода — делите на 4.
- Пропуски при быстрой крутке: опрос без прерываний не успевает —
attachInterruptна CLK. - Дребезг: RC-цепь на CLK/DT или программный дебаунс.
- Направление перепутано: поменяйте CLK и DT местами или знак.
- ESP32: используйте
PCNTаппаратный счётчик импульсов — лучше программного подсчёта.
Альтернативы
- Потенциометр — проще, но не бесконечное вращение.
- Энкодер с абсолютной позицией — AS5600 (I2C) для точного угла.
- Сенсорная крутилка — для премиальных интерфейсов.
Полный проект: энкодер + LCD
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
const char* items[] = {"Назад", "Яркость", "Температура", "Режим"};
int pos = 0;
void loop() {
if (encoderDelta()) {
pos = (pos + encoderDelta() + 4) % 4;
lcd.clear();
lcd.print(">");
lcd.print(items[pos]);
}
if (buttonPressed()) enterItem(pos);
}
Энкодер с абсолютной позицией
Для задач «крутилка знает свой угол» — AS5600 (I2C, 12 бит) или энкодеры с Z-каналом. KY-040 — инкрементальный, не знает абсолютную позицию.
Альтернативы
- Потенциометр — проще, но не бесконечное вращение.
- AS5600 — абсолютный угол по I2C.
- Сенсорная крутилка — для премиальных интерфейсов.