Rotary Encoder Arduino: KY-040 Kullanımı, Kesme ve Menü Örneği
Rotary encoder Arduino ile nasıl kullanılır? KY-040 bağlantısı, A/B fazlı okuma, kesmeyle tık kaçırmadan sayma, debounce, LCD menü, ESP32 PCNT ve Pico kodu.
Rotary encoder (döner kodlayıcı), Arduino projelerinde sonsuz dönebilen, her tıkta bir adım sayan ve miline basınca buton gibi çalışan dijital bir ayar düğmesidir. Rotary encoder Arduino'ya bağlandığında 3D yazıcılardaki menü düğmesi, osiloskopların ayar topuzu ya da arabalardaki ses düğmesi gibi davranır: kaç tık çevirdiğini ve hangi yöne çevirdiğini bilir, hiçbir zaman bir sınıra takılmaz. Hobi setlerinde en sık karşına çıkacak model, beş pinli KY-040 modülüdür.
Bu rehberde KY-040'ın potansiyometreden farkını, A ve B fazlarından (quadrature) yönün nasıl bulunduğunu, kontak sıçramasını donanım ve yazılımla nasıl süzeceğini ve kesme (interrupt) kullanarak tek bir tıkı bile kaçırmadan nasıl sayacağını öğreneceksin. Ardından Encoder kütüphanesiyle kısa yolu, LCD ve OLED ekranda menü gezinmeyi, ESP32'nin donanım sayacı PCNT'yi ve Raspberry Pi Pico için MicroPython kodunu adım adım kuracağız.
Rotary encoder nedir, nasıl çalışır?
KY-040 gibi mekanik bir encoder'ın içinde milde dönen dişli bir kontak diski ve bu diske sürtünen iki küçük anahtar vardır. Mil döndükçe bu iki anahtar sırayla açılıp kapanır ve CLK (A) ile DT (B) pinlerinde kare dalga biçiminde darbeler oluşur. İki dalga birbirinden çeyrek periyot, yani 90 derece kaydırılmıştır; buna quadrature (dik fazlı) sinyal denir. Mikrodenetleyici hangi dalganın önce değiştiğine bakarak yönü, değişimlerin sayısına bakarak da ne kadar döndüğünü bulur. Encoder kendi konumunu hatırlamaz, yalnızca hareketi bildirir. Bu yüzden KY-040 bir artımlı (incremental) encoder'dır: açılışta konum sıfırdır ve sayma işi tamamen senin kodundadır.
| Özellik | KY-040 değeri |
|---|---|
| Tür | Mekanik, artımlı (incremental), A/B çıkışlı |
| Tık sayısı | Turda 20 tık (bazı encoder'larda 30) |
| Darbe sayısı | Turda 20 tam A/B döngüsü, yani 80 kenar |
| Açı çözünürlüğü | Tık başına 18 derece |
| Pinler | CLK (A), DT (B), SW (buton), + (besleme), GND |
| Besleme | 3,3–5 V (yalnızca pull-up dirençlerini besler) |
| Dahili dirençler | CLK ve DT'de 10 kΩ pull-up; SW direnci her modülde takılı değil |
| Buton | Normalde açık; mile basınca SW pini GND'ye bağlanır (aktif LOW) |
| Dönüş | Sınırsız, durdurucu yok |
Rotary encoder ile potansiyometre arasındaki fark
İlk bakışta encoder bir potansiyometreye çok benzer: ikisinin de dönen bir mili vardır ve ikisi de bir değeri ayarlamaya yarar. Ama çalışma prensipleri tamamen farklıdır. Potansiyometre bir gerilim bölücüdür; milin konumuna göre 0 ile 5 V arasında analog bir gerilim verir ve analogRead ile okunur. Encoder ise gerilim değil darbe üretir; konumu değil, değişimi söyler.

| Özellik | Potansiyometre | Rotary encoder (KY-040) |
|---|---|---|
| Dönüş | Yaklaşık 270–300 derece, sınırlı | Sınırsız, sürekli 360 derece |
| Çıkış | Analog gerilim (0–5 V) | Dijital darbeler (A ve B) |
| Okuma | analogRead (ADC) | digitalRead ya da kesme ile sayma |
| Konum bilgisi | Mutlak; elektrik kesilse de mil konumu değeri verir | Göreli; açılışta konum bilinmez |
| Tık hissi | Yok, pürüzsüz döner | Var, turda genellikle 20 tık |
| Buton | Yok | Var (mile basınca) |
| Tipik sorun | Zamanla cızırtı ve değer oynaması | Kontak sıçraması (yazılımla çözülür) |
| Tipik kullanım | Ses seviyesi, servo açısı, analog ayar | Menü gezinme, adımlı sayısal ayar, cihaz paneli |
Kısacası, değer milin fiziksel konumuyla birebir eşleşmeliyse (bir servo kolunu elle yönlendirmek ya da basit bir ses ayarı gibi) potansiyometre daha basittir. Menüde gezinmek, bir sayıyı hassas adımlarla artırıp azaltmak, aynı düğmeyle birden fazla ayarı değiştirmek ya da değeri yazılımdan da değiştirebilmek istiyorsan encoder çok daha esnektir. Örneğin değer ekranda 50 iken telefondan 80 yapılsa bile encoder'ı çevirdiğinde 80'den devam edersin; potansiyometrede ise düğmenin konumu ile ekrandaki değer artık birbirini tutmaz.
A ve B fazları: quadrature okuma mantığı
KY-040 bir tıkta dururken iki anahtar da açıktır ve pull-up dirençleri sayesinde CLK ile DT ikisi de HIGH (1) okunur. Düğmeyi bir tık çevirdiğinde iki hat sırayla LOW'a düşer ve sırayla geri HIGH'a çıkar. Saat yönünde önce CLK, ters yönde önce DT düşer. Bu dört durumlu sıralama bir Gray kodudur: her adımda yalnızca bir bit değişir. Aşağıdaki tablo, bir tıklık hareket boyunca iki pinin okuduğu değerleri gösteriyor:
| Adım | Saat yönü (CLK, DT) | Ters yön (CLK, DT) |
|---|---|---|
| Tıkta duruyor | 1, 1 | 1, 1 |
| 1. geçiş | 0, 1 | 1, 0 |
| 2. geçiş | 0, 0 | 0, 0 |
| 3. geçiş | 1, 0 | 0, 1 |
| 4. geçiş (sonraki tık) | 1, 1 | 1, 1 |
Tablodan iki basit kural çıkar. Birincisi: CLK düşerken DT hâlâ HIGH ise dönüş saat yönündedir, DT zaten LOW ise ters yöndedir. İkincisi: bir tıkta dört geçiş olur, yani turda 20 tık 80 geçiş demektir. Basit kodlar yalnızca birinci kuralı kullanır ve tık başına bir kez sayar. Sağlam kodlar ise dört geçişin hepsini izler; böylece sıçrayan ya da yarıda bırakılan hareketleri ayıklayabilir. Hangi yönün saat yönü sayılacağı modüle ve kablolamaya göre değişebilir; sayaç ters yönde ilerliyorsa kodda CLK ile DT pinlerini yer değiştirmen yeterli.
KY-040 pinleri ve bağlantı şeması
| KY-040 pini | Görevi | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|---|
| CLK | A fazı | D2 | GPIO 25 | GP16 |
| DT | B fazı | D3 | GPIO 26 | GP17 |
| SW | Buton (basınca LOW) | D4 | GPIO 27 | GP18 |
| + | Besleme | 5V | 3V3 | 3V3(OUT) |
| GND | Toprak | GND | GND | GND |
Arduino Uno ve Nano'da donanım kesmesi yalnızca D2 ve D3 pinlerinde vardır, bu yüzden CLK ve DT'yi bu iki pine bağlamak en iyisidir. Mega'da 2, 3, 18, 19, 20 ve 21; Leonardo'da 0, 1, 2, 3 ve 7 kesme pinidir. ESP32 ve Pico'da ise neredeyse her GPIO kesme üretebilir. Kartının pin haritasına Pinout aracından bakabilirsin. SW pini için kodda INPUT_PULLUP kullan: bazı modüllerde bu hatta direnç takılı değildir ve boşta kalan pin rastgele basılmış gibi okunur. ESP32'de GPIO 34–39 yalnızca giriştir ve dahili pull-up içermez; SW için bu pinleri seçme.
Adım adım ilk deneme: yoklama (polling) ile okuma
- KY-040'ın CLK pinini D2'ye, DT pinini D3'e, SW pinini D4'e bağla.
- + pinini 5V'a, GND pinini Arduino'nun GND pinine bağla.
- Aşağıdaki kodu Arduino IDE'ye yapıştır ve karta yükle.
- Seri Monitör'ü aç ve hızı 115200 baud yap.
- Düğmeyi iki yöne birkaç tık çevir, sonra mile basarak sayacı sıfırla.
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}Kod her loop turunda CLK'yi okur ve yalnızca CLK'nin HIGH'tan LOW'a düştüğü anı yakalar. O anda DT hâlâ HIGH ise düğme saat yönünde, LOW ise ters yönde dönmüştür. Bu yöntem anlaması en kolay olanıdır ama iki zayıf noktası vardır. Birincisi, loop başka işlerle (ekran güncelleme, delay, sensör okuma) meşgulken gelen darbeler kaçar ve hızlı çevirdiğinde sayaç geride kalır. İkincisi, mekanik kontaklar sıçrar: CLK bir kez düşerken birkaç kez LOW-HIGH yapabilir ve kod tek bir tıkı iki ya da üç tık sayabilir. Sonraki iki bölüm bu iki sorunu çözüyor.
Kontak sıçraması (debounce): donanım ve yazılımla çözüm
Encoder'ın içindeki metal yaylar kontağa değdiği anda birkaç kez sekip ancak sonra oturur. Üretici datasheet'lerinde bu süre genellikle birkaç milisaniye olarak verilir (ör. en fazla 2 ms). Butonlarda sıçramayı "son değişimden sonra 20–50 ms bekle" diyerek çözersin, ama encoder'da bu işe yaramaz: hızlı çevrilen bir encoder'da iki kenar arasında yalnızca birkaç milisaniye vardır ve uzun bir bekleme gerçek adımları da yutar. Bunun yerine şu yöntemler kullanılır:
- Donanım filtresi: CLK ve DT ile GND arasına birer 10–100 nF kondansatör koy. Modüldeki 10 kΩ pull-up ile birlikte bir RC filtresi oluşur; zaman sabiti 10 kΩ × 10 nF = 0,1 ms ile 10 kΩ × 100 nF = 1 ms arasındadır ve kısa sekmeleri yutar. Değeri RC zaman sabiti aracıyla hesaplayabilir, üzerinde 103 ya da 104 yazan kondansatörün değerini kondansatör kodu aracıyla çözebilirsin.
- Durum tablosu: Her iki hattın her kenarında eski ve yeni AB durumuna bakılır. Sıçrama, aynı iki durum arasında gidip gelmek demektir; bu da sırayla +1 ve −1 üretir ve birbirini götürür. Aynı anda iki bitin değiştiği geçişler geçersiz sayılıp atlanır.
- Yalnızca tıkta karar ver: Çeyrek adımlar biriktirilir ve sayaç yalnızca encoder bir tıka oturduğunda (iki hat da HIGH) artırılır ya da azaltılır. Böylece tık üzerinde titreyen bir kontak menüde imleci oynatmaz.
- Buton için zaman kilidi: SW pini sıradan bir buton gibi davranır; ona millis() ile 30 ms'lik bir kilit koymak yeterlidir.
Kesme (interrupt) ile adım kaçırmadan okuma
Kesme, bir pin değiştiği anda işlemcinin elindeki işi bırakıp kısa bir fonksiyonu (ISR) çalıştırmasıdır. Encoder hatlarını kesmeye bağladığında loop içinde 50 ms'lik bir delay olsa ya da ekran yavaş güncellense bile hiçbir kenar kaçmaz. Aşağıdaki kod CLK ve DT'nin her iki kenarında da çalışır, yukarıda anlattığımız durum tablosunu kullanır ve yalnızca tıka oturunca sayar. Konunun temelleri için Arduino interrupt yazısına da bakabilirsin.
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}| Kod parçası | Ne yapar? |
|---|---|
| QEM[16] tablosu | Eski ve yeni AB durumundan +1, −1 ya da 0 üretir; iki hattın aynı anda değiştiği geçersiz geçişleri 0 sayar |
| attachInterrupt(..., CHANGE) | CLK ve DT'nin hem yükselen hem düşen kenarında readEncoder() fonksiyonunu çalıştırır |
| quarter | İki tık arasındaki çeyrek adımları biriktirir; sıçramadan gelen +1 ve −1 çiftleri birbirini götürür |
| if (ab == 3) | Karar yalnızca encoder tıka oturunca (iki hat da HIGH) verilir; birikim +2 ya da daha fazlaysa bir tık sayılır |
| volatile long position | ISR ile loop'un paylaştığı değişken; volatile, derleyicinin eski bir kopyayı kullanmasını engeller |
| noInterrupts() / interrupts() | 4 baytlık long değişkeni kopyalarken ISR'ın araya girmesini önler |
| buttonPressed() | SW için 30 ms kilitli debounce; yalnızca basış anında bir kez true döndürür |
Encoder kütüphanesi ile kısa yol
Kendi ISR'ını yazmak istemiyorsan Paul Stoffregen'in Encoder kütüphanesi aynı işi optimize edilmiş bir kodla yapar. Arduino IDE'de Kütüphane Yöneticisi'ni açıp "Encoder" araması yap ve Paul Stoffregen'in kütüphanesini kur; kurulum adımları için Arduino kütüphane ekleme rehberine göz atabilirsin. Kütüphane iki pin de kesme pini olduğunda en iyi, yalnızca kurucuya verilen ilk pin kesme pini olduğunda iyi, hiçbiri değilse düşük performansla çalışır. Her kenarı saydığı için değer bir tıkta 4 değişir; tık sayısını bulmak için 4'e bölmen gerekir.
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() o ana kadarki toplam kenar sayısını verir, write(0) sayacı sıfırlar, readAndReset() ise okuyup aynı anda sıfırlar. Kurucuya önce DT, sonra CLK pinini vermemizin sebebi yön işaretidir: bu sırayla saat yönü pozitif sayılır. Sende ters çıkarsa 2 ile 3'ü yer değiştir. Kütüphane pinlerin dahili pull-up dirençlerini de kendisi açar.
LCD ekranda rotary encoder ile menü gezinme

Encoder'ın en çok kullanıldığı yer menülerdir: düğmeyi çevirerek satırlar arasında gezersin, basarak seçili ayarı düzenleme moduna alırsın, çevirerek değeri değiştirirsin, bir kez daha basarak menüye dönersin. Aşağıdaki örnek bir RGB LED'in kırmızı, yeşil ve mavi kanallarını 0–255 arasında ayarlıyor ve değerleri I2C LCD ekranda gösteriyor. LCD'yi A4 (SDA) ve A5 (SCL) pinlerine, ortak katotlu RGB LED'in renk bacaklarını 220 Ω dirençlerle D9, D10 ve D11'e bağla.
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}Seçili satırın başında > işareti, düzenleme modunda ise * işareti görünür. Encoder kodu önceki örnekle aynıdır; tek fark, ISR'ın konum yerine işlenmemiş tık sayısını (steps) biriktirmesi ve loop'un bunu okuyup sıfırlamasıdır. Böylece loop bir süre meşgul kalsa bile hiçbir tık kaybolmaz, birikenler bir sonraki turda topluca işlenir. Düzenleme modunda her tık değeri 5 değiştirir; daha ince ayar için adımı 1'e indirebilir ya da hızlı çevrildiğinde adımı büyüten bir ivme ekleyebilirsin.
Aynı menüyü OLED ekranda göstermek için yalnızca drawMenu() fonksiyonunu değiştirmen yeter. Aşağıdaki sürüm seçili satırı beyaz bir çubuk üzerinde ters renkte çizer. LCD kütüphanesinin yerine Adafruit SSD1306 ve Adafruit GFX kütüphanelerini ekle, lcd nesnesini sil ve setup() içinde lcd.init() yerine display.begin() çağır.
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 ile rotary encoder: PCNT donanım sayacı
ESP32'nin içinde PCNT (Pulse Counter) adında, darbeleri işlemciyi hiç meşgul etmeden sayan bir donanım birimi vardır. Her birimin iki kanalı olduğu için A/B sinyalini doğrudan çözebilir; klasik ESP32'de 8, ESP32-S2 ve S3'te 4 PCNT birimi bulunur. Sayaç 16 bitliktir ve kısa parazitleri süzen bir glitch filtresi içerir. Bu filtre en fazla yaklaşık 12,8 mikrosaniyeye ayarlanabilir, yani kontak sıçramasının tamamını değil yalnızca çok kısa iğneleri yutar. Sıçramanın geri kalanını dört kenarlı sayma kendiliğinden dengeler: ileri geri sekmeler +1 ve −1 olarak birbirini götürür. Aşağıdaki kod, Arduino ESP32 çekirdeğinin 3.x sürümüyle gelen ESP-IDF sürücüsünü kullanır.
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}Kodda iki kanal birbirinin aynası gibi kurulur: A kanalı CLK'nin kenarlarını sayar ve yönü DT'nin seviyesine göre çevirir, B kanalı ise DT'nin kenarlarını CLK'ye göre sayar. Sonuçta her tıkta sayaç 4 değişir. Sayaç ±10000 sınırına ulaşınca sıfıra döner; daha geniş bir aralık gerekiyorsa flags.accum_count ayarı ve izleme noktaları (watch point) ile taşmaları biriktirebilirsin. ESP32-C3'te PCNT birimi yoktur; bu kartta kesmeli Arduino kodunu kullan. Arduino ESP32 çekirdeğinin 2.x sürümündeysen ya da daha kısa kod istiyorsan, aynı donanımı kullanan ESP32Encoder kütüphanesi attachFullQuad() ve getCount() fonksiyonlarıyla aynı işi yapar. ESP32'de MicroPython kullanıyorsan güncel sürümlerdeki (ör. 1.27) machine.Encoder sınıfı da arka planda PCNT'yi kullanır. ESP32'ye yeni başlıyorsan önce ESP32 nedir yazısına göz at.
Raspberry Pi Pico ve MicroPython ile rotary encoder
Pico'nun her GPIO pini kesme üretebildiği için encoder'ı MicroPython'da Pin.irq() ile okumak kolaydır. Aşağıdaki kod Arduino örneğindeki durum tablosunun aynısını kullanıyor ve encoder'ı bir LED dimmer'a çeviriyor: her tık parlaklığı yüzde 5 değiştiriyor, mile basmak LED'i açıp kapatıyor. CLK'yi GP16'ya, DT'yi GP17'ye, SW'yi GP18'e, bir LED'i de 220 Ω dirençle GP15'e bağla. MicroPython kurulumu için Raspberry Pi Pico nedir yazısına bakabilirsin.
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)Değerin sınırlarını (0–20 tık) kesme fonksiyonunun içinde kontrol ettik; böylece ana döngü değişkeni yalnızca okur ve iki tarafın aynı anda yazdığı bir yarış durumu oluşmaz. LED parlaklığının nasıl ayarlandığını PWM nedir yazısında bulabilirsin. Hazır bir çözüm istersen micropython-rotary projesindeki rotary_irq_rp2 modülü sınır, başa sarma (wrap) ve yön çevirme ayarlarıyla gelir. Çok hızlı dönen motor encoder'ları için ise Pico'nun PIO birimiyle işlemciyi hiç yormadan sayım yapılabilir. MicroPython'a yeniysen MicroPython temelleri yazısı iyi bir başlangıçtır.
Rotary encoder çeşitleri: mekanik, optik ve manyetik
| Tür | Nasıl çalışır? | Tipik çözünürlük | Nerede kullanılır? |
|---|---|---|---|
| Mekanik (KY-040, EC11) | Kontak diski ve iki anahtar | Turda 12–30 tık | Menü düğmeleri, ses ayarı, cihaz ön panelleri |
| Optik | Yarıklı disk, LED ve fototransistör | Turda yüzlerce ile binlerce darbe | CNC tezgâhları, robot eklemleri, endüstriyel motorlar |
| Manyetik (Hall etkili) | Mıknatıslı disk ve Hall sensörleri | Motor milinde turda birkaç darbe (ör. 7 ya da 11) | Encoder'lı N20 ve redüktörlü DC motorlar |
| Mutlak (absolute) | Her konum için ayrı bir kod üretir | Örneğin AS5600'de 12 bit (4096 konum) | Robot kolları, açı ölçümü; açılışta konumu bilir |
Bu yazıdaki kodlar A/B çıkışlı diğer artımlı encoder'larla da çalışır. Örneğin encoder'lı bir redüktörlü DC motorda motor milinde turda 11 darbe veren bir Hall encoder, dört kenarlı sayım ve 1:30 dişli oranıyla çıkış milinin her turunda 11 × 4 × 30 = 1320 sayım üretir. Darbe hızı saniyede binlere çıktığında kesmeli kod işlemcinin zamanının belirgin bir kısmını alır; ESP32'nin PCNT birimi ise bu sayımı zahmetsizce yapar. Endüstriyel optik encoder'lar çoğunlukla 5–24 V ile çalışır ve açık kollektör ya da diferansiyel çıkış verir; bağlamadan önce çıkış tipini ve gerilimini mutlaka datasheet'ten kontrol et.
Rotary encoder ile proje fikirleri
- Dijital ses düğmesi: DFPlayer Mini ile çalan müziğin sesini encoder'la ayarla, mile basınca sonraki şarkıya geç.
- Renk seçici lamba: WS2812B LED halkasında renk tonunu encoder'la döndür, basınca parlaklık moduna geç.
- Hassas konumlandırma: Her tık bir step motoru belirli bir adım ilerletsin; kamera kaydırıcısı ya da döner tabla yap.
- Mutfak zamanlayıcısı: Süreyi encoder'la ayarla, basınca geri sayım başlasın, süre dolunca buzzer çalsın.
- Oyun kumandası: Encoder'ı Pong'da raket olarak kullan; Arduino oyun projelerinde joystick'ten daha hassas kontrol sağlar.
- Saat ve alarm ayarı: RTC saat modülünün saatini ve alarmını tek düğmeyle ayarla.
- Test cihazı paneli: PWM frekansını, sinyal üretecinin değerini ya da bir lehim istasyonunun hedef sıcaklığını ince adımlarla değiştir.
Sık yapılan hatalar ve çözümleri
| Sorun | Muhtemel sebep | Çözüm |
|---|---|---|
| Bir tıkta 2 ya da 4 sayıyor | Her kenar ayrı sayılıyor ya da kontak sıçrıyor | Kenar sayısını 4'e böl, durum tablosu kullan, CLK ve DT'ye 10–100 nF kondansatör ekle |
| Her iki tıkta bir sayıyor | 30 tık / 15 darbeli encoder; her tık yarım döngü | Kütüphane değerini 4 yerine 2'ye böl; tablo kodunda if (ab == 3) koşulunu if (ab == 3 || ab == 0) yap |
| Yön ters | CLK ve DT yer değiştirmiş ya da modül farklı | Koddaki CLK ve DT pin numaralarını yer değiştir |
| Hızlı çevirince adım kaçıyor | Polling kullanılıyor, loop'ta delay var | Kesme ya da ESP32 PCNT kullan, ISR'ı kısa tut |
| Buton kendi kendine basılıyor | SW pini boşta, pull-up yok | pinMode(SW, INPUT_PULLUP) kullan |
| Sayaç rastgele zıplıyor | GND ortak değil, kablolar uzun ya da gevşek | GND'yi ortakla, kabloları kısalt, breadboard temasını kontrol et |
| fatal error: driver/pulse_cnt.h: No such file or directory / undefined reference to 'pcnt_new_unit' | Arduino ESP32 çekirdeği 2.x (ilk hata) ya da PCNT birimi olmayan ESP32-C3 (ikinci hata) | Kart Yöneticisi'nden çekirdeği 3.x'e güncelle ya da ESP32Encoder kütüphanesini kullan; C3'te kesmeli kodu kullan |
| LCD'de yalnızca kareler var | Kontrast ayarı ya da I2C adresi yanlış | Arkadaki trimpotu çevir, adresi 0x27 yerine 0x3F dene |
Rotary encoder, bir projeye profesyonel bir kullanıcı arayüzü kazandırmanın en ucuz yollarından biridir: tek bir düğme, bir ekranla birlikte onlarca ayarı yönetebilir. Önce yoklamalı kodla mantığı kavra, sonra kesmeli ya da PCNT'li sürüme geç ve menü örneğini kendi projene uyarla. BlueGrays uygulamasında potansiyometreyle servo kontrolü, I2C LCD ekran, buton ve 7 segment sayaç projelerini devre şemaları ve adım adım kodlarıyla bulabilir, bu projelerdeki potansiyometre ya da butonun yerine encoder koyarak onları kolayca geliştirebilirsin.
Sıkça sorulan sorular
Rotary encoder ile potansiyometre arasındaki fark nedir?
Potansiyometre milin konumuna göre analog bir gerilim verir, dönüşü yaklaşık 270–300 dereceyle sınırlıdır ve analogRead ile okunur. Rotary encoder ise sınırsız döner ve her tıkta dijital darbeler üretir; konumu değil, hareketin yönünü ve miktarını bildirir. Menü gezinme ve adımlı ayarlar için encoder, konuma bağlı basit ayarlar için potansiyometre daha uygundur.
Rotary encoder neden bir tıkta iki adım sayıyor ya da adım atlıyor?
Fazla saymanın sebebi genellikle kontak sıçraması ya da her kenarın ayrı sayılmasıdır; tık başına 4 kenarı 4'e bölmek ve durum tablosu kullanmak bunu çözer. Adım atlaması ise loop meşgulken gelen darbelerin kaçmasından kaynaklanır. CLK ve DT'yi kesme pinlerine bağlayıp kesmeyle okursan ve gerekirse 10–100 nF kondansatör eklersen sorun ortadan kalkar.
KY-040 rotary encoder 3,3 V ile çalışır mı?
Evet. KY-040 yalnızca mekanik anahtarlardan ve pull-up dirençlerinden oluştuğu için 3,3 V ile de sorunsuz çalışır. ESP32 ve Raspberry Pi Pico'da + pinini mutlaka 3V3'e bağla; 5 V ile beslersen pull-up dirençleri CLK ve DT hatlarını 5 V'a çeker ve 3,3 V'luk girişlerin sınırı aşılır.
KY-040 bir turda kaç adım (pulse) verir?
Yaygın KY-040 modülleri turda 20 tık verir ve her tıkta A ile B hatları bir tam döngü yapar. Her döngüde dört kenar olduğu için turda 80 kenar sayılır ve tık başına açı 18 derecedir. Bazı encoder'lar 30 tık ve 15 darbe verir; bunlarda her tık yarım döngüye karşılık gelir.
Rotary encoder kesme (interrupt) olmadan okunabilir mi?
Evet, loop içinde CLK pinini sürekli kontrol eden yoklama (polling) yöntemiyle okunabilir ve yavaş çevrilen bir menü düğmesi için çoğu zaman yeterlidir. Ancak loop'ta delay, ekran güncelleme ya da uzun hesaplamalar varsa hızlı çevrilen tıklar kaçar. Güvenilir sonuç için kesme, ESP32'de ise PCNT donanım sayacı önerilir.
Arduino'ya birden fazla rotary encoder bağlanabilir mi?
Bağlanabilir. Uno'da yalnızca D2 ve D3 donanım kesmesi verdiği için Encoder kütüphanesiyle her encoder'ın kurucuya ilk verilen pinini kesme pinine, diğerini normal bir pine bağlayabilir ya da pin değişim kesmesi (pin change interrupt) kullanabilirsin. Mega'da altı kesme pini vardır, klasik ESP32'de 8 PCNT birimiyle 8 encoder donanımda sayılabilir, Pico'da ise her GPIO kesme üretebilir.
Bu yazıdan sonra yapabileceğin projeler
Tüm projeleri gör
Buton İle Led Yakma (On Off)
Bir butona bir kez basınca ışık yanar, bir daha basınca söner; tıpkı odandaki lamba düğmesi gibi. Arduino her basışı fark edip LED'in durumunu tersine çevirir, düğme içindeki minik titreşimler ışığı k

ESP32 Buton ile LED Kontrol (Toggle)
Butona her basıldığında LED'in durumu değişir: açıksa kapanır, kapalıysa açılır. Klasik toggle (geçiş) pattern'i. Debounce (sıçrama önleme) ile butonun mekanik gürültüsünü temizleyerek tek basışta tek değişim sağlarız.

Raspberry Pi Pico Buton Debounce Karşılaştırma: IRQ vs Software Polling (GP5 + GP4)
Butona bir kere bastığında kod 3-4 kere saydırdıysa nedeni **contact bounce** — buton metal kontakları temas anında mikrosaniyelerde defalarca aç-kapa yapar! 🔁 Bu projede aynı butonu 2 farklı yöntemle sayıp Serial'da yan yana karşılaştırıyoruz: **RAW polling** (bounce'u yakalar, hatalı sayar) vs **IRQ + software debounce** (doğru sayar). Öğrenme: **contact bounce fiziği** (metal titreşim), **software debounce** (`ticks_diff() > 50ms`), **Pin.irq()** interrupt kaydı, **IRQ_FALLING** trigger tipi,

Potansiyometre İle Servo Motor Kullanım
Potansiyometrenin düğmesini çevirdikçe servo motorun kolu da onunla birlikte döner; düğmenin bir ucu sıfır dereceye, öbür ucu yarım tura karşılık gelir. Arduino düğmenin konumunu sürekli okur ve servo

ESP32 Potansiyometre ile LED Parlaklık (Analog → PWM)
Bu projede bir potansiyometre ile LED parlaklığını analog olarak kontrol ediyoruz. ESP32'nin ADC pini potansiyometreden gelen 0-3.3V gerilimi okur, ardından bu değer PWM (Pulse Width Modulation) sinyaline çevrilerek LED'e gönderilir. Böylece potansiyometreyi çevirdikçe LED kademeli olarak parlar ve söner. Arduino Core 3.0+ ile `analogWrite()` doğrudan kullanılabilir; ESP32-S2/S3'te ADC çözünürlüğü 12-bit'e sabitlenir.

7 Segment Display Ve Buton İle Sayaç
Üç butonlu basit bir sayaç: birine bastıkça ekrandaki rakam bir artar, diğerine bastıkça bir azalır, üçüncüsü sıfıra döndürür. Tek haneli 7 segment ekran 0 ile 9 arasında sayar; sınıra dayandığında kö

I2C İle LCD Ekran Kullanımı
Arduino'ya kendi ekranını kazandır ve ilk mesajını yazdır. İki satırlık LCD ekran yalnızca birkaç kabloyla bağlanır; önce üst satıra bir selam, ardından alt satıra ikinci bir yazı gelir, ekran temizle

Raket Topu Oyunu - SSD1306
Bir top, iki raket ve sen: video oyunlarının en eskilerinden masa tenisini küçük bir OLED ekranda oynarsın. İki butonla sağdaki raketini yukarı aşağı oynatırsın, soldaki raketi ise topu gözüyle izleye
Diğer yazılar
BlueGrays uygulaması
Bu devreyi telefonunda adım adım kur
Devre şeması, kod ve parça listesi tek ekranda. 160'tan fazla Arduino, ESP32 ve Pico projesi seni bekliyor.
Telefonunla tara
Kamerayı koda tut; telefonunun uygulama mağazası açılır.
ya da mağazada aç

