Drehencoder Arduino: KY-040 auslesen mit Interrupt und Menü
Drehencoder am Arduino: KY-040 anschließen, A/B-Quadratursignal auswerten, per Interrupt ohne Schrittverlust zählen, entprellen, LCD-Menü, ESP32 PCNT, Pico.
Ein Drehencoder (Rotary Encoder, auch Drehgeber) ist ein digitaler Einstellknopf für Arduino-Projekte, der sich endlos drehen lässt, pro Rastung einen Schritt zählt und beim Drücken der Achse wie ein Taster funktioniert. Am Arduino verhält sich ein Drehencoder wie der Menüknopf eines 3D-Druckers, der Einstellregler eines Oszilloskops oder der Lautstärkeknopf im Auto: Er weiß, um wie viele Klicks und in welche Richtung du gedreht hast, und stößt nie an einen Anschlag. Das Modell, das dir in Hobby-Sets am häufigsten begegnet, ist das fünfpolige Modul KY-040.
In dieser Anleitung lernst du, wie sich der KY-040 von einem Potentiometer unterscheidet, wie die Drehrichtung aus den Phasen A und B (Quadratur) bestimmt wird, wie du das Kontaktprellen per Hardware und Software filterst und wie du mit Interrupts jeden einzelnen Klick zählst. Danach bauen wir Schritt für Schritt die Abkürzung mit der Encoder-Bibliothek, die Menüsteuerung auf LCD und OLED, den Hardware-Zähler PCNT des ESP32 und MicroPython-Code für den Raspberry Pi Pico.
Was ist ein Drehencoder und wie funktioniert er?
In einem mechanischen Encoder wie dem KY-040 sitzt eine gezahnte Kontaktscheibe, die sich mit der Achse dreht, und zwei kleine Schalter, die über sie gleiten. Beim Drehen öffnen und schließen die beiden Schalter nacheinander, und an den Pins CLK (A) und DT (B) entstehen Rechteckimpulse. Die beiden Signale sind um eine Viertelperiode, also 90 Grad, gegeneinander verschoben; man spricht von einem Quadratursignal. Der Mikrocontroller erkennt die Richtung daran, welches Signal sich zuerst ändert, und die Drehmenge an der Zahl der Änderungen. Der Encoder merkt sich seine Position nicht, er meldet nur Bewegung. Deshalb ist der KY-040 ein inkrementeller Drehgeber: Beim Einschalten ist die Position null, und das Zählen übernimmt komplett dein Code.
| Eigenschaft | Wert beim KY-040 |
|---|---|
| Typ | Mechanisch, inkrementell, Ausgänge A/B |
| Rastungen | 20 Klicks pro Umdrehung (bei manchen Encodern 30) |
| Impulse | 20 volle A/B-Zyklen pro Umdrehung, also 80 Flanken |
| Winkelauflösung | 18 Grad pro Klick |
| Pins | CLK (A), DT (B), SW (Taster), + (Versorgung), GND |
| Versorgung | 3,3–5 V (speist nur die Pull-up-Widerstände) |
| Eingebaute Widerstände | 10-kΩ-Pull-ups an CLK und DT; der SW-Widerstand ist nicht auf jedem Modul bestückt |
| Taster | Schließer; beim Drücken der Achse wird SW mit GND verbunden (aktiv LOW) |
| Drehbereich | Unbegrenzt, kein Anschlag |
Drehencoder oder Potentiometer: Was ist der Unterschied?
Auf den ersten Blick sieht ein Encoder einem Potentiometer sehr ähnlich: Beide haben eine drehbare Achse und beide dienen zum Einstellen eines Wertes. Ihre Funktionsweise ist aber völlig verschieden. Ein Potentiometer ist ein Spannungsteiler; es liefert je nach Stellung der Achse eine analoge Spannung zwischen 0 und 5 V, die du mit analogRead einliest. Ein Encoder erzeugt dagegen Impulse statt einer Spannung; er meldet nicht die Position, sondern die Änderung.

| Eigenschaft | Potentiometer | Drehencoder (KY-040) |
|---|---|---|
| Drehbereich | Etwa 270–300 Grad, begrenzt | Unbegrenzt, durchgehend 360 Grad |
| Ausgang | Analoge Spannung (0–5 V) | Digitale Impulse (A und B) |
| Auslesen | analogRead (ADC) | digitalRead oder Zählen per Interrupt |
| Positionsangabe | Absolut; die Achsstellung liefert den Wert auch nach einem Stromausfall | Relativ; die Position ist beim Einschalten unbekannt |
| Rastgefühl | Keins, dreht gleichmäßig | Ja, meist 20 Klicks pro Umdrehung |
| Taster | Nein | Ja (Achse drücken) |
| Typisches Problem | Mit der Zeit Kratzen und schwankende Werte | Kontaktprellen (per Software lösbar) |
| Typischer Einsatz | Lautstärke, Servowinkel, analoge Einstellung | Menüsteuerung, schrittweise Zahleneinstellung, Gerätebedienfelder |
Kurz gesagt: Wenn der Wert eins zu eins der physischen Achsstellung entsprechen soll (etwa um einen Servoarm von Hand zu führen oder für eine einfache Lautstärkeregelung), ist ein Potentiometer einfacher. Willst du durch ein Menü blättern, eine Zahl in genauen Schritten erhöhen und verringern, mit einem Knopf mehrere Einstellungen ändern oder den Wert auch per Software verändern, ist ein Encoder viel flexibler. Wird zum Beispiel ein Wert von 50 per Handy auf 80 gesetzt, machst du beim Drehen des Encoders einfach bei 80 weiter; beim Potentiometer passen Knopfstellung und angezeigter Wert dann nicht mehr zusammen.
Phasen A und B: So funktioniert die Quadratur-Auswertung
Steht der KY-040 in einer Rastung, sind beide Schalter offen, und dank der Pull-up-Widerstände lesen CLK und DT beide HIGH (1). Drehst du den Knopf um einen Klick, fallen die beiden Leitungen nacheinander auf LOW und steigen nacheinander wieder auf HIGH. Im Uhrzeigersinn fällt zuerst CLK, gegen den Uhrzeigersinn zuerst DT. Diese Folge aus vier Zuständen ist ein Gray-Code: Bei jedem Schritt ändert sich nur ein Bit. Die Tabelle zeigt, was die beiden Pins während eines Klicks lesen:
| Schritt | Im Uhrzeigersinn (CLK, DT) | Gegen den Uhrzeigersinn (CLK, DT) |
|---|---|---|
| In der Rastung | 1, 1 | 1, 1 |
| 1. Übergang | 0, 1 | 1, 0 |
| 2. Übergang | 0, 0 | 0, 0 |
| 3. Übergang | 1, 0 | 0, 1 |
| 4. Übergang (nächster Klick) | 1, 1 | 1, 1 |
Aus der Tabelle ergeben sich zwei einfache Regeln. Erstens: Ist DT noch HIGH, wenn CLK fällt, dreht der Knopf im Uhrzeigersinn; ist DT schon LOW, dreht er gegen den Uhrzeigersinn. Zweitens: Ein Klick hat vier Übergänge, 20 Klicks pro Umdrehung bedeuten also 80 Übergänge. Einfacher Code nutzt nur die erste Regel und zählt einmal pro Klick. Robuster Code verfolgt alle vier Übergänge und kann so prellende oder halb ausgeführte Bewegungen aussortieren. Welche Richtung als Uhrzeigersinn gilt, kann je nach Modul und Verdrahtung variieren; zählt dein Zähler falsch herum, tausche einfach die Pins CLK und DT im Code.
KY-040 Pinbelegung und Anschluss
| KY-040-Pin | Funktion | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|---|
| CLK | Phase A | D2 | GPIO 25 | GP16 |
| DT | Phase B | D3 | GPIO 26 | GP17 |
| SW | Taster (gedrückt LOW) | D4 | GPIO 27 | GP18 |
| + | Versorgung | 5V | 3V3 | 3V3(OUT) |
| GND | Masse | GND | GND | GND |
Beim Arduino Uno und Nano gibt es Hardware-Interrupts nur an den Pins D2 und D3, daher schließt du CLK und DT am besten dort an. Beim Mega sind 2, 3, 18, 19, 20 und 21 Interrupt-Pins, beim Leonardo 0, 1, 2, 3 und 7. Beim ESP32 und Pico kann fast jeder GPIO einen Interrupt auslösen. Die Pinbelegung deines Boards findest du im Tool Pinout. Verwende für den SW-Pin im Code INPUT_PULLUP: Manche Module haben an dieser Leitung keinen Widerstand, und ein offener Pin liest sich, als würde zufällig gedrückt. Beim ESP32 sind GPIO 34–39 reine Eingänge ohne internen Pull-up; nimm sie nicht für SW.
Erster Test Schritt für Schritt: Auslesen per Polling
- Verbinde CLK des KY-040 mit D2, DT mit D3 und SW mit D4.
- Verbinde + mit 5V und GND mit GND des Arduino.
- Füge den folgenden Code in die Arduino IDE ein und lade ihn auf das Board.
- Öffne den seriellen Monitor und stelle die Geschwindigkeit auf 115200 Baud.
- Drehe den Knopf ein paar Klicks in beide Richtungen und drücke dann die Achse, um den Zähler zurückzusetzen.
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}Der Code liest CLK bei jedem loop-Durchlauf und erkennt nur den Moment, in dem CLK von HIGH auf LOW fällt. Ist DT in diesem Moment noch HIGH, wurde im Uhrzeigersinn gedreht, ist es LOW, gegen den Uhrzeigersinn. Diese Methode ist am leichtesten zu verstehen, hat aber zwei Schwachstellen. Erstens gehen Impulse verloren, die ankommen, während loop mit anderen Aufgaben beschäftigt ist (Display aktualisieren, delay, Sensoren lesen), und bei schnellem Drehen hinkt der Zähler hinterher. Zweitens prellen mechanische Kontakte: CLK kann beim einmaligen Fallen mehrmals LOW-HIGH wechseln, und der Code zählt einen Klick womöglich doppelt oder dreifach. Die nächsten beiden Abschnitte lösen beide Probleme.
Kontaktprellen: Entprellen per Hardware und Software
Die Metallfedern im Encoder springen beim Berühren des Kontakts ein paarmal zurück, bevor sie aufliegen. In Herstellerdatenblättern wird diese Zeit meist mit wenigen Millisekunden angegeben (zum Beispiel höchstens 2 ms). Bei Tastern löst du das Prellen mit "nach der letzten Änderung 20–50 ms warten", beim Encoder funktioniert das aber nicht: Wird er schnell gedreht, liegen zwischen zwei Flanken nur wenige Millisekunden, und eine lange Wartezeit verschluckt auch echte Schritte. Stattdessen nutzt man diese Methoden:
- Hardware-Filter: Setze je einen 10–100-nF-Kondensator zwischen CLK und GND sowie zwischen DT und GND. Zusammen mit den 10-kΩ-Pull-ups des Moduls entsteht ein RC-Filter; die Zeitkonstante liegt zwischen 10 kΩ × 10 nF = 0,1 ms und 10 kΩ × 100 nF = 1 ms und schluckt kurze Preller. Berechnen kannst du sie mit dem Tool RC-Zeitkonstante, und einen Kondensator mit dem Aufdruck 103 oder 104 entschlüsselst du mit dem Tool Kondensator-Code.
- Zustandstabelle: Bei jeder Flanke beider Leitungen werden alter und neuer AB-Zustand verglichen. Prellen bedeutet Hin- und Herspringen zwischen denselben zwei Zuständen; das ergibt abwechselnd +1 und −1, was sich aufhebt. Übergänge, bei denen sich beide Bits gleichzeitig ändern, gelten als ungültig und werden übersprungen.
- Nur in der Rastung entscheiden: Die Viertelschritte werden gesammelt, und der Zähler wird erst erhöht oder verringert, wenn der Encoder in eine Rastung einrastet (beide Leitungen HIGH). So lässt ein auf der Rastung zitternder Kontakt den Menücursor nicht springen.
- Sperrzeit für den Taster: Der SW-Pin verhält sich wie ein gewöhnlicher Taster; eine Sperrzeit von 30 ms mit millis() genügt.
Mit Interrupts auslesen und keinen Schritt verpassen
Bei einem Interrupt unterbricht der Prozessor sofort seine Arbeit, wenn sich ein Pin ändert, und führt eine kurze Funktion (ISR) aus. Hängen die Encoder-Leitungen an Interrupts, geht keine Flanke verloren, selbst wenn in loop ein delay von 50 ms steht oder das Display langsam aktualisiert wird. Der folgende Code läuft an beiden Flanken von CLK und DT, nutzt die oben beschriebene Zustandstabelle und zählt erst, wenn der Knopf in eine Rastung einrastet. Die Grundlagen findest du auch im Artikel Arduino Interrupt.
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}| Codeteil | Was macht er? |
|---|---|
| Tabelle QEM[16] | Macht aus altem und neuem AB-Zustand +1, −1 oder 0; ungültige Übergänge, bei denen sich beide Leitungen gleichzeitig ändern, zählen als 0 |
| attachInterrupt(..., CHANGE) | Ruft readEncoder() bei steigender und fallender Flanke von CLK und DT auf |
| quarter | Sammelt die Viertelschritte zwischen zwei Klicks; durch Prellen entstehende +1/−1-Paare heben sich auf |
| if (ab == 3) | Entschieden wird nur, wenn der Encoder in einer Rastung steht (beide Leitungen HIGH); eine Summe von +2 oder mehr zählt als ein Klick |
| volatile long position | Die von ISR und loop gemeinsam genutzte Variable; volatile verhindert, dass der Compiler eine veraltete Kopie verwendet |
| noInterrupts() / interrupts() | Verhindert, dass die ISR dazwischenfunkt, während der 4 Byte lange long-Wert kopiert wird |
| buttonPressed() | Entprellt SW mit 30 ms Sperrzeit und liefert nur einmal, im Moment des Drückens, true |
Die Abkürzung: die Encoder-Bibliothek
Wenn du keine eigene ISR schreiben willst, erledigt die Encoder-Bibliothek von Paul Stoffregen dieselbe Aufgabe mit optimiertem Code. Öffne in der Arduino IDE den Bibliotheksverwalter, suche nach "Encoder" und installiere die Bibliothek von Paul Stoffregen; die einzelnen Schritte zeigt die Anleitung Arduino-Bibliothek hinzufügen. Die Bibliothek arbeitet am besten, wenn beide Pins Interrupt-Pins sind, gut, wenn nur der zuerst an den Konstruktor übergebene Pin einer ist, und mit geringer Leistung, wenn keiner es ist. Da sie jede Flanke zählt, ändert sich der Wert pro Klick um 4; teile durch 4, um die Klicks zu erhalten.
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() liefert die bisherige Gesamtzahl der Flanken, write(0) setzt den Zähler zurück, und readAndReset() liest und setzt in einem Schritt zurück. Dem Konstruktor übergeben wir zuerst DT und dann CLK wegen des Vorzeichens der Drehrichtung: In dieser Reihenfolge zählt der Uhrzeigersinn positiv. Ist es bei dir umgekehrt, tausche 2 und 3. Die internen Pull-up-Widerstände der Pins schaltet die Bibliothek selbst ein.
Menüsteuerung mit Drehencoder auf einem LCD

Am häufigsten werden Encoder für Menüs verwendet: Du drehst den Knopf, um zwischen den Zeilen zu wechseln, drückst ihn, um die ausgewählte Einstellung in den Bearbeitungsmodus zu setzen, drehst, um den Wert zu ändern, und drückst noch einmal, um ins Menü zurückzukehren. Das folgende Beispiel stellt den roten, grünen und blauen Kanal einer RGB-LED zwischen 0 und 255 ein und zeigt die Werte auf einem I2C-LCD an. Schließe das LCD an A4 (SDA) und A5 (SCL) an und die Farbbeinchen einer RGB-LED mit gemeinsamer Kathode über 220-Ω-Widerstände an D9, D10 und D11.
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}Am Anfang der ausgewählten Zeile erscheint ein >, im Bearbeitungsmodus ein *. Der Encoder-Code ist derselbe wie im vorigen Beispiel; der einzige Unterschied ist, dass die ISR statt einer Position die Zahl der noch nicht verarbeiteten Klicks (steps) sammelt und loop sie ausliest und zurücksetzt. So geht kein Klick verloren, auch wenn loop eine Weile beschäftigt ist; die gesammelten Klicks werden im nächsten Durchlauf zusammen verarbeitet. Im Bearbeitungsmodus ändert jeder Klick den Wert um 5; für feinere Einstellungen kannst du den Schritt auf 1 senken oder eine Beschleunigung einbauen, die den Schritt bei schnellem Drehen vergrößert.
Um dasselbe Menü auf einem OLED-Display anzuzeigen, musst du nur die Funktion drawMenu() austauschen. Die folgende Version zeichnet die ausgewählte Zeile invertiert auf einem weißen Balken. Binde statt der LCD-Bibliothek die Bibliotheken Adafruit SSD1306 und Adafruit GFX ein, lösche das lcd-Objekt und rufe in setup() display.begin() statt lcd.init() auf.
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}Drehencoder am ESP32: der PCNT-Hardware-Zähler
Der ESP32 besitzt einen Hardware-Block namens PCNT (Pulse Counter), der Impulse zählt, ohne den Prozessor zu belasten. Jede Einheit hat zwei Kanäle und kann das A/B-Signal daher direkt auswerten; der klassische ESP32 hat 8 PCNT-Einheiten, ESP32-S2 und S3 haben 4. Der Zähler ist 16 Bit breit und enthält einen Glitch-Filter gegen kurze Störungen. Dieser Filter lässt sich auf höchstens etwa 12,8 Mikrosekunden einstellen und schluckt daher nur sehr kurze Spitzen, nicht das gesamte Prellen. Den Rest gleicht die Vierflanken-Zählung von selbst aus: Hin- und Herpreller heben sich als +1 und −1 auf. Der folgende Code nutzt den ESP-IDF-Treiber, der mit Version 3.x des Arduino-ESP32-Cores mitkommt.
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}Die beiden Kanäle werden spiegelbildlich eingerichtet: Kanal A zählt die Flanken von CLK und kehrt die Richtung je nach Pegel von DT um, Kanal B zählt die Flanken von DT bezogen auf CLK. Dadurch ändert sich der Zähler pro Klick um 4. Erreicht er die Grenze von ±10000, springt er auf null zurück; brauchst du einen größeren Bereich, kannst du Überläufe mit der Option flags.accum_count und Watch Points aufsummieren. Der ESP32-C3 hat keine PCNT-Einheit; nutze auf diesem Board den Arduino-Code mit Interrupts. Arbeitest du mit Version 2.x des Arduino-ESP32-Cores oder willst du kürzeren Code, erledigt die Bibliothek ESP32Encoder mit derselben Hardware die Aufgabe über attachFullQuad() und getCount(). Nutzt du MicroPython auf dem ESP32, verwendet auch die Klasse machine.Encoder aktueller Versionen (z. B. 1.27) im Hintergrund PCNT. Bist du neu beim ESP32, lies zuerst Was ist der ESP32.
Drehencoder mit dem Raspberry Pi Pico und MicroPython
Jeder GPIO-Pin des Pico kann einen Interrupt auslösen, daher ist das Auslesen eines Encoders in MicroPython mit Pin.irq() einfach. Der folgende Code nutzt genau dieselbe Zustandstabelle wie das Arduino-Beispiel und macht aus dem Encoder einen LED-Dimmer: Jeder Klick ändert die Helligkeit um 5 Prozent, ein Druck auf die Achse schaltet die LED ein und aus. Verbinde CLK mit GP16, DT mit GP17, SW mit GP18 und eine LED über einen 220-Ω-Widerstand mit GP15. Wie du MicroPython einrichtest, erfährst du im Artikel Was ist der 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)Die Grenzen des Wertes (0–20 Klicks) prüfen wir in der Interrupt-Funktion; so liest die Hauptschleife die Variable nur, und es entsteht keine Race Condition, bei der beide Seiten gleichzeitig schreiben. Wie die LED-Helligkeit gesteuert wird, erklärt der Artikel Was ist PWM. Möchtest du eine fertige Lösung, bringt das Modul rotary_irq_rp2 aus dem Projekt micropython-rotary Grenzen, Überlauf (Wrap) und Richtungsumkehr mit. Für sehr schnelle Motor-Encoder kann der PIO-Block des Pico zählen, ohne den Prozessor zu belasten. Bist du neu bei MicroPython, ist der Artikel MicroPython-Grundlagen ein guter Einstieg.
Arten von Drehencodern: mechanisch, optisch und magnetisch
| Typ | Wie funktioniert er? | Typische Auflösung | Wo wird er eingesetzt? |
|---|---|---|---|
| Mechanisch (KY-040, EC11) | Kontaktscheibe und zwei Schalter | 12–30 Klicks pro Umdrehung | Menüknöpfe, Lautstärkeregler, Gerätefrontplatten |
| Optisch | Schlitzscheibe, LED und Fototransistor | Hunderte bis Tausende Impulse pro Umdrehung | CNC-Maschinen, Robotergelenke, Industriemotoren |
| Magnetisch (Hall-Effekt) | Magnetscheibe und Hall-Sensoren | Wenige Impulse pro Umdrehung der Motorwelle (z. B. 7 oder 11) | N20- und Getriebe-DC-Motoren mit Encoder |
| Absolut | Gibt für jede Position einen eigenen Code aus | Zum Beispiel 12 Bit (4096 Positionen) beim AS5600 | Roboterarme, Winkelmessung; kennt die Position beim Einschalten |
Der Code in diesem Artikel funktioniert auch mit anderen inkrementellen Encodern mit A/B-Ausgängen. Bei einem Getriebemotor mit Encoder ergeben zum Beispiel ein Hall-Encoder mit 11 Impulsen pro Umdrehung der Motorwelle, Vierflanken-Zählung und ein Getriebe von 1:30 genau 11 × 4 × 30 = 1320 Zählschritte pro Umdrehung der Abtriebswelle. Steigt die Impulsrate auf Tausende pro Sekunde, beansprucht Interrupt-Code einen spürbaren Teil der Prozessorzeit, während die PCNT-Einheit des ESP32 das mühelos zählt. Industrielle optische Encoder arbeiten meist mit 5–24 V und haben Open-Collector- oder Differenzausgänge; prüfe Ausgangstyp und Spannung vor dem Anschließen unbedingt im Datenblatt.
Projektideen mit Drehencoder
- Digitaler Lautstärkeregler: Stelle die Lautstärke der Musik eines DFPlayer Mini mit dem Encoder ein und springe per Achsendruck zum nächsten Titel.
- Farbwähler-Lampe: Drehe den Farbton eines WS2812B-LED-Rings mit dem Encoder und wechsle per Druck in den Helligkeitsmodus.
- Präzise Positionierung: Lass jeden Klick einen Schrittmotor um eine feste Schrittzahl bewegen; baue einen Kamera-Slider oder einen Drehteller.
- Küchentimer: Stelle die Zeit mit dem Encoder ein, starte den Countdown per Druck und lass am Ende einen Summer ertönen.
- Spielcontroller: Nutze den Encoder als Schläger in Pong; bei Arduino-Spieleprojekten erlaubt er feinere Steuerung als ein Joystick.
- Uhrzeit und Wecker stellen: Stelle Uhrzeit und Alarm eines RTC-Uhrmoduls mit einem einzigen Knopf ein.
- Bedienfeld für Messgeräte: Ändere eine PWM-Frequenz, den Wert eines Signalgenerators oder die Solltemperatur einer Lötstation in feinen Schritten.
Häufige Fehler und ihre Lösungen
| Problem | Wahrscheinliche Ursache | Lösung |
|---|---|---|
| Zählt 2 oder 4 pro Klick | Jede Flanke wird einzeln gezählt oder die Kontakte prellen | Flankenzahl durch 4 teilen, Zustandstabelle verwenden, 10–100-nF-Kondensatoren an CLK und DT |
| Zählt nur jeden zweiten Klick | Encoder mit 30 Klicks / 15 Impulsen; jeder Klick ist ein halber Zyklus | Bibliothekswert durch 2 statt 4 teilen; im Tabellencode if (ab == 3) zu if (ab == 3 || ab == 0) ändern |
| Richtung vertauscht | CLK und DT vertauscht oder anderes Modul | Pinnummern von CLK und DT im Code tauschen |
| Bei schnellem Drehen gehen Schritte verloren | Polling, delay in loop | Interrupts oder ESP32 PCNT nutzen, ISR kurz halten |
| Taster löst von selbst aus | SW-Pin offen, kein Pull-up | pinMode(SW, INPUT_PULLUP) verwenden |
| Zähler springt zufällig | Keine gemeinsame Masse, lange oder lose Kabel | GND verbinden, Kabel kürzen, Steckbrett-Kontakte prüfen |
| fatal error: driver/pulse_cnt.h: No such file or directory / undefined reference to 'pcnt_new_unit' | Arduino-ESP32-Core 2.x (erster Fehler) oder ESP32-C3 ohne PCNT-Einheit (zweiter Fehler) | Core im Boardverwalter auf 3.x aktualisieren oder die Bibliothek ESP32Encoder nutzen; beim C3 den Interrupt-Code nutzen |
| LCD zeigt nur Kästchen | Kontrast oder I2C-Adresse falsch | Trimmer auf der Rückseite drehen, Adresse 0x3F statt 0x27 probieren |
Ein Drehencoder ist eine der günstigsten Möglichkeiten, einem Projekt eine professionelle Bedienoberfläche zu geben: Ein einziger Knopf und ein Display verwalten Dutzende Einstellungen. Verstehe zuerst mit dem Polling-Code die Logik, wechsle dann zur Interrupt- oder PCNT-Version und passe das Menübeispiel an dein eigenes Projekt an. In der BlueGrays-App findest du Projekte zu Servosteuerung per Potentiometer, I2C-LCD, Taster und 7-Segment-Zähler mit Schaltplänen und Schritt-für-Schritt-Code und kannst sie leicht erweitern, indem du Potentiometer oder Taster durch einen Encoder ersetzt.
Häufige Fragen
Was ist der Unterschied zwischen Drehencoder und Potentiometer?
Ein Potentiometer liefert eine analoge Spannung abhängig von der Achsstellung, sein Drehbereich ist auf etwa 270–300 Grad begrenzt und es wird mit analogRead gelesen. Ein Drehencoder dreht sich endlos und erzeugt bei jedem Klick digitale Impulse; er meldet Richtung und Umfang der Bewegung, nicht die Position. Für Menüs und schrittweise Einstellungen eignet sich der Encoder, für einfache positionsabhängige Einstellungen das Potentiometer.
Warum zählt mein Drehencoder zwei Schritte pro Klick oder überspringt Schritte?
Zu viele Zählschritte entstehen meist durch Kontaktprellen oder weil jede Flanke einzeln gezählt wird; die 4 Flanken pro Klick durch 4 zu teilen und eine Zustandstabelle zu verwenden, behebt das. Übersprungene Schritte entstehen, wenn Impulse ankommen, während loop beschäftigt ist. Schließe CLK und DT an Interrupt-Pins an, lies sie per Interrupt und ergänze bei Bedarf 10–100-nF-Kondensatoren, dann ist das Problem gelöst.
Funktioniert der Drehencoder KY-040 mit 3,3 V?
Ja. Der KY-040 besteht nur aus mechanischen Schaltern und Pull-up-Widerständen und funktioniert daher auch mit 3,3 V einwandfrei. Beim ESP32 und Raspberry Pi Pico schließt du den +-Pin unbedingt an 3V3 an; bei 5 V ziehen die Pull-ups CLK und DT auf 5 V, und die Grenze der 3,3-V-Eingänge wird überschritten.
Wie viele Impulse pro Umdrehung liefert der KY-040?
Übliche KY-040-Module haben 20 Klicks pro Umdrehung, und die Leitungen A und B durchlaufen pro Klick einen vollen Zyklus. Jeder Zyklus hat vier Flanken, also zählst du 80 Flanken pro Umdrehung, und ein Klick entspricht 18 Grad. Manche Encoder haben 30 Klicks und 15 Impulse; dort entspricht jeder Klick einem halben Zyklus.
Kann man einen Drehencoder ohne Interrupt auslesen?
Ja, per Polling, also durch ständiges Prüfen des CLK-Pins in loop, und für einen langsam gedrehten Menüknopf reicht das oft aus. Enthält loop aber delay, Display-Aktualisierungen oder lange Berechnungen, gehen schnell gedrehte Klicks verloren. Für zuverlässige Ergebnisse empfehlen sich Interrupts, beim ESP32 der PCNT-Hardware-Zähler.
Kann ich mehrere Drehencoder an einen Arduino anschließen?
Ja. Da beim Uno nur D2 und D3 Hardware-Interrupts bieten, kannst du mit der Encoder-Bibliothek den zuerst an den Konstruktor übergebenen Pin jedes Encoders an einen Interrupt-Pin und den anderen an einen normalen Pin anschließen oder Pin-Change-Interrupts verwenden. Der Mega hat sechs Interrupt-Pins, der klassische ESP32 kann mit seinen 8 PCNT-Einheiten 8 Encoder in Hardware zählen, und beim Pico kann jeder GPIO einen Interrupt auslösen.
Projekte, die du danach bauen kannst
Alle Projekte ansehen
LED mit Taster ein- und ausschalten
Einmal drücken und das Licht geht an, noch einmal drücken und es geht aus, genau wie der Lichtschalter in deinem Zimmer. Der Arduino bemerkt jeden Druck und schaltet die LED in den jeweils anderen Zus

ESP32 LED Steuerung mit Taster (Toggle)
Jedes Mal, wenn die Taste gedrückt wird, ändert sich der Zustand der LED: ist sie an, schaltet sie aus, ist sie aus, schaltet sie ein. Klassisches Toggle-Muster. Mit der Entprellung wird das mechanische Geräusch des Tasters beseitigt, um ein einmaliges Umschalten mit einem einzigen Druck zu gewährleisten.

Raspberry Pi Pico Taster Debounce: IRQ vs Software Polling (GP5 + GP4)
Einmal Taster gedrückt, Code zählt 3-4? Das ist Contact Bounce! 🔁 Vergleiche 2 Zählmethoden in Serial: RAW Polling (fängt Bounce, zählt falsch) vs IRQ + Software Debounce (genau). Lerne Contact Bounce Physik, Software Debounce, Pin.irq() Interrupt, IRQ_FALLING Trigger, ISR Handler, global Variablen ISR-Sicherheit.

Servomotorbetrieb mit Potentiometer
Während du am Knopf des Potentiometers drehst, dreht sich der Arm des Servomotors einfach mit: Das eine Ende des Knopfes bedeutet null Grad, das andere eine halbe Umdrehung. Der Arduino liest die Stel

ESP32: LED-Helligkeit mit Potentiometer (Analog → PWM)
In diesem Projekt steuern wir die Helligkeit einer LED analog über ein Potentiometer. Der ADC-Pin des ESP32 liest die vom Potentiometer kommende Spannung von 0-3,3 V aus, woraufhin dieser Wert in ein PWM-Signal (Pulsweitenmodulation) umgewandelt und an die LED gesendet wird. So leuchtet die LED beim Drehen des Potentiometers stufenweise auf und erlischt wieder. Mit Arduino Core 3.0+ kann `analogWrite()` direkt verwendet werden; bei ESP32-S2/S3 ist die ADC-Auflösung auf 12 Bit festgelegt.

Zähler mit 7-Segment-Anzeige und Tastern
Ein einfacher Zähler mit drei Knöpfen: Mit dem einen zählt die Ziffer auf der Anzeige um eins hoch, mit dem anderen um eins runter, und der dritte setzt sie auf null zurück. Die einstellige 7-Segment-

Verwendung des LCD-Bildschirms mit I2C
Gib deinem Arduino ein eigenes Display und lass es seine erste Nachricht schreiben. Das zweizeilige LCD wird mit nur wenigen Kabeln angeschlossen; oben erscheint ein Gruß, dann unten eine zweite Zeile

Paddle-Ball-Spiel - SSD1306
Ein Ball, zwei Schläger und du: Du spielst Tischtennis, eines der allerersten Videospiele, auf einem kleinen OLED-Display. Mit zwei Knöpfen bewegst du deinen Schläger rechts nach oben und unten, den l
Weitere Artikel
BlueGrays-App
Diese Schaltung Schritt für Schritt auf dem Handy nachbauen
Schaltplan, Code und Teileliste auf einem Bildschirm. Über 160 Arduino-, ESP32- und Pico-Projekte zum Nachbauen.
Mit dem Handy scannen
Richte die Kamera auf den Code, dann öffnet sich der App-Store deines Handys.
oder im Store öffnen

