Parçalar · 11 dk okuma

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.

ÖzellikKY-040 değeri
TürMekanik, 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
PinlerCLK (A), DT (B), SW (buton), + (besleme), GND
Besleme3,3–5 V (yalnızca pull-up dirençlerini besler)
Dahili dirençlerCLK ve DT'de 10 kΩ pull-up; SW direnci her modülde takılı değil
ButonNormalde 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.

Potansiyometre ile rotary encoder karşılaştırması için potansiyometre
Potansiyometre: dönüşü yaklaşık 270–300 dereceyle sınırlıdır ve konumu analog gerilim olarak verir. Rotary encoder ise durmadan döner ve dijital darbe üretir.
ÖzellikPotansiyometreRotary 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)
OkumaanalogRead (ADC)digitalRead ya da kesme ile sayma
Konum bilgisiMutlak; elektrik kesilse de mil konumu değeri verirGöreli; açılışta konum bilinmez
Tık hissiYok, pürüzsüz dönerVar, turda genellikle 20 tık
ButonYokVar (mile basınca)
Tipik sorunZamanla cızırtı ve değer oynamasıKontak sıçraması (yazılımla çözülür)
Tipik kullanımSes seviyesi, servo açısı, analog ayarMenü 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ımSaat yönü (CLK, DT)Ters yön (CLK, DT)
Tıkta duruyor1, 11, 1
1. geçiş0, 11, 0
2. geçiş0, 00, 0
3. geçiş1, 00, 1
4. geçiş (sonraki tık)1, 11, 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 piniGöreviArduino UnoESP32Raspberry Pi Pico
CLKA fazıD2GPIO 25GP16
DTB fazıD3GPIO 26GP17
SWButon (basınca LOW)D4GPIO 27GP18
+Besleme5V3V33V3(OUT)
GNDToprakGNDGNDGND
ESP32 ve Raspberry Pi Pico'da modülün + pinini mutlaka 3,3 V'a bağla. KY-040'taki pull-up dirençleri + pinine gittiği için modülü 5 V ile beslersen CLK ve DT hatları 5 V'a çekilir ve 3,3 V'luk girişlerin sınırı aşılır. Encoder yalnızca anahtarlardan oluştuğu için 3,3 V ile sorunsuz çalışır.

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

  1. KY-040'ın CLK pinini D2'ye, DT pinini D3'e, SW pinini D4'e bağla.
  2. + pinini 5V'a, GND pinini Arduino'nun GND pinine bağla.
  3. Aşağıdaki kodu Arduino IDE'ye yapıştır ve karta yükle.
  4. Seri Monitör'ü aç ve hızı 115200 baud yap.
  5. Düğmeyi iki yöne birkaç tık çevir, sonra mile basarak sayacı sıfırla.
Arduino (C++)
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.

Arduino (C++)
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] tablosuEski 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 positionISR 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
ISR'ı olabildiğince kısa tut: içinde Serial.print, delay ya da ekran komutu kullanma; yalnızca sayacı güncelle ve asıl işi loop'a bırak. ISR ile loop arasında paylaşılan her değişkeni volatile tanımla. Uno 8 bitlik bir işlemci olduğu için 2 ya da 4 baytlık bir değişkeni okurken kesmeleri kısa süre kapatmak, yarım güncellenmiş bir değeri okumanı önler.

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.

Arduino (C++)
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

I2C 2x16 LCD ekran
I2C 2x16 LCD ekran: yalnızca SDA ve SCL kablolarıyla bağlanır; menü örneğindeki ayarlar bu ekranda gösteriliyor.

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.

Arduino (C++)
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.

Arduino (C++)
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}
Ayarların elektrik kesilince kaybolmasını istemiyorsan düzenleme modundan çıkarken değeri EEPROM'a yaz ve açılışta geri oku. EEPROM'un yazma ömrü sınırlı olduğu için her tıkta değil, yalnızca butona basıldığında kaydet. Uzun basışı "geri" komutu olarak kullanmak da menüleri derinleştirmenin kolay bir yoludur.

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.

Arduino (C++)
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.

MicroPython
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ürNasıl çalışır?Tipik çözünürlükNerede kullanılır?
Mekanik (KY-040, EC11)Kontak diski ve iki anahtarTurda 12–30 tıkMenü düğmeleri, ses ayarı, cihaz ön panelleri
OptikYarıklı disk, LED ve fototransistörTurda yüzlerce ile binlerce darbeCNC tezgâhları, robot eklemleri, endüstriyel motorlar
Manyetik (Hall etkili)Mıknatıslı disk ve Hall sensörleriMotor 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

SorunMuhtemel sebepÇözüm
Bir tıkta 2 ya da 4 sayıyorHer kenar ayrı sayılıyor ya da kontak sıçrıyorKenar 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ıyor30 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 tersCLK 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çıyorPolling kullanılıyor, loop'ta delay varKesme ya da ESP32 PCNT kullan, ISR'ı kısa tut
Buton kendi kendine basılıyorSW pini boşta, pull-up yokpinMode(SW, INPUT_PULLUP) kullan
Sayaç rastgele zıplıyorGND ortak değil, kablolar uzun ya da gevşekGND'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 varKontrast ayarı ya da I2C adresi yanlışArkadaki trimpotu çevir, adresi 0x27 yerine 0x3F dene
Devreyi kurmadan önce kodu denemek istersen Wokwi simülatöründe KY-040 parçası hazır bulunur: encoder'a tıkladıktan sonra ok tuşlarıyla tık tık döndürebilir, boşluk tuşuyla butona basabilirsin. Kurulum için Wokwi simülasyon rehberine bakabilirsin.

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.

Tüm projeleri gör
Buton İle Led Yakma (On Off)
#10Başlangıç

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

4.8(100)
162
ESP32 Buton ile LED Kontrol (Toggle)
#105Başlangıç

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.

4.6(39)
57
Raspberry Pi Pico Buton Debounce Karşılaştırma: IRQ vs Software Polling (GP5 + GP4)
#159Orta
Premium

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,

0.0(0)
12
Potansiyometre İle Servo Motor Kullanım
#36Başlangıç
Premium

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

4.6(37)
52
ESP32 Potansiyometre ile LED Parlaklık (Analog → PWM)
#115Orta
Premium

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.

4.5(19)
32
7 Segment Display Ve Buton İle Sayaç
#43İleri
Premium

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ö

4.6(35)
47
I2C İle LCD Ekran Kullanımı
#32Başlangıç
Premium

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

4.8(53)
70
Raket Topu Oyunu - SSD1306
#86Orta
Premium

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

4.5(47)
65