Энкодер и Arduino: подключение KY-040, прерывания и код меню
Как подключить поворотный энкодер к Arduino: схема KY-040, квадратурное декодирование A/B, прерывания без пропуска шагов, антидребезг, меню на LCD, PCNT в ESP32 и Pico.
Поворотный энкодер — это цифровая ручка управления для проектов на Arduino: она вращается бесконечно, отсчитывает один шаг на каждый щелчок и работает как кнопка, если нажать на вал. Подключённый к Arduino энкодер ведёт себя как ручка меню 3D-принтера, регулятор осциллографа или ручка громкости в машине: он знает, на сколько щелчков и в какую сторону повёрнута ручка, и никогда не упирается в ограничитель. В большинстве наборов для хобби встречается пятипиновый модуль KY-040.
В этом руководстве ты узнаешь, чем KY-040 отличается от потенциометра, как направление декодируется по фазам A и B (квадратура), как подавить дребезг контактов аппаратно и программно и как с помощью прерываний посчитать каждый щелчок. Затем пошагово разберём быстрый путь с библиотекой Encoder, навигацию по меню на LCD и OLED, аппаратный счётчик PCNT в ESP32 и код на MicroPython для Raspberry Pi Pico.
Что такое поворотный энкодер и как он работает?
Внутри механического энкодера вроде KY-040 есть зубчатый контактный диск, который вращается вместе с валом, и два маленьких переключателя, скользящих по нему. При вращении вала оба переключателя по очереди замыкаются и размыкаются и формируют прямоугольные импульсы на пинах CLK (A) и DT (B). Две волны сдвинуты на четверть периода, то есть на 90 градусов; такой сигнал называется квадратурным. Микроконтроллер определяет направление по тому, какая волна меняется первой, а величину поворота — по количеству изменений. Энкодер не помнит своё положение, он сообщает только о движении. Поэтому KY-040 — это инкрементальный энкодер: при включении питания положение равно нулю, а весь подсчёт целиком ложится на твой код.
| Параметр | Значение для KY-040 |
|---|---|
| Тип | Механический, инкрементальный, выходы A/B |
| Фиксации (щелчки) | 20 щелчков на оборот (у некоторых энкодеров 30) |
| Импульсы | 20 полных циклов A/B на оборот, то есть 80 фронтов |
| Угловое разрешение | 18 градусов на щелчок |
| Пины | CLK (A), DT (B), SW (кнопка), + (питание), GND |
| Питание | 3,3–5 V (питает только подтягивающие резисторы) |
| Встроенные резисторы | Подтяжка 10 kΩ на CLK и DT; резистор для SW распаян не на каждом модуле |
| Кнопка | Нормально разомкнутая; нажатие на вал соединяет SW с GND (активный LOW) |
| Вращение | Без ограничений, нет упора |
Энкодер или потенциометр: в чём разница?
На первый взгляд энкодер очень похож на потенциометр: у обоих есть вращающийся вал, и оба служат для настройки значения. Но работают они совершенно по-разному. Потенциометр — это делитель напряжения; в зависимости от положения вала он выдаёт аналоговое напряжение от 0 до 5 V, которое читается через analogRead. Энкодер вместо напряжения выдаёт импульсы; он сообщает об изменении, а не о положении.

| Параметр | Потенциометр | Поворотный энкодер (KY-040) |
|---|---|---|
| Вращение | Примерно 270–300 градусов, ограничено | Без ограничений, непрерывно 360 градусов |
| Выход | Аналоговое напряжение (0–5 V) | Цифровые импульсы (A и B) |
| Чтение | analogRead (АЦП) | digitalRead или подсчёт с прерываниями |
| Положение | Абсолютное; положение вала даёт значение даже после отключения питания | Относительное; при включении положение неизвестно |
| Ощущение щелчков | Нет, вращается плавно | Есть, обычно 20 щелчков на оборот |
| Кнопка | Нет | Есть (нажатие на вал) |
| Типичная проблема | Треск и уплывающие со временем значения | Дребезг контактов (решается программно) |
| Типичное применение | Громкость, угол сервопривода, аналоговая регулировка | Навигация по меню, пошаговая настройка чисел, панели приборов |
Короче говоря, если значение должно один к одному соответствовать физическому положению вала (поворот рычага сервопривода вручную или простая регулировка громкости), потенциометр проще. Если нужно перемещаться по меню, точно по шагам увеличивать и уменьшать число, менять несколько настроек одной ручкой или ещё и менять значение из программы, энкодер гораздо гибче. Например, если значение 50 с телефона изменить на 80, то при вращении энкодера отсчёт просто продолжится с 80; у потенциометра же положение ручки и значение на экране перестанут совпадать.
Фазы A и B: как работает квадратурное декодирование
Когда KY-040 стоит в положении фиксации, оба переключателя разомкнуты, и благодаря подтягивающим резисторам на CLK и DT читается HIGH (1). Когда поворачиваешь ручку на один щелчок, обе линии по очереди падают в LOW и затем по очереди возвращаются в HIGH. По часовой стрелке первым падает CLK, против часовой — DT. Эта последовательность из четырёх состояний — код Грея: на каждом шаге меняется только один бит. В таблице ниже показано, что читают два пина за один щелчок:
| Шаг | По часовой стрелке (CLK, DT) | Против часовой стрелки (CLK, DT) |
|---|---|---|
| Покой в положении фиксации | 1, 1 | 1, 1 |
| 1-й переход | 0, 1 | 1, 0 |
| 2-й переход | 0, 0 | 0, 0 |
| 3-й переход | 1, 0 | 0, 1 |
| 4-й переход (следующий щелчок) | 1, 1 | 1, 1 |
Из таблицы следуют два простых правила. Первое: если в момент спада CLK линия DT ещё в HIGH, ручка вращается по часовой стрелке; если DT уже в LOW — против часовой. Второе: в одном щелчке четыре перехода, значит, 20 щелчков на оборот дают 80 переходов. Простой код использует только первое правило и считает один раз за щелчок. Надёжный код отслеживает все четыре перехода и поэтому может отсеивать дребезг или незавершённые движения. Какое направление считается «по часовой», зависит от модуля и подключения; если счётчик идёт в обратную сторону, достаточно поменять местами пины CLK и DT в коде.
Распиновка и подключение KY-040
| Пин KY-040 | Назначение | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|---|
| CLK | Фаза A | D2 | GPIO 25 | GP16 |
| DT | Фаза B | D3 | GPIO 26 | GP17 |
| SW | Кнопка (LOW при нажатии) | D4 | GPIO 27 | GP18 |
| + | Питание | 5V | 3V3 | 3V3(OUT) |
| GND | Земля | GND | GND | GND |
На Arduino Uno и Nano аппаратные прерывания есть только на пинах D2 и D3, поэтому CLK и DT лучше всего подключать именно к ним. На Mega пины прерываний — 2, 3, 18, 19, 20 и 21; на Leonardo — 0, 1, 2, 3 и 7. На ESP32 и Pico прерывание может вызвать почти любой GPIO. Карту пинов своей платы можно посмотреть в утилите распиновки. Для пина SW в коде используй INPUT_PULLUP: на некоторых модулях на этой линии нет резистора, а «висящий» пин читается так, будто кнопку нажимают случайным образом. У ESP32 пины GPIO 34–39 работают только на вход и не имеют встроенной подтяжки, поэтому не используй их для SW.
Первая проверка шаг за шагом: чтение опросом
- Подключи пин CLK модуля KY-040 к D2, DT — к D3, а SW — к D4.
- Подключи пин + к 5V, а GND — к GND Arduino.
- Вставь код ниже в Arduino IDE и загрузи его в плату.
- Открой монитор порта и выставь скорость 115200 бод.
- Поверни ручку на несколько щелчков в обе стороны, затем нажми на вал, чтобы сбросить счётчик.
1// KY-040: CLK -> D2, DT -> D3, SW -> D4, + -> 5V, GND -> GND
2const byte PIN_CLK = 2;
3const byte PIN_DT = 3;
4const byte PIN_SW = 4;
5
6int counter = 0;
7int lastClk;
8
9void setup() {
10 pinMode(PIN_CLK, INPUT_PULLUP); // modülde 10k pull-up zaten var / the module already has 10k pull-ups
11 pinMode(PIN_DT, INPUT_PULLUP);
12 pinMode(PIN_SW, INPUT_PULLUP); // basınca LOW okunur / reads LOW when pressed
13 Serial.begin(115200);
14 lastClk = digitalRead(PIN_CLK);
15}
16
17void loop() {
18 int clk = digitalRead(PIN_CLK);
19 if (clk != lastClk && clk == LOW) { // CLK düşen kenar / CLK falling edge
20 if (digitalRead(PIN_DT) == HIGH) { // DT hâlâ HIGH: saat yönü / DT still HIGH: clockwise
21 counter++;
22 Serial.print("CW ");
23 } else { // DT zaten LOW: ters yön / DT already LOW: counter-clockwise
24 counter--;
25 Serial.print("CCW ");
26 }
27 Serial.println(counter);
28 }
29 lastClk = clk;
30
31 if (digitalRead(PIN_SW) == LOW) { // mile basıldı / shaft pressed
32 delay(20); // buton sıçramasını bekle / wait out the button bounce
33 if (digitalRead(PIN_SW) == LOW) {
34 counter = 0;
35 Serial.println("Sifirlandi / reset");
36 while (digitalRead(PIN_SW) == LOW) {} // bırakılmasını bekle / wait for release
37 }
38 }
39}При каждом проходе loop код читает CLK и ловит только момент, когда CLK падает из HIGH в LOW. Если в этот момент DT ещё в HIGH, ручка повернулась по часовой стрелке; если в LOW — против часовой. Это самый понятный способ, но у него два слабых места. Во-первых, импульсы, пришедшие, пока loop занят другой работой (обновлением дисплея, delay, чтением датчиков), теряются, и при быстром вращении счётчик отстаёт. Во-вторых, механические контакты дребезжат: за одно падение CLK может несколько раз переключиться LOW-HIGH, и код может засчитать один щелчок как два или три. Следующие два раздела решают обе проблемы.
Дребезг контактов (антидребезг): аппаратные и программные решения
Металлические пружинки внутри энкодера, коснувшись контакта, несколько раз отскакивают, прежде чем успокоиться. В даташитах производителей это время обычно указано в несколько миллисекунд (например, максимум 2 мс). У кнопок дребезг устраняют правилом «подождать 20–50 мс после последнего изменения», но для энкодера это не годится: при быстром вращении между двумя фронтами проходит всего несколько миллисекунд, и долгое ожидание проглатывает и настоящие шаги. Вместо этого используют такие методы:
- Аппаратный фильтр: поставь конденсатор 10–100 nF между CLK и GND и ещё один между DT и GND. Вместе с подтяжками модуля на 10 kΩ они образуют RC-фильтр; постоянная времени лежит между 10 kΩ × 10 nF = 0,1 мс и 10 kΩ × 100 nF = 1 мс, и этого хватает, чтобы проглотить короткий дребезг. Рассчитать её можно в утилите постоянной времени RC, а расшифровать конденсатор с маркировкой 103 или 104 — в утилите кодов конденсаторов.
- Таблица состояний: на каждом фронте обеих линий старое и новое состояние AB сравниваются. Дребезг — это прыжки туда-обратно между одними и теми же двумя состояниями, которые по очереди дают +1 и −1 и взаимно гасятся. Переходы, при которых оба бита меняются одновременно, считаются недопустимыми и пропускаются.
- Решение только в положении фиксации: четвертьшаги накапливаются, а счётчик увеличивается или уменьшается, только когда энкодер встаёт в фиксацию (обе линии в HIGH). Так дрожащий на щелчке контакт не заставляет курсор меню прыгать.
- Блокировка для кнопки: пин SW ведёт себя как обычная кнопка; хватит блокировки на 30 мс с помощью millis().
Чтение с прерываниями, чтобы не терять ни одного шага
Прерывание заставляет процессор в момент изменения пина бросить текущую работу и выполнить короткую функцию (ISR). Если линии энкодера привязаны к прерываниям, ни один фронт не теряется, даже если в loop есть delay на 50 мс или дисплей обновляется медленно. Код ниже срабатывает на обоих фронтах CLK и DT, использует описанную выше таблицу состояний и считает, только когда ручка встаёт в фиксацию. Основы описаны также в статье Прерывания в Arduino.
1// KY-040: CLK -> D2 (INT0), DT -> D3 (INT1), SW -> D4, + -> 5V
2const byte PIN_CLK = 2;
3const byte PIN_DT = 3;
4const byte PIN_SW = 4;
5
6// İndeks: (eski AB << 2) | yeni AB / Index: (old AB << 2) | new AB
7const int8_t QEM[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
8
9volatile long position = 0; // tık sayısı, ISR ile loop paylaşır / detent count shared by ISR and loop
10uint8_t lastAB = 3; // son durum, 3 = ikisi de HIGH / last state, 3 = both HIGH
11int8_t quarter = 0; // iki tık arası çeyrek adımlar / quarter steps between detents
12
13void readEncoder() {
14 uint8_t ab = (digitalRead(PIN_CLK) << 1) | digitalRead(PIN_DT);
15 quarter += QEM[(lastAB << 2) | ab];
16 lastAB = ab;
17 if (ab == 3) { // tıka oturdu / settled in a detent
18 if (quarter > 1) position++; // saat yönü / clockwise
19 else if (quarter < -1) position--; // ters yön / counter-clockwise
20 quarter = 0;
21 }
22}
23
24bool buttonPressed() {
25 static bool lastState = HIGH;
26 static unsigned long lastChange = 0;
27 bool state = digitalRead(PIN_SW);
28 if (state != lastState && millis() - lastChange > 30) { // 30 ms kilit / 30 ms lockout
29 lastChange = millis();
30 lastState = state;
31 return state == LOW; // yalnızca basış anı / only the moment of pressing
32 }
33 return false;
34}
35
36void setup() {
37 pinMode(PIN_CLK, INPUT_PULLUP);
38 pinMode(PIN_DT, INPUT_PULLUP);
39 pinMode(PIN_SW, INPUT_PULLUP);
40 Serial.begin(115200);
41 lastAB = (digitalRead(PIN_CLK) << 1) | digitalRead(PIN_DT);
42 attachInterrupt(digitalPinToInterrupt(PIN_CLK), readEncoder, CHANGE); // her iki kenar / both edges
43 attachInterrupt(digitalPinToInterrupt(PIN_DT), readEncoder, CHANGE);
44}
45
46long lastPosition = 0;
47
48void loop() {
49 noInterrupts(); // 4 baytı güvenle kopyala / copy the 4 bytes safely
50 long pos = position;
51 interrupts();
52
53 if (pos != lastPosition) {
54 Serial.print(pos > lastPosition ? "CW " : "CCW ");
55 Serial.println(pos);
56 lastPosition = pos;
57 }
58
59 if (buttonPressed()) {
60 noInterrupts();
61 position = 0;
62 interrupts();
63 Serial.println("Sifirlandi / reset");
64 }
65
66 delay(50); // loop yavaş olsa da tık kaçmaz / no detent is lost even with a slow loop
67}| Часть кода | Что делает? |
|---|---|
| Таблица QEM[16] | Превращает старое и новое состояние AB в +1, −1 или 0; недопустимые переходы, когда обе линии меняются одновременно, считаются как 0 |
| attachInterrupt(..., CHANGE) (на оба фронта) | Запускает readEncoder() и на нарастающем, и на спадающем фронте CLK и DT |
| quarter (четвертьшаги) | Накапливает четвертьшаги между двумя щелчками; пары +1 и −1 от дребезга взаимно гасятся |
| if (ab == 3) (обе линии HIGH) | Решение принимается, только когда энкодер встаёт в фиксацию (обе линии в HIGH); накопленное значение +2 и больше засчитывается как один щелчок |
| volatile long position (общая переменная) | Переменная, общая для ISR и loop; volatile не даёт компилятору использовать устаревшую копию |
| noInterrupts() / interrupts() (защита при копировании) | Не даёт ISR вклиниться, пока копируется 4-байтовый long |
| buttonPressed() (кнопка) | Подавляет дребезг SW блокировкой на 30 мс и возвращает true только один раз, в момент нажатия |
Быстрый путь: библиотека Encoder
Если не хочется писать свою ISR, библиотека Encoder от Paul Stoffregen делает то же самое оптимизированным кодом. Открой «Менеджер библиотек» (Library Manager) в Arduino IDE, найди «Encoder» и установи библиотеку Paul Stoffregen; шаги установки описаны в руководстве Как добавить библиотеку в Arduino. Библиотека работает лучше всего, когда оба пина — пины прерываний, хорошо, когда прерывание есть только у первого пина, переданного в конструктор, и медленно, когда его нет ни у одного. Она считает каждый фронт, поэтому значение меняется на 4 за щелчок; чтобы получить число щелчков, раздели на 4.
1#include <Encoder.h>
2
3// Önce DT (3), sonra CLK (2): saat yönü artı sayılır / DT (3) first, then CLK (2): clockwise counts up
4Encoder knob(3, 2);
5const byte PIN_SW = 4;
6long lastDetent = 0;
7
8void setup() {
9 pinMode(PIN_SW, INPUT_PULLUP);
10 Serial.begin(115200);
11}
12
13void loop() {
14 long detent = knob.read() / 4; // 4 kenar = 1 tık / 4 edges = 1 detent
15 if (detent != lastDetent) {
16 lastDetent = detent;
17 Serial.println(detent);
18 }
19 if (digitalRead(PIN_SW) == LOW) { // basınca sıfırla / reset on press
20 knob.write(0);
21 }
22}read() возвращает общее число фронтов на данный момент, write(0) сбрасывает счётчик, а readAndReset() читает и сбрасывает за один вызов. В конструктор мы передаём сначала DT, потом CLK из-за знака направления: в таком порядке вращение по часовой стрелке считается положительным. Если у тебя получается наоборот, поменяй местами 2 и 3. Встроенные подтягивающие резисторы пинов библиотека тоже включает сама.
Навигация по меню на LCD с помощью поворотного энкодера

Меню — самое частое применение энкодеров: вращаешь ручку, чтобы переходить между строками, нажимаешь, чтобы перевести выбранную настройку в режим редактирования, вращаешь, чтобы изменить значение, и нажимаешь ещё раз, чтобы вернуться в меню. Пример ниже регулирует красный, зелёный и синий каналы RGB-светодиода от 0 до 255 и показывает значения на LCD-дисплее с I2C. Подключи LCD к A4 (SDA) и A5 (SCL), а цветовые ножки RGB-светодиода с общим катодом — к D9, D10 и D11 через резисторы 220 Ω.
1#include <Wire.h>
2#include <LiquidCrystal_I2C.h>
3
4LiquidCrystal_I2C lcd(0x27, 16, 2); // adres 0x27 ya da 0x3F / address 0x27 or 0x3F
5
6const byte PIN_CLK = 2, PIN_DT = 3, PIN_SW = 4;
7const byte COUNT = 3;
8const byte LED_PINS[COUNT] = {9, 10, 11}; // RGB LED, PWM pinleri / PWM pins
9const char* NAMES[COUNT] = {"Kirmizi/Red", "Yesil/Green", "Mavi/Blue"};
10int values[COUNT] = {0, 0, 0}; // 0-255 parlaklık / brightness
11
12const int8_t QEM[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
13volatile int steps = 0; // loop'un işlemediği tıklar / detents not yet handled by loop
14uint8_t lastAB = 3;
15int8_t quarter = 0;
16
17int selected = 0; // seçili satır / selected item
18bool editing = false; // değer düzenleme modu / value edit mode
19
20void readEncoder() {
21 uint8_t ab = (digitalRead(PIN_CLK) << 1) | digitalRead(PIN_DT);
22 quarter += QEM[(lastAB << 2) | ab];
23 lastAB = ab;
24 if (ab == 3) {
25 if (quarter > 1) steps++;
26 else if (quarter < -1) steps--;
27 quarter = 0;
28 }
29}
30
31bool buttonPressed() {
32 static bool lastState = HIGH;
33 static unsigned long lastChange = 0;
34 bool state = digitalRead(PIN_SW);
35 if (state != lastState && millis() - lastChange > 30) {
36 lastChange = millis();
37 lastState = state;
38 return state == LOW;
39 }
40 return false;
41}
42
43void drawMenu() {
44 int top = selected > COUNT - 2 ? COUNT - 2 : selected; // görünen ilk satır / first visible row
45 for (byte row = 0; row < 2; row++) {
46 int i = top + row;
47 char mark = ' ';
48 if (i == selected) mark = editing ? '*' : '>';
49 char line[17];
50 snprintf(line, sizeof(line), "%c%-11s%4d", mark, NAMES[i], values[i]);
51 lcd.setCursor(0, row);
52 lcd.print(line);
53 }
54}
55
56void setup() {
57 pinMode(PIN_CLK, INPUT_PULLUP);
58 pinMode(PIN_DT, INPUT_PULLUP);
59 pinMode(PIN_SW, INPUT_PULLUP);
60 for (byte i = 0; i < COUNT; i++) pinMode(LED_PINS[i], OUTPUT);
61 lcd.init();
62 lcd.backlight();
63 lastAB = (digitalRead(PIN_CLK) << 1) | digitalRead(PIN_DT);
64 attachInterrupt(digitalPinToInterrupt(PIN_CLK), readEncoder, CHANGE);
65 attachInterrupt(digitalPinToInterrupt(PIN_DT), readEncoder, CHANGE);
66 drawMenu();
67}
68
69void loop() {
70 noInterrupts();
71 int s = steps; // biriken tıkları al / take the pending detents
72 steps = 0;
73 interrupts();
74
75 if (s != 0) {
76 if (editing) {
77 values[selected] = constrain(values[selected] + s * 5, 0, 255); // tık başına 5 / 5 per detent
78 analogWrite(LED_PINS[selected], values[selected]);
79 } else {
80 selected = constrain(selected + s, 0, COUNT - 1);
81 }
82 drawMenu();
83 }
84
85 if (buttonPressed()) { // bas: düzenle ya da bitir / press: edit or finish
86 editing = !editing;
87 drawMenu();
88 }
89}В начале выбранной строки появляется знак >, а в режиме редактирования — знак *. Код энкодера такой же, как в предыдущем примере; единственное отличие в том, что ISR накапливает не положение, а число необработанных щелчков (steps), а loop читает его и обнуляет. Так что даже если loop какое-то время занят, ни один щелчок не теряется: накопленные обрабатываются все вместе на следующем проходе. В режиме редактирования каждый щелчок меняет значение на 5; для более тонкой настройки можно уменьшить шаг до 1 или добавить ускорение, которое увеличивает шаг при быстром вращении ручки.
Чтобы показать то же меню на OLED-дисплее, достаточно изменить функцию drawMenu(). Версия ниже рисует выбранную строку инверсными цветами на белой полосе. Вместо библиотеки LCD подключи библиотеки Adafruit SSD1306 и Adafruit GFX, удали объект lcd и в setup() вызови display.begin() вместо lcd.init().
1// LCD menü kodunda değişecek kısımlar / parts to change in the LCD menu sketch
2#include <Adafruit_GFX.h>
3#include <Adafruit_SSD1306.h>
4
5Adafruit_SSD1306 display(128, 64, &Wire, -1);
6// setup() içinde lcd.init() ve lcd.backlight() yerine / in setup() instead of lcd.init() and lcd.backlight():
7// display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
8
9void drawMenu() {
10 display.clearDisplay();
11 display.setTextSize(1);
12 for (byte i = 0; i < COUNT; i++) {
13 int y = i * 16;
14 if (i == selected) {
15 display.fillRect(0, y, 128, 14, SSD1306_WHITE); // seçili satır çubuğu / highlight bar
16 display.setTextColor(SSD1306_BLACK);
17 } else {
18 display.setTextColor(SSD1306_WHITE);
19 }
20 display.setCursor(4, y + 3);
21 display.print(NAMES[i]);
22 display.setCursor(98, y + 3);
23 display.print(values[i]);
24 if (i == selected && editing) display.print('*'); // düzenleme işareti / edit marker
25 }
26 display.display();
27}Поворотный энкодер и ESP32: аппаратный счётчик PCNT
В ESP32 есть аппаратный блок PCNT (Pulse Counter, счётчик импульсов), который считает импульсы, совсем не нагружая процессор. У каждого блока два канала, поэтому он может напрямую декодировать сигнал A/B; в классическом ESP32 8 блоков PCNT, в ESP32-S2 и S3 — 4. Счётчик 16-битный и включает фильтр помех (glitch filter), убирающий короткие помехи. Этот фильтр можно настроить максимум примерно на 12,8 микросекунды, поэтому он проглатывает только очень короткие всплески, а не весь дребезг контактов. Подсчёт по четырём фронтам сам уравновешивает остаток дребезга: колебания туда-обратно гасят друг друга как +1 и −1. Код ниже использует драйвер ESP-IDF, который входит в ядро Arduino ESP32 версии 3.x.
1#include "driver/pulse_cnt.h"
2#include "driver/gpio.h"
3
4// KY-040: CLK -> GPIO 25, DT -> GPIO 26, SW -> GPIO 27, + -> 3V3
5const int PIN_CLK = 25;
6const int PIN_DT = 26;
7const int PIN_SW = 27;
8
9pcnt_unit_handle_t unit = NULL;
10
11void setup() {
12 Serial.begin(115200);
13 pinMode(PIN_SW, INPUT_PULLUP);
14
15 pcnt_unit_config_t unitCfg = {};
16 unitCfg.low_limit = -10000; // sınıra varınca sayaç 0'a döner / the counter returns to 0 at a limit
17 unitCfg.high_limit = 10000;
18 ESP_ERROR_CHECK(pcnt_new_unit(&unitCfg, &unit));
19
20 pcnt_glitch_filter_config_t filterCfg = {};
21 filterCfg.max_glitch_ns = 1000; // 1 µs'den kısa iğneleri yok say / ignore spikes shorter than 1 µs
22 ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(unit, &filterCfg));
23
24 pcnt_chan_config_t aCfg = {};
25 aCfg.edge_gpio_num = PIN_CLK; // A kanalı: CLK kenarları, DT seviyesi / channel A: CLK edges, DT level
26 aCfg.level_gpio_num = PIN_DT;
27 pcnt_channel_handle_t chA = NULL;
28 ESP_ERROR_CHECK(pcnt_new_channel(unit, &aCfg, &chA));
29
30 pcnt_chan_config_t bCfg = {};
31 bCfg.edge_gpio_num = PIN_DT; // B kanalı: DT kenarları, CLK seviyesi / channel B: DT edges, CLK level
32 bCfg.level_gpio_num = PIN_CLK;
33 pcnt_channel_handle_t chB = NULL;
34 ESP_ERROR_CHECK(pcnt_new_channel(unit, &bCfg, &chB));
35
36 // 4x çözme: iki hattın tüm kenarları sayılır / 4x decoding: every edge of both lines is counted
37 ESP_ERROR_CHECK(pcnt_channel_set_edge_action(chA, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE));
38 ESP_ERROR_CHECK(pcnt_channel_set_level_action(chA, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));
39 ESP_ERROR_CHECK(pcnt_channel_set_edge_action(chB, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE));
40 ESP_ERROR_CHECK(pcnt_channel_set_level_action(chB, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));
41
42 gpio_pullup_en((gpio_num_t)PIN_CLK); // çıplak encoder için iç pull-up / internal pull-up for a bare encoder
43 gpio_pullup_en((gpio_num_t)PIN_DT);
44
45 ESP_ERROR_CHECK(pcnt_unit_enable(unit));
46 ESP_ERROR_CHECK(pcnt_unit_clear_count(unit));
47 ESP_ERROR_CHECK(pcnt_unit_start(unit));
48}
49
50int lastDetent = 0;
51
52void loop() {
53 int count = 0;
54 pcnt_unit_get_count(unit, &count);
55 int detent = count / 4; // 4 kenar = 1 tık / 4 edges = 1 detent
56 if (detent != lastDetent) {
57 lastDetent = detent;
58 Serial.printf("Tik / detent: %d (ham / raw: %d)\n", detent, count);
59 }
60 if (digitalRead(PIN_SW) == LOW) { // basınca sıfırla / reset on press
61 pcnt_unit_clear_count(unit);
62 }
63 delay(20);
64}Два канала настроены зеркально: канал A считает фронты CLK и меняет направление в зависимости от уровня DT, а канал B считает фронты DT относительно CLK. В результате счётчик меняется на 4 за щелчок. Дойдя до предела ±10000, он возвращается к нулю; если нужен более широкий диапазон, переполнения можно накапливать с помощью опции flags.accum_count и точек наблюдения (watch points). В ESP32-C3 нет блока PCNT; на этой плате используй Arduino-код на прерываниях. Если у тебя ядро Arduino ESP32 версии 2.x или хочется кода покороче, библиотека ESP32Encoder использует то же оборудование и справляется с задачей функциями attachFullQuad() и getCount(). Если на ESP32 используется MicroPython, класс machine.Encoder в свежих версиях (например, 1.27) тоже работает через PCNT. Если ESP32 для тебя в новинку, сначала прочитай статью Что такое ESP32.
Поворотный энкодер с Raspberry Pi Pico и MicroPython
На Pico прерывание может вызвать любой пин GPIO, поэтому читать энкодер в MicroPython через Pin.irq() просто. Код ниже использует ровно ту же таблицу состояний, что и пример для Arduino, и превращает энкодер в диммер для светодиода: каждый щелчок меняет яркость на 5 процентов, а нажатие на вал включает и выключает светодиод. Подключи CLK к GP16, DT к GP17, SW к GP18, а светодиод через резистор 220 Ω — к GP15. Как установить MicroPython, описано в статье Что такое Raspberry Pi Pico.
1from machine import Pin, PWM
2import time
3
4clk = Pin(16, Pin.IN, Pin.PULL_UP) # A fazı / phase A
5dt = Pin(17, Pin.IN, Pin.PULL_UP) # B fazı / phase B
6sw = Pin(18, Pin.IN, Pin.PULL_UP) # buton, basınca 0 / button, 0 when pressed
7led = PWM(Pin(15)) # LED + 220 ohm
8led.freq(1000)
9
10# İndeks: (eski AB << 2) | yeni AB / Index: (old AB << 2) | new AB
11QEM = (0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0)
12last_ab = (clk.value() << 1) | dt.value()
13quarter = 0
14position = 10 # 0-20 tık, başlangıç %50 / 0-20 detents, start at 50 %
15
16def on_edge(pin):
17 global last_ab, quarter, position
18 ab = (clk.value() << 1) | dt.value()
19 quarter += QEM[(last_ab << 2) | ab]
20 last_ab = ab
21 if ab == 3: # tıka oturdu / settled in a detent
22 if quarter > 1 and position < 20:
23 position += 1
24 elif quarter < -1 and position > 0:
25 position -= 1
26 quarter = 0
27
28clk.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
29dt.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
30
31on = True
32shown = -1
33while True:
34 if sw.value() == 0: # mile basıldı / shaft pressed
35 time.sleep_ms(20) # sıçramayı bekle / wait out the bounce
36 if sw.value() == 0:
37 on = not on
38 shown = -1 # yeniden yaz / force an update
39 while sw.value() == 0: # bırakılmasını bekle / wait for release
40 time.sleep_ms(10)
41 if position != shown:
42 shown = position
43 led.duty_u16(shown * 65535 // 20 if on else 0)
44 print("Parlaklik / brightness: %d%%" % (shown * 5), "acik / on" if on else "kapali / off")
45 time.sleep_ms(10)Границы значения (0–20 щелчков) мы проверяем внутри функции прерывания, поэтому основной цикл только читает переменную, и не возникает состояния гонки, когда обе стороны пишут одновременно. Как управлять яркостью светодиода, рассказано в статье Что такое ШИМ (PWM). Если нужно готовое решение, модуль rotary_irq_rp2 из проекта micropython-rotary умеет работать с границами, зацикливанием (wrap-around) и сменой направления. Для очень быстрых энкодеров моторов блок PIO в Pico может считать, совсем не нагружая процессор. Если MicroPython для тебя в новинку, хорошая отправная точка — статья Основы MicroPython.
Виды поворотных энкодеров: механические, оптические и магнитные
| Тип | Как работает? | Типичное разрешение | Где применяется? |
|---|---|---|---|
| Механический (KY-040, EC11) | Контактный диск и два переключателя | 12–30 щелчков на оборот | Ручки меню, регуляторы громкости, передние панели приборов |
| Оптический | Диск с прорезями, светодиод и фототранзистор | От сотен до тысяч импульсов на оборот | Станки с ЧПУ, суставы роботов, промышленные двигатели |
| Магнитный (на эффекте Холла) | Магнитный диск и датчики Холла | Несколько импульсов на оборот вала мотора (например, 7 или 11) | N20 и DC-моторы с редуктором и энкодером |
| Абсолютный | Выдаёт уникальный код для каждого положения | Например, 12 бит (4096 положений) у AS5600 | Руки роботов, измерение угла; знает своё положение при включении |
Код из этой статьи работает и с другими инкрементальными энкодерами с выходами A/B. Например, у DC-мотора с редуктором и энкодером датчик Холла, дающий 11 импульсов на оборот вала мотора, при подсчёте по четырём фронтам и передаточном отношении 1:30 даёт 11 × 4 × 30 = 1320 отсчётов на каждый оборот выходного вала. Когда частота импульсов доходит до тысяч в секунду, код на прерываниях забирает заметную долю времени процессора, а блок PCNT в ESP32 справляется с таким подсчётом без усилий. Промышленные оптические энкодеры чаще всего работают от 5–24 V и имеют выходы с открытым коллектором или дифференциальные; перед подключением обязательно проверь в даташите тип выхода и напряжение.
Идеи проектов с поворотным энкодером
- Цифровая ручка громкости: регулируй энкодером громкость музыки на DFPlayer Mini, а нажатием на вал переключай на следующий трек.
- Лампа с выбором цвета: меняй энкодером цветовой тон кольца из светодиодов WS2812B, а нажатием переходи в режим яркости.
- Точное позиционирование: пусть каждый щелчок поворачивает шаговый двигатель на заданное число шагов; так можно собрать слайдер для камеры или поворотный столик.
- Кухонный таймер: выставляй время энкодером, нажатием запускай обратный отсчёт, а когда время выйдет, включай зуммер.
- Игровой контроллер: используй энкодер как ракетку в Pong; в игровых проектах на Arduino он даёт более точное управление, чем джойстик.
- Настройка часов и будильника: выставляй время и будильник модуля часов реального времени (RTC) одной ручкой.
- Панель измерительного прибора: меняй мелкими шагами частоту ШИМ, значение генератора сигналов или целевую температуру паяльной станции.
Частые ошибки и как их исправить
| Проблема | Вероятная причина | Решение |
|---|---|---|
| Считает 2 или 4 за щелчок | Каждый фронт считается отдельно или дребезжат контакты | Раздели число фронтов на 4, используй таблицу состояний, добавь конденсаторы 10–100 nF на CLK и DT |
| Считает только каждый второй щелчок | Энкодер на 30 щелчков / 15 импульсов; каждый щелчок — это половина цикла | Дели значение библиотеки на 2, а не на 4; в коде с таблицей замени if (ab == 3) на if (ab == 3 || ab == 0) |
| Направление перепутано | CLK и DT перепутаны местами или модуль другой | Поменяй местами номера пинов CLK и DT в коде |
| При быстром вращении шаги пропускаются | Используется опрос, в loop есть delay | Используй прерывания или PCNT в ESP32, делай ISR короткой |
| Кнопка «нажимается» сама | Пин SW «висит» в воздухе, нет подтяжки | Используй pinMode(SW, INPUT_PULLUP) |
| Счётчик скачет случайным образом | Нет общей земли, длинные или плохо закреплённые провода | Объедини GND, укороти провода, проверь контакты макетной платы |
| fatal error: driver/pulse_cnt.h: No such file or directory (заголовочный файл не найден) / undefined reference to 'pcnt_new_unit' (функция не найдена при сборке) | Ядро Arduino ESP32 2.x (первая ошибка) или ESP32-C3, у которого нет блока PCNT (вторая ошибка) | Обнови ядро до 3.x в «Менеджере плат» (Boards Manager) или используй библиотеку ESP32Encoder; на C3 используй код с прерываниями |
| На LCD видны только прямоугольники | Неверная настройка контраста или I2C-адрес | Покрути подстроечный резистор на обратной стороне, попробуй адрес 0x3F вместо 0x27 |
Поворотный энкодер — один из самых дешёвых способов дать проекту профессиональный интерфейс: одна ручка и дисплей справятся с десятками настроек. Начни с кода на опросе, чтобы понять логику, затем переходи на версию с прерываниями или PCNT и адаптируй пример с меню под свой проект. В приложении BlueGrays есть проекты с управлением сервоприводом от потенциометра, I2C LCD, кнопкой и счётчиком на 7-сегментном индикаторе — со схемами подключения и пошаговым кодом; их легко улучшить, заменив потенциометр или кнопку энкодером.
Частые вопросы
Чем поворотный энкодер отличается от потенциометра?
Потенциометр выдаёт аналоговое напряжение, зависящее от положения вала, его поворот ограничен примерно 270–300 градусами, а читается он через analogRead. Поворотный энкодер вращается бесконечно и на каждом щелчке выдаёт цифровые импульсы; он сообщает направление и величину движения, а не положение. Энкодер лучше подходит для навигации по меню и пошаговых настроек, потенциометр — для простых регулировок по положению.
Почему энкодер считает два шага за щелчок или пропускает шаги?
Лишний счёт обычно вызван дребезгом контактов или тем, что каждый фронт считается отдельно; если разделить 4 фронта на щелчок на 4 и использовать таблицу состояний, проблема решается. Пропуски шагов бывают, когда импульсы приходят, пока loop занят. Подключи CLK и DT к пинам прерываний, читай их через прерывания и при необходимости добавь конденсаторы 10–100 nF — и проблема исчезнет.
Работает ли энкодер KY-040 от 3,3 V?
Да. KY-040 состоит только из механических переключателей и подтягивающих резисторов, поэтому отлично работает и от 3,3 V. На ESP32 и Raspberry Pi Pico всегда подключай пин + к 3V3; если питать модуль от 5 V, подтягивающие резисторы поднимут CLK и DT до 5 V, и предел входов на 3,3 V будет превышен.
Сколько импульсов на оборот даёт KY-040?
У распространённых модулей KY-040 20 щелчков на оборот, и линии A и B за один щелчок проходят один полный цикл. В каждом цикле четыре фронта, так что за оборот насчитывается 80 фронтов, а каждый щелчок равен 18 градусам. У некоторых энкодеров 30 щелчков и 15 импульсов; у них каждый щелчок соответствует половине цикла.
Можно ли читать поворотный энкодер без прерываний?
Да, его можно читать опросом, постоянно проверяя пин CLK внутри loop, и для медленно вращаемой ручки меню этого часто хватает. Но если в loop есть delay, обновление дисплея или долгие вычисления, быстрые щелчки будут теряться. Для надёжного результата рекомендуются прерывания, а на ESP32 — аппаратный счётчик PCNT.
Можно ли подключить к Arduino несколько поворотных энкодеров?
Да. На Uno аппаратные прерывания есть только на D2 и D3, поэтому с библиотекой Encoder можно подключить первый пин каждого энкодера (тот, что передаётся в конструктор первым) к пину прерывания, а второй — к обычному пину, или использовать прерывания по изменению состояния пина (pin change interrupts). У Mega шесть пинов прерываний, классический ESP32 может аппаратно считать 8 энкодеров своими 8 блоками PCNT, а на Pico прерывание может вызвать любой GPIO.
Что собрать после прочтения
Все проекты
Включение и выключение светодиода кнопкой (вкл/выкл)
Нажми кнопку один раз — свет загорится, нажми ещё раз — погаснет, совсем как выключатель в твоей комнате. Arduino замечает каждое нажатие и переводит светодиод в противоположное состояние, выжидая кор

Управление светодиодом кнопкой на ESP32 (переключение)
При каждом нажатии кнопки состояние светодиода меняется: если горел — гаснет, если не горел — загорается. Классический шаблон переключения. Антидребезг убирает механический шум кнопки, чтобы одно нажатие давало одно переключение.

Антидребезг кнопки на Pico: IRQ против программного опроса
Нажимаешь кнопку один раз, а код насчитывает 3–4? Это дребезг контактов — металлические контакты колеблются доли миллисекунды! Сравни в мониторе два способа подсчёта рядом: сырой опрос (ловит дребезг и считает лишнее) и IRQ + программный антидребезг (точно). Изучи физику дребезга, программный антидребезг (ticks_diff() > 50ms), регистрацию прерывания Pin.irq(), триггер IRQ_FALLING, обработчики ISR и безопасность global в ISR.

Управление сервоприводом потенциометром
Когда ты поворачиваешь ручку потенциометра, рычаг сервопривода поворачивается вместе с ней: одно крайнее положение ручки — ноль градусов, другое — пол-оборота. Arduino всё время читает положение ручки

Регулировка яркости светодиода потенциометром на ESP32 (аналог → PWM)
В этом проекте яркость светодиода плавно регулируется потенциометром. Пин АЦП ESP32 читает напряжение 0–3,3V с потенциометра, затем это значение превращается в сигнал PWM (широтно-импульсная модуляция) и подаётся на светодиод. В результате при повороте потенциометра светодиод постепенно становится ярче или тусклее. В Arduino Core 3.0+ можно сразу использовать analogWrite(); на ESP32-S2/S3 разрешение АЦП фиксировано — 12 бит.

Счётчик на семисегментном индикаторе с кнопками
Простой счётчик с тремя кнопками: нажми одну — цифра на индикаторе увеличится на единицу, нажми другую — уменьшится, а третья возвращает её к нулю. Одноразрядный семисегментный индикатор считает от 0

LCD-экран с I2C
Дай своему Arduino собственный экран и заставь его написать первое сообщение. Двухстрочный LCD-экран подключается всего несколькими проводами; в верхней строке появляется приветствие, затем второе соо

Игра в пинг-понг — SSD1306
Мяч, две ракетки и ты: на маленьком OLED-экране ты играешь в настольный теннис — одну из самых первых видеоигр. Две кнопки двигают твою ракетку справа вверх и вниз, а левой ракеткой управляет компьюте
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

