Rotary Encoder Arduino: Pengkabelan KY-040, Interrupt, dan Kode Menu
Cara memakai rotary encoder dengan Arduino: pengkabelan KY-040, decoding quadrature A/B, interrupt tanpa langkah terlewat, debounce, menu LCD, PCNT ESP32, dan Pico.
Rotary encoder adalah kenop pengatur digital untuk proyek Arduino yang bisa diputar tanpa batas, menghitung satu langkah di setiap klik, dan berfungsi sebagai push button ketika porosnya ditekan. Ketika dihubungkan ke Arduino, rotary encoder bekerja seperti kenop menu di printer 3D, kenop pengatur di osiloskop, atau kenop volume di mobil: ia tahu berapa klik kamu memutarnya dan ke arah mana, dan tidak pernah mentok di ujung. Model yang paling sering kamu temukan di kit hobi adalah modul KY-040 berkaki lima.
Di panduan ini kamu akan mempelajari bedanya KY-040 dengan potensiometer, cara arah putaran dibaca dari fase A dan B (quadrature), cara menyaring pantulan kontak lewat hardware dan software, serta cara menghitung setiap klik tanpa terlewat dengan interrupt. Setelah itu kita akan membuat jalan pintas dengan library Encoder, navigasi menu di LCD dan OLED, penghitung hardware PCNT milik ESP32, dan kode MicroPython untuk Raspberry Pi Pico, langkah demi langkah.
Apa itu rotary encoder dan bagaimana cara kerjanya?
Di dalam encoder mekanis seperti KY-040 ada piringan kontak bergerigi yang ikut berputar bersama poros dan dua saklar kecil yang bergesekan di atasnya. Ketika poros berputar, kedua saklar membuka dan menutup bergantian, sehingga muncul pulsa gelombang kotak di pin CLK (A) dan DT (B). Kedua gelombang bergeser seperempat periode, atau 90 derajat; sinyal seperti ini disebut sinyal quadrature. Mikrokontroler menentukan arah putaran dengan melihat gelombang mana yang berubah lebih dulu, dan besar putarannya dengan menghitung jumlah perubahan. Encoder tidak mengingat posisinya, ia hanya melaporkan gerakan. Karena itu KY-040 termasuk encoder inkremental: posisinya nol ketika dinyalakan dan urusan menghitung sepenuhnya ada di kodemu.
| Fitur | Nilai KY-040 |
|---|---|
| Jenis | Mekanis, inkremental, output A/B |
| Detent (klik) | 20 klik per putaran (30 pada beberapa encoder) |
| Pulsa | 20 siklus A/B penuh per putaran, yaitu 80 tepi |
| Resolusi sudut | 18 derajat per klik |
| Pin | CLK (A), DT (B), SW (tombol), + (catu daya), GND |
| Catu daya | 3,3–5 V (hanya untuk resistor pull-up) |
| Resistor bawaan | Pull-up 10 kΩ di CLK dan DT; resistor SW tidak terpasang di semua modul |
| Tombol | Normally open; menekan poros menghubungkan SW ke GND (aktif LOW) |
| Putaran | Tanpa batas, tidak ada titik mentok |
Rotary encoder vs potensiometer: apa bedanya?
Sekilas encoder sangat mirip potensiometer: keduanya punya poros yang bisa diputar dan keduanya dipakai untuk mengatur nilai. Tapi cara kerjanya sama sekali berbeda. Potensiometer adalah pembagi tegangan; ia mengeluarkan tegangan analog antara 0 dan 5 V tergantung posisi poros, dan kamu membacanya dengan analogRead. Encoder justru menghasilkan pulsa, bukan tegangan; ia memberi tahu perubahan, bukan posisi.

| Fitur | Potensiometer | Rotary encoder (KY-040) |
|---|---|---|
| Putaran | Sekitar 270–300 derajat, terbatas | Tanpa batas, 360 derajat terus-menerus |
| Output | Tegangan analog (0–5 V) | Pulsa digital (A dan B) |
| Cara membaca | analogRead (ADC) | digitalRead atau menghitung dengan interrupt |
| Posisi | Absolut; posisi poros tetap memberi nilai bahkan setelah listrik padam | Relatif; posisi tidak diketahui ketika dinyalakan |
| Rasa klik | Tidak ada, berputar halus | Ada, biasanya 20 klik per putaran |
| Tombol | Tidak ada | Ada (tekan porosnya) |
| Masalah umum | Bunyi kresek dan nilai yang bergeser seiring waktu | Pantulan kontak (diatasi lewat software) |
| Pemakaian umum | Volume, sudut servo, pengaturan analog | Navigasi menu, pengaturan angka bertahap, panel perangkat |
Singkatnya, kalau nilainya harus sama persis dengan posisi fisik poros (menggerakkan lengan servo dengan tangan atau pengatur volume sederhana), potensiometer lebih praktis. Kalau kamu ingin berpindah-pindah di menu, menaikkan dan menurunkan angka dengan langkah yang presisi, mengubah beberapa pengaturan dengan satu kenop, atau juga mengubah nilainya dari software, encoder jauh lebih fleksibel. Misalnya, kalau nilai yang tadinya 50 diubah menjadi 80 dari ponselmu, memutar encoder tinggal melanjutkan dari 80; dengan potensiometer, posisi kenop dan nilai di layar jadi tidak cocok lagi.
Fase A dan B: cara kerja decoding quadrature
Ketika KY-040 diam di satu detent, kedua saklar terbuka dan, berkat resistor pull-up, CLK dan DT sama-sama terbaca HIGH (1). Ketika kenop diputar satu klik, kedua jalur turun ke LOW bergantian lalu kembali ke HIGH bergantian. Searah jarum jam, CLK turun lebih dulu; berlawanan arah jarum jam, DT yang turun lebih dulu. Urutan empat keadaan ini adalah kode Gray: di setiap langkah hanya satu bit yang berubah. Tabel di bawah menunjukkan nilai yang terbaca di kedua pin selama gerakan satu klik:
| Langkah | Searah jarum jam (CLK, DT) | Berlawanan arah jarum jam (CLK, DT) |
|---|---|---|
| Diam di detent | 1, 1 | 1, 1 |
| Transisi ke-1 | 0, 1 | 1, 0 |
| Transisi ke-2 | 0, 0 | 0, 0 |
| Transisi ke-3 | 1, 0 | 0, 1 |
| Transisi ke-4 (klik berikutnya) | 1, 1 | 1, 1 |
Dari tabel itu muncul dua aturan sederhana. Pertama: kalau DT masih HIGH ketika CLK turun, kenop berputar searah jarum jam; kalau DT sudah LOW, kenop berputar berlawanan arah jarum jam. Kedua: satu klik punya empat transisi, jadi 20 klik per putaran berarti 80 transisi. Kode sederhana hanya memakai aturan pertama dan menghitung sekali per klik. Kode yang tangguh mengikuti keempat transisi, sehingga bisa menyaring pantulan atau gerakan yang setengah jalan. Arah mana yang dihitung sebagai searah jarum jam bisa berbeda tergantung modul dan pengkabelannya; kalau penghitungmu berjalan terbalik, cukup tukar pin CLK dan DT di kode.
Pinout dan pengkabelan KY-040
| Pin KY-040 | Fungsi | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|---|
| CLK | Fase A | D2 | GPIO 25 | GP16 |
| DT | Fase B | D3 | GPIO 26 | GP17 |
| SW | Tombol (LOW ketika ditekan) | D4 | GPIO 27 | GP18 |
| + | Catu daya | 5V | 3V3 | 3V3(OUT) |
| GND | Ground | GND | GND | GND |
Di Arduino Uno dan Nano, interrupt hardware hanya tersedia di pin D2 dan D3, jadi paling baik CLK dan DT dihubungkan ke dua pin ini. Di Mega, pin interrupt-nya adalah 2, 3, 18, 19, 20, dan 21; di Leonardo 0, 1, 2, 3, dan 7. Di ESP32 dan Pico hampir semua GPIO bisa memicu interrupt. Kamu bisa mengecek peta pin board-mu di alat Pinout. Pakai INPUT_PULLUP untuk pin SW di kodemu: beberapa modul tidak punya resistor di jalur ini, dan pin yang mengambang terbaca seolah-olah ditekan secara acak. Di ESP32, GPIO 34–39 hanya untuk input dan tidak punya pull-up internal, jadi jangan dipakai untuk SW.
Tes pertama langkah demi langkah: membaca dengan polling
- Hubungkan pin CLK KY-040 ke D2, DT ke D3, dan SW ke D4.
- Hubungkan pin + ke 5V dan GND ke GND Arduino.
- Tempel kode di bawah ke Arduino IDE lalu upload ke board.
- Buka Serial Monitor dan atur kecepatannya ke 115200 baud.
- Putar kenop beberapa klik ke dua arah, lalu tekan porosnya untuk me-reset penghitung.
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}Di setiap putaran loop, kode membaca CLK dan hanya menangkap momen ketika CLK turun dari HIGH ke LOW. Kalau pada momen itu DT masih HIGH, kenop berputar searah jarum jam; kalau LOW, berlawanan arah jarum jam. Ini metode yang paling mudah dipahami, tapi punya dua titik lemah. Pertama, pulsa yang datang ketika loop sibuk dengan pekerjaan lain (memperbarui layar, delay, membaca sensor) akan terlewat, dan penghitungnya tertinggal kalau kamu memutar dengan cepat. Kedua, kontak mekanis memantul: CLK bisa naik-turun LOW-HIGH beberapa kali dalam satu kali turun, dan kode bisa menghitung satu klik sebagai dua atau tiga. Dua bagian berikutnya menyelesaikan kedua masalah ini.
Pantulan kontak (debounce): solusi hardware dan software
Pegas logam di dalam encoder memantul beberapa kali ketika menyentuh kontak sebelum akhirnya diam. Datasheet pabrikan biasanya menyebut waktu ini beberapa milidetik (misalnya maksimal 2 ms). Pada push button, pantulan diatasi dengan aturan “tunggu 20–50 ms setelah perubahan terakhir”, tapi cara itu tidak cocok untuk encoder: ketika diputar cepat, jarak antara dua tepi hanya beberapa milidetik, dan penantian yang lama ikut menelan langkah yang asli. Karena itu metode berikut yang dipakai:
- Filter hardware: Pasang kapasitor 10–100 nF antara CLK dan GND, dan satu lagi antara DT dan GND. Bersama pull-up 10 kΩ di modul, keduanya membentuk filter RC; konstanta waktunya antara 10 kΩ × 10 nF = 0,1 ms dan 10 kΩ × 100 nF = 1 ms, cukup untuk menelan pantulan pendek. Kamu bisa menghitungnya dengan alat konstanta waktu RC dan membaca kapasitor bertanda 103 atau 104 dengan alat kode kapasitor.
- Tabel keadaan: Di setiap tepi kedua jalur, keadaan AB lama dan baru dibandingkan. Pantulan berarti melompat bolak-balik di antara dua keadaan yang sama, yang menghasilkan +1 dan −1 bergantian, sehingga saling meniadakan. Transisi yang mengubah kedua bit sekaligus dianggap tidak valid dan dilewati.
- Putuskan hanya di detent: Langkah seperempat dikumpulkan dan penghitung baru dinaikkan atau diturunkan ketika encoder masuk ke detent (kedua jalur HIGH). Dengan begitu, kontak yang bergetar di satu klik tidak membuat kursor menu melompat.
- Jeda kunci untuk tombol: Pin SW berperilaku seperti push button biasa; jeda kunci 30 ms dengan millis() sudah cukup.
Membaca dengan interrupt agar tidak ada langkah yang terlewat
Interrupt membuat prosesor langsung meninggalkan pekerjaannya begitu sebuah pin berubah, lalu menjalankan fungsi pendek (ISR). Begitu jalur encoder dipasang ke interrupt, tidak ada tepi yang hilang meskipun loop berisi delay 50 ms atau layar diperbarui dengan lambat. Kode di bawah berjalan di kedua tepi CLK dan DT, memakai tabel keadaan yang dijelaskan di atas, dan hanya menghitung ketika kenop masuk ke detent. Untuk dasar-dasarnya, baca juga artikel interrupt 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}| Bagian kode | Apa fungsinya? |
|---|---|
| Tabel QEM[16] | Mengubah keadaan AB lama dan baru menjadi +1, −1, atau 0; transisi tidak valid ketika kedua jalur berubah sekaligus dihitung 0 |
| attachInterrupt(..., CHANGE) (di kedua tepi) | Menjalankan readEncoder() di tepi naik dan tepi turun CLK dan DT |
| quarter (seperempat langkah) | Mengumpulkan langkah seperempat di antara dua klik; pasangan +1 dan −1 akibat pantulan saling meniadakan |
| if (ab == 3) (di detent) | Keputusan hanya diambil ketika encoder masuk ke detent (kedua jalur HIGH); nilai terkumpul +2 atau lebih dihitung sebagai satu klik |
| volatile long position (variabel bersama) | Variabel yang dipakai bersama oleh ISR dan loop; volatile mencegah compiler memakai salinan yang sudah basi |
| noInterrupts() / interrupts() (bagian kritis) | Mencegah ISR menyela ketika long 4 byte sedang disalin |
| buttonPressed() (membaca tombol) | Melakukan debounce SW dengan jeda kunci 30 ms dan mengembalikan true hanya sekali, tepat ketika ditekan |
Jalan pintas: library Encoder
Kalau kamu tidak mau menulis ISR sendiri, library Encoder buatan Paul Stoffregen mengerjakan hal yang sama dengan kode yang sudah dioptimalkan. Buka Library Manager di Arduino IDE, cari “Encoder”, lalu instal library milik Paul Stoffregen; untuk langkah instalasinya, lihat panduan Cara Instal Library Arduino. Library ini memberi performa terbaik kalau kedua pin adalah pin interrupt, performa bagus kalau hanya pin pertama yang diberikan ke constructor yang merupakan pin interrupt, dan performa rendah kalau keduanya bukan. Karena setiap tepi dihitung, nilainya berubah 4 per klik; bagi dengan 4 untuk mendapat jumlah klik.
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() mengembalikan total jumlah tepi sejauh ini, write(0) me-reset penghitung, dan readAndReset() membaca sekaligus me-reset dalam satu langkah. Kita memberikan DT lebih dulu dan CLK kedua ke constructor karena tanda arah putaran: dengan urutan ini, searah jarum jam dihitung positif. Kalau di tempatmu terbalik, tukar 2 dan 3. Library ini juga mengaktifkan resistor pull-up internal pin dengan sendirinya.
Navigasi menu dengan rotary encoder di LCD

Menu adalah tempat encoder paling sering dipakai: kamu memutar kenop untuk berpindah antarbaris, menekannya untuk masuk ke mode edit pada pengaturan yang dipilih, memutarnya untuk mengubah nilai, lalu menekan sekali lagi untuk kembali ke menu. Contoh di bawah mengatur kanal merah, hijau, dan biru sebuah LED RGB antara 0 dan 255 dan menampilkan nilainya di layar LCD I2C. Hubungkan LCD ke A4 (SDA) dan A5 (SCL), lalu kaki warna LED RGB common cathode ke D9, D10, dan D11 lewat resistor 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}Tanda > muncul di awal baris yang dipilih, dan tanda * di mode edit. Kode encoder-nya sama dengan contoh sebelumnya; bedanya hanya ISR mengumpulkan jumlah klik yang belum diproses (steps) alih-alih posisi, lalu loop membaca dan mengosongkannya. Jadi meskipun loop sibuk untuk sementara, tidak ada klik yang hilang; klik yang terkumpul ditangani bersama di putaran berikutnya. Di mode edit, setiap klik mengubah nilai sebesar 5; untuk kendali yang lebih halus kamu bisa menurunkan langkahnya ke 1 atau menambahkan akselerasi yang memperbesar langkah ketika kenop diputar cepat.
Untuk menampilkan menu yang sama di layar OLED, kamu cukup mengganti fungsi drawMenu(). Versi di bawah menggambar baris yang dipilih dengan warna terbalik di atas bilah putih. Tambahkan library Adafruit SSD1306 dan Adafruit GFX sebagai pengganti library LCD, hapus objek lcd, dan panggil display.begin() alih-alih lcd.init() di setup().
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}Rotary encoder dengan ESP32: penghitung hardware PCNT
ESP32 punya blok hardware bernama PCNT (Pulse Counter) yang menghitung pulsa tanpa membuat prosesor sibuk sama sekali. Setiap unit punya dua kanal, jadi bisa langsung men-decode sinyal A/B; ESP32 klasik punya 8 unit PCNT, sedangkan ESP32-S2 dan S3 punya 4. Penghitungnya selebar 16 bit dan dilengkapi glitch filter yang membuang derau pendek. Filter ini paling tinggi bisa diatur ke sekitar 12,8 mikrodetik, jadi hanya menelan lonjakan yang sangat pendek, bukan seluruh pantulan kontak. Penghitungan empat tepi menyeimbangkan sisa pantulan dengan sendirinya: pantulan bolak-balik saling meniadakan sebagai +1 dan −1. Kode di bawah memakai driver ESP-IDF yang ikut di core Arduino ESP32 versi 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}Kedua kanal diatur sebagai bayangan cermin satu sama lain: kanal A menghitung tepi CLK dan membalik arah sesuai level DT, sedangkan kanal B menghitung tepi DT relatif terhadap CLK. Hasilnya, penghitung berubah 4 per klik. Ketika mencapai batas ±10000, nilainya kembali ke nol; kalau butuh rentang yang lebih lebar, kamu bisa mengumpulkan overflow dengan opsi flags.accum_count dan watch point. ESP32-C3 tidak punya unit PCNT; di board itu pakai kode Arduino berbasis interrupt. Kalau kamu memakai core Arduino ESP32 versi 2.x atau ingin kode yang lebih pendek, library ESP32Encoder memakai hardware yang sama dan mengerjakannya dengan fungsi attachFullQuad() dan getCount(). Kalau kamu memakai MicroPython di ESP32, kelas machine.Encoder di versi terbaru (misalnya 1.27) juga memakai PCNT di balik layar. Kalau kamu baru mengenal ESP32, baca dulu Apa Itu ESP32?.
Rotary encoder dengan Raspberry Pi Pico dan MicroPython
Setiap pin GPIO di Pico bisa memicu interrupt, jadi membaca encoder di MicroPython dengan Pin.irq() itu mudah. Kode di bawah memakai tabel keadaan yang persis sama dengan contoh Arduino dan mengubah encoder menjadi peredup LED: setiap klik mengubah kecerahan sebesar 5 persen dan menekan poros menyalakan atau mematikan LED. Hubungkan CLK ke GP16, DT ke GP17, SW ke GP18, dan sebuah LED lewat resistor 220 Ω ke GP15. Untuk menyiapkan MicroPython, lihat artikel Apa Itu 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)Batas nilainya (0–20 klik) kita cek di dalam fungsi interrupt, jadi loop utama hanya membaca variabelnya dan tidak ada race condition ketika kedua sisi menulis bersamaan. Cara mengatur kecerahan LED bisa kamu temukan di artikel Apa Itu PWM?. Kalau lebih suka solusi siap pakai, modul rotary_irq_rp2 dari proyek micropython-rotary sudah dilengkapi batas, wrap-around, dan pembalik arah. Untuk encoder motor yang sangat cepat, blok PIO di Pico bisa menghitung tanpa membebani prosesor sama sekali. Kalau kamu baru mengenal MicroPython, artikel dasar-dasar MicroPython adalah tempat yang bagus untuk mulai.
Jenis rotary encoder: mekanis, optik, dan magnetik
| Jenis | Bagaimana cara kerjanya? | Resolusi umum | Dipakai di mana? |
|---|---|---|---|
| Mekanis (KY-040, EC11) | Piringan kontak dan dua saklar | 12–30 klik per putaran | Kenop menu, pengatur volume, panel depan perangkat |
| Optik | Piringan bercelah, LED, dan fototransistor | Ratusan sampai ribuan pulsa per putaran | Mesin CNC, sendi robot, motor industri |
| Magnetik (efek Hall) | Piringan magnet dan sensor Hall | Beberapa pulsa per putaran poros motor (misalnya 7 atau 11) | Motor N20 dan motor DC bergigi dengan encoder |
| Absolut | Mengeluarkan kode unik untuk setiap posisi | Misalnya 12 bit (4096 posisi) pada AS5600 | Lengan robot, pengukuran sudut; tahu posisinya ketika dinyalakan |
Kode di artikel ini juga bekerja dengan encoder inkremental lain yang punya output A/B. Misalnya, pada motor DC bergigi dengan encoder, encoder Hall yang memberi 11 pulsa per putaran poros motor, penghitungan empat tepi, dan rasio gigi 1:30 menghasilkan 11 × 4 × 30 = 1320 hitungan per putaran poros output. Ketika laju pulsa naik ke ribuan per detik, kode berbasis interrupt memakan bagian waktu prosesor yang terasa, sedangkan unit PCNT ESP32 mengerjakan penghitungan ini dengan ringan. Encoder optik industri umumnya bekerja di 5–24 V dan punya output open-collector atau diferensial; selalu cek jenis output dan tegangannya di datasheet sebelum menghubungkannya.
Ide proyek rotary encoder
- Kenop volume digital: Atur volume musik yang diputar di DFPlayer Mini dengan encoder dan tekan porosnya untuk lompat ke lagu berikutnya.
- Lampu pemilih warna: Putar hue cincin LED WS2812B dengan encoder dan tekan untuk pindah ke mode kecerahan.
- Pemosisian presisi: Buat setiap klik menggerakkan motor stepper sejumlah langkah tertentu; buat slider kamera atau meja putar.
- Timer dapur: Atur waktunya dengan encoder, tekan untuk memulai hitung mundur, dan bunyikan buzzer ketika waktunya habis.
- Pengendali game: Pakai encoder sebagai pemukul di Pong; di proyek game Arduino, encoder memberi kendali yang lebih halus daripada joystick.
- Pengaturan jam dan alarm: Atur waktu dan alarm modul jam RTC dengan satu kenop.
- Panel alat ukur: Ubah frekuensi PWM, nilai generator sinyal, atau suhu target stasiun solder dengan langkah yang halus.
Kesalahan umum dan cara memperbaikinya
| Masalah | Kemungkinan penyebab | Solusi |
|---|---|---|
| Menghitung 2 atau 4 per klik | Setiap tepi dihitung terpisah atau kontaknya memantul | Bagi jumlah tepi dengan 4, pakai tabel keadaan, tambahkan kapasitor 10–100 nF di CLK dan DT |
| Hanya menghitung setiap klik kedua | Encoder 30 klik / 15 pulsa; setiap klik adalah setengah siklus | Bagi nilai library dengan 2, bukan 4; di kode tabel, ubah if (ab == 3) menjadi if (ab == 3 || ab == 0) |
| Arah putaran terbalik | CLK dan DT tertukar atau modulnya berbeda | Tukar nomor pin CLK dan DT di kode |
| Langkah terlewat ketika diputar cepat | Memakai polling, ada delay di loop | Pakai interrupt atau PCNT ESP32, buat ISR tetap pendek |
| Tombol tertekan sendiri | Pin SW mengambang, tidak ada pull-up | Pakai pinMode(SW, INPUT_PULLUP) |
| Penghitung melompat secara acak | Tidak ada GND bersama, kabel panjang atau longgar | Satukan GND, perpendek kabel, cek kontak breadboard |
| “fatal error: driver/pulse_cnt.h: No such file or directory” / “undefined reference to 'pcnt_new_unit'” | Core Arduino ESP32 2.x (error pertama) atau ESP32-C3 yang tidak punya unit PCNT (error kedua) | Update core ke 3.x di Boards Manager atau pakai library ESP32Encoder; di C3 pakai kode interrupt |
| LCD hanya menampilkan kotak-kotak | Pengaturan kontras atau alamat I2C salah | Putar trimmer di bagian belakang, coba alamat 0x3F sebagai pengganti 0x27 |
Rotary encoder adalah salah satu cara termurah untuk memberi proyek antarmuka pengguna yang profesional: satu kenop dan satu layar bisa menangani puluhan pengaturan. Mulailah dengan kode polling untuk memahami logikanya, lalu pindah ke versi interrupt atau PCNT dan sesuaikan contoh menu dengan proyekmu sendiri. Di aplikasi BlueGrays kamu akan menemukan proyek kendali servo dengan potensiometer, LCD I2C, push button, dan penghitung 7-segmen lengkap dengan skema rangkaian dan kode langkah demi langkah, dan kamu bisa dengan mudah meningkatkannya dengan mengganti potensiometer atau tombol dengan encoder.
Pertanyaan umum
Apa beda rotary encoder dan potensiometer?
Potensiometer mengeluarkan tegangan analog yang tergantung posisi poros, putarannya terbatas sekitar 270–300 derajat, dan kamu membacanya dengan analogRead. Rotary encoder berputar tanpa batas dan menghasilkan pulsa digital di setiap klik; ia melaporkan arah dan besar gerakan, bukan posisi. Encoder lebih cocok untuk navigasi menu dan pengaturan bertahap, potensiometer untuk pengaturan sederhana berbasis posisi.
Kenapa rotary encoder-ku menghitung dua langkah per klik atau melewatkan langkah?
Hitungan berlebih biasanya disebabkan pantulan kontak atau karena setiap tepi dihitung terpisah; membagi 4 tepi per klik dengan 4 dan memakai tabel keadaan akan memperbaikinya. Langkah terlewat terjadi ketika pulsa datang selagi loop sibuk. Hubungkan CLK dan DT ke pin interrupt dan baca dengan interrupt, tambahkan kapasitor 10–100 nF kalau perlu, dan masalahnya hilang.
Apakah rotary encoder KY-040 bisa bekerja di 3,3 V?
Bisa. KY-040 hanya terdiri dari saklar mekanis dan resistor pull-up, jadi ia juga bekerja sempurna di 3,3 V. Di ESP32 dan Raspberry Pi Pico, selalu hubungkan pin + ke 3V3; kalau diberi daya 5 V, resistor pull-up menarik CLK dan DT ke 5 V dan batas input 3,3 V terlampaui.
Berapa pulsa per putaran yang diberikan KY-040?
Modul KY-040 yang umum punya 20 klik per putaran, dan jalur A dan B menyelesaikan satu siklus penuh di setiap klik. Setiap siklus punya empat tepi, jadi kamu menghitung 80 tepi per putaran, dan setiap klik sama dengan 18 derajat. Beberapa encoder punya 30 klik dan 15 pulsa; pada encoder itu, setiap klik setara dengan setengah siklus.
Apakah rotary encoder bisa dibaca tanpa interrupt?
Bisa, dengan polling, yaitu mengecek pin CLK terus-menerus di dalam loop, dan cara ini sering sudah cukup untuk kenop menu yang diputar pelan. Tapi kalau loop berisi delay, pembaruan layar, atau perhitungan panjang, klik yang diputar cepat akan terlewat. Untuk hasil yang andal, disarankan memakai interrupt, dan di ESP32 penghitung hardware PCNT.
Bisakah aku menghubungkan lebih dari satu rotary encoder ke Arduino?
Bisa. Karena di Uno hanya D2 dan D3 yang punya interrupt hardware, kamu bisa menghubungkan pin pertama setiap encoder (yang diberikan pertama ke constructor) ke pin interrupt dan pin lainnya ke pin biasa dengan library Encoder, atau memakai pin change interrupt. Mega punya enam pin interrupt, ESP32 klasik bisa menghitung 8 encoder secara hardware dengan 8 unit PCNT-nya, dan di Pico setiap GPIO bisa memicu interrupt.
Proyek untuk dibuat setelah membaca ini
Lihat semua proyek
Nyala/Mati LED dengan Tombol (On/Off)
Tekan tombol sekali dan lampu menyala, tekan lagi dan lampu mati, persis seperti sakelar lampu di kamarmu. Arduino menyadari setiap tekanan dan membalik LED ke keadaan sebaliknya, sambil menunggu sebe

Kendali LED dengan Tombol Tekan ESP32 (Toggle)
Setiap kali tombol ditekan, keadaan LED berubah: jika menyala jadi mati, jika mati jadi menyala. Pola toggle klasik. Dengan debounce, kita membersihkan derau mekanis tombol tekan agar satu tekanan menghasilkan satu kali toggle.

Debounce Tombol Raspberry Pi Pico: IRQ vs Polling Perangkat Lunak
Tekan tombol sekali, kode menghitung 3-4? Itu pantulan kontak: kontak logam bergetar beberapa mikrodetik! Bandingkan 2 metode di Serial: polling MENTAH (ikut menghitung pantulan) vs IRQ + debounce perangkat lunak (akurat). Pelajari pantulan kontak, debounce perangkat lunak (ticks_diff() > 50ms), interrupt Pin.irq(), pemicu IRQ_FALLING, handler ISR, serta global + keamanan ISR. LED berbalik lewat IRQ, dan Serial menampilkan kedua penghitung.

Menggerakkan Motor Servo dengan Potensiometer
Ketika kamu memutar kenop potensiometer, lengan motor servo ikut berputar: satu ujung kenop berarti nol derajat, ujung lainnya berarti setengah putaran. Arduino terus membaca posisi kenop dan servo la

Kendali Kecerahan LED dengan Potensiometer ESP32 (Analog → PWM)
Di proyek ini, kita mengendalikan kecerahan LED secara analog memakai potensiometer. Pin ADC ESP32 membaca tegangan 0-3,3V dari potensiometer, lalu nilai ini diubah menjadi sinyal PWM (Pulse Width Modulation) dan dikirim ke LED. Hasilnya, ketika kamu memutar potensiometer, LED perlahan menerang dan meredup. Dengan Arduino Core 3.0+, analogWrite() bisa langsung dipakai; di ESP32-S2/S3, resolusi ADC tetap 12-bit.

Penghitung dengan Layar 7 Segmen dan Tombol
Penghitung sederhana dengan tiga tombol: tekan satu dan angka di layar naik satu, tekan yang lain dan turun satu, dan yang ketiga mengembalikannya ke nol. Layar 7 segmen satu digit menghitung dari 0 s

Menggunakan Layar LCD dengan I2C
Beri Arduino-mu layar sendiri dan buat ia menulis pesan pertamanya. Layar LCD dua baris tersambung hanya dengan beberapa kabel; sapaan muncul di baris atas, lalu pesan kedua di baris bawah, layar dibe

Game Bola Pemukul - SSD1306
Satu bola, dua pemukul, dan kamu: kamu bermain tenis meja, salah satu video game paling awal, di layar OLED kecil. Dua tombol menggerakkan pemukulmu di kanan ke atas dan ke bawah, sementara komputer m
Artikel lainnya
Aplikasi BlueGrays
Rakit rangkaian ini langkah demi langkah di ponselmu
Skema pengkabelan, kode, dan daftar komponen dalam satu layar. 200+ proyek Arduino, ESP32, dan Pico siap dibuat.
Pindai dengan ponselmu
Arahkan kamera ke kode ini dan toko aplikasi di ponselmu akan terbuka.
atau buka toko aplikasi

