A03|01 Der Drehrichtungs- und Impuls-Sensor
Rotary Encoder Module

Sensorwerte / Drehbewegungen messen

Mit diesem Drehgeber Modul (Drehrichtungs- und Impuls-Sensor) kannst du vier Dinge messen: Du kannst bei Drehbewegungen die Winkelposition bestimmen, ein Richtungswechsel erkennen, die Drehgeschwindigkeit berechnen und den Drehgeber durch Drücken als Taster benutzen.

Der Drehgeber wandelt eine Drehbewegung in ein digitales Ausgangssignal um, das anschließend vom Arduino verarbeitet werden kann. Das Grundprinzip des Drehgebers ist die Umwandlung von mechanischen Bewegungen in elektrische Signale.

 

Spezifikationen
- Signalart: digital
- Spannung: 3,3V – 5V
- Pinabstand: 2,54mm

Encoder, Drehgeber, Winkelmesser oder Winkelgeber arbeiten alle auf Basis einer mechanischen Bewegung (Rotation), die in ein elektrisches Ausgangssignal umgewandelt wird. Durch eine Befestigung kann der Encoder schnell und mühelos mit dem drehenden Element verbunden werden.

Ein typischer Einsatz könnte ein Dreh-Rücksteller sein, der im Auto zur Steuerung des User-Interfaces eingesetzt wird. Durch Drehen können Elemente in einer Liste durchlaufen und mit einem Druck ein Listenelement aktiviert werden. In vielen Produktionsanlagen und Maschinen kommen solche Drehgeber zum Einsatz, um Positionieraufgaben sicher und genau zu realisieren. Aufgrund ihrer Robustheit und der feinen digitalen Steuerung werden sie in vielen Anwendungen wie der Robotik, in CNC-Maschinen, in Druckern oder als Scrollrad in Computermäusen eingesetzt.

Äußerlich ähnelt sich der Drehgeber dem Potentiometer. Intern sind beide Komponenten jedoch komplett anders aufgebaut. Drehgeber sind das moderne digitale Äquivalent des Potentiometers und vielseitiger als diese. Sie können sich vollständig und ohne Endanschläge drehen, während ein Potentiometer sich nur um etwa 270° drehen kann.

Potentiometer eignen sich am besten für Situationen, in denen die genaue Position des Drehknopfes bekannt sein muss. Die Endposition eines Potentiometers hat haptische Vorteile: Ohne hinzusehen, spürt man, dass das Minimum oder Maximum erreicht wurde, sobald der Endanschlag erreicht wird. Drehgeber hingegen eignen sich am besten für Projekte, in denen man nicht die exakte Position, sondern die Positionsänderung kennen muss.

Funktionsprinzip von Drehgebern

Der interne Aufbau des Drehgebers ist einfach wie genial. Die Rechteck-Signale werden durch die zwei Kontaktstifte A, B und eine Scheibe mit gleichmäßig verteilten Kontaktzonen erzeugt. Die Scheibe ist dauerhaft mit dem Erdungsstift C verbunden.

 
 

Wenn sich die Scheibe schrittweise zu drehen beginnt, werden die Stifte A und B mit dem gemeinsamen Erdungsstift C in Kontakt gebracht und die beiden Rechteck-Ausgangssignale werden entsprechend erzeugt.

Jeder der beiden einzelnen Signale (A oder B) kann zur Bestimmung der Drehposition verwendet werden, wenn wir die Impulse des Signals zählen. Soll jedoch auch die Drehrichtung bestimmt werden, müssen beide Signale gleichzeitig betrachtet werden. Je nachdem in welcher Richtung gedreht wird, kommt entweder Kontakt A oder B zuerst in Berührung, mit dem Erdungsstift in Berührung. Die Reihenfolge ist von entscheidender Bedeutung für die Richtungsbestimmung. Wird der Drehgeber im Uhrzeigersinn gedreht, verbindet sich zuerst der A-Stift mit C - danach der B-Stift. Gegen den Uhrzeigersinn, wird zuerst der B-Stift verbunden, danach der A-Stift. Bei weiterer Drehung sind schließlich beide Stifte A und B mit der gemeinsamen Masse C in Kontakt. Die Kontakte kommen zeitversetzt mit der Metallscheibe in Kontakt: Das Timing ist so bestimmt, dass die Signale um 90° zueinander phasenverschoben sind. Dies wird als Quadraturkodierung bezeichnet.

 
 

Um die Drehrichtung zu bestimmen, müssen wir auf bestimmte Signalkombinationen achten. Sehen wir uns an, welche Signalsequenzen erzeugt werden können. In der folgenden Abbildung ist eine komplette Signalfolge gegen den Uhrzeiger zu sehen. Der Ausgangspunkt (Phase 1) für A und B ist 0. Dreht sich der Encoder (Phase 2), wechselt der B-Anschluss zuerst seinen Wert zu B = 1. Bei weiterem Drehen (Phase 3) wechselt nun auch der A-Anschluss zu A = 1. In der letzten Phase 4 ändert sich B = 0 schließlich zu B = 1. Damit ist die Signalsequenz abgeschlossen. Diese Vierer-Sequenz wird als ein Zyklus bezeichnet und stellt die Kodierung gegen den Uhrzeigersinn dar. Die Programmierung fragt fortlaufend diese Kombinationsfolge ab und reagiert, sobald sie auftaucht. Im Signal-Diagramm erkennst du die zeitliche Verschiebung der beiden Signale A und B.

 

Die Signalsequenz für die Drehrichtung im Uhrzeigersinn verhält sich bei Phase 2 und 4 genau entgegengesetzt - Phase 1 und 3 sind identisch. Das heißt für uns, dass wir in der Programmierung vier aufeinander folgende Phasen beobachten müssen, um eine Aussage machen zu können. Hierfür benützen wir if-Abfragen. Wichtig dabei ist, dass wir das aktuelle Signal mit dem letzten Signal vergleichen.

Ein Beispiel: Falls in der ersten Phase A=0 B=0, in der zweiten A=0 B=1, in der dritten Phase A=1 B=1 und in der vierten Phase A=1 B=0 lesen, dann dreht sich der Encoder laut unserer Codierungsliste im Uhrzeigersinn.

 

Noch ein kleiner Hinweis: Beim Drehen des Encoders wirst du erkennen, dass sie in kleinen Schritten einrastet. Eine komplette Umdrehung beinhaltet 20 dieser Schritte. Der oben beschriebene Zyklus voll zieht sich schon bei einem Schritt. Bei jedem Einrasten (Klicken) werden an den Anschlüssen A und B jeweils vier Signale erzeugt.



Pinbelegung des Drehgebers

Die Pinbelegung des Drehgebers ist wie folgt: GND ist der Masseanschluss. VCC ist die positive Versorgungsspannung, normalerweise 3,3 oder 5 Volt. SW ist der aktive Low-Tastschalter-Ausgang. Wenn der Knopf gedrückt wird, geht die Spannung auf LOW. DT (Anschluss B) ist derselbe wie der CLK-Ausgang, jedoch um 90° phasenverschoben gegenüber dem CLK.

 
 

Der Aufbau

Verbinde dein Arduino mithilfe der Jumper Wire mit dem Modul:

 
 

Der minimal Sketch

Der folgende Sketch gibt aus, in welche Richtung der Encoder gedreht wird und ob der Taster betätigt wird. Kopiere den unteren Sketch, füge ihn in die Arduino IDE ein und lade ihn auf das Arduino-Board rauf.

/********************************************************
A03|01 Der Drehrichtungs- und Impuls-Sensor A
Der minimal Sketch
Mr Robot UXSD / www.mrrobotuxsd.com
*********************************************************/

//Drehgeber-Eingänge
#define CLK 2
#define DT 3
#define SW 4 //Taster-Pin

int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDirection ="";
unsigned long lastButtonPress = 0;


void setup() 
{
  //Encoderpins als Eingänge festlegen
  pinMode(CLK,INPUT);
  pinMode(DT,INPUT);
  pinMode(SW, INPUT_PULLUP);

  //Initialisiere die Serielle Kommunikation mit 9600 Bits pro Sekunde:
  Serial.begin(9600);

  //Lesen des Ausgangszustands von CLK
  lastStateCLK = digitalRead(CLK);
}


void loop() 
{  
  //Lesen des aktuellen Zustands von CLK
  currentStateCLK = digitalRead(CLK);

  //Wenn der letzte und der aktuelle Zustand von CLK unterschiedlich sind, 
  //dann erfolgt ein Impuls. && Reagiere nur auf 1 Zustandsänderung, 
  //um Doppelzählung zu vermeiden.
  if (currentStateCLK != lastStateCLK  && currentStateCLK == 1)
  {
    // Wenn der DT-Zustand anders ist als der CLK-Zustand, dann
    // dreht sich der Geber gegen den Uhrzeigersinn, also runterzählen.
    if (digitalRead(DT) != currentStateCLK) 
    {
      counter --;
      currentDirection ="Gegen Uhrzeigersinn";
    } 
    else 
    {
      //Geber dreht sich im Uhrzeigersinn, also hochzählen
      counter ++;
      currentDirection ="Im Uhrzeigersinn";
    }

    Serial.print("Drehrichtung ");
    Serial.print(currentDirection);
    Serial.print(" | Zähler: ");
    Serial.println(counter);
  }


  //Letzten CLK-Zustand merken
  lastStateCLK = currentStateCLK;

  //Den Zustand des Tasters lesen
  int buttonState = digitalRead(SW);

  //Wenn der Taster ein LOW-Signal sendet (=Taste wird gedrückt)
  if (buttonState == LOW) 
  {
    //Wenn seit dem letzten LOW-Impuls 50 ms vergangen sind, 
    //bedeutet dies, dass die Taste gedrückt, losgelassen 
    //und erneut gedrückt wurde
    if (millis() - lastButtonPress > 50) 
    {
      Serial.println("Taster gedrückt!");
    }

    //Letztes Tastendruck-Ereignis speichern
    lastButtonPress = millis();
  }

  //Zeitverzögerung => leichtes Debouncing des Tasters
  delay(1);
}

Schalte den seriellen Monitor ein, indem du in der Arduino IDE in der Menüleiste auf Werkzeuge > Serieller Monitor gehst. Du solltest nun die folgende Anzeige sehen. Falls die Richtungen verkehrt sind, vertausche die Anschlüsse CLK und DT.

 

Der Code im Detail

Der Sketch beginnt zunächst mit der Zuweisung der Anschlüsse des Encoders CLK, DT und SW. Als Nächstes werden einige Integer definiert: Die Zählvariable repräsentiert den Zählerstand, der jedes Mal geändert wird, wenn der Drehknopf um eine Rastung (Klick) gedreht wird. Die Variablen currentValue_CLK und lastValue_CLK speichern den Zustand des CLK-Pins fest und werden zur Bestimmung der Drehung verwendet. Eine Zeichenkette namens currentDirection wird verwendet, um die aktuelle Drehrichtung auf dem seriellen Monitor auszugeben. Die Variable lastButtonPress wird zur Entprellung (debouncing) eines Schalters verwendet, um Fehleingaben zu vermeiden.

//Drehgeber-Eingänge
#define CLK 2
#define DT 3
#define SW 4 //Taster-Pin

int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDirection ="";
unsigned long lastButtonPress = 0;
 

Im void setup definieren wir zunächst die Verbindungen zum Encoder als INPUTs. Beim Taster müssen wir mit der Angabe INPUT_PULLUP festlegen, dass wir den internen Pullup-Widerstand verwenden. Mit Serial.begin() richten wir den seriellen Monitor ein. Schließlich wird der aktuelle Wert des CLK-Pins zum ersten Mal eingelesen und in der Variable lastValue_CLK gespeichert.

void setup() 
{
  //Encoderpins als Eingänge festlegen
  pinMode(CLK,INPUT);
  pinMode(DT,INPUT);
  pinMode(SW, INPUT_PULLUP);

  //Initialisiere die Serielle Kommunikation mit 9600 Bits pro Sekunde:
  Serial.begin(9600);

  //Lesen des Ausgangszustands von CLK
  lastStateCLK = digitalRead(CLK);
}
 

Im void loop überprüfen wir den CLK-Wert (currentValueCLK) erneut und vergleichen ihn mit dem letzten Wert (lastValueCLK) aus void setup. Wenn sie unterschiedlich sind, bedeutet dies, dass der Drehgeber gedreht wurde und dadurch ein Impuls aufgetreten ist. Gleichzeitig wird überprüft, ob der Wert von currentValue_CLK = 1 ist, um auf nur EINE Zustandsänderung zu reagieren und eine Doppelzählung zu vermeiden.

//Lesen des aktuellen Zustands von CLK
currentStateCLK = digitalRead(CLK);

//Wenn der letzte und der aktuelle Zustand von CLK unterschiedlich sind, 
//dann erfolgt ein Impuls. && Reagiere nur auf 1 Zustandsänderung, 
//um Doppelzählung zu vermeiden.
if (currentStateCLK != lastStateCLK  && currentStateCLK == 1)
{
  ...
}
 

Innerhalb der if-Anweisung bestimmen wir die Drehrichtung. Dazu lesen wir den DT-Pin am Encoder-Modul und vergleichen ihn mit dem aktuellen Zustand des CLK-Pins. Wenn sie unterschiedlich sind, bedeutet das, dass der Drehgeber gegen den Uhrzeigersinn gedreht wird. Wir dekrementieren dann den Zähler und setzen currentDirection auf "GEGEN Uhrzeigersinn".

Falls jedoch die beiden Werte gleich sind, bedeutet dies, dass der Drehgeber im Uhrzeigersinn gedreht wird. Wir erhöhen dann den Zähler und setzen currentDirection auf "IM Uhrzeigersinn".

 if (currentStateCLK != lastStateCLK  && currentStateCLK == 1)
  {
    // Wenn der DT-Zustand anders ist als der CLK-Zustand, dann
    // dreht sich der Geber gegen den Uhrzeigersinn, also runterzählen.
    if (digitalRead(DT) != currentStateCLK) 
    {
      counter --;
      currentDirection ="Gegen Uhrzeigersinn";
    } 
    else 
    {
      //Geber dreht sich im Uhrzeigersinn, also hochzählen
      counter ++;
      currentDirection ="Im Uhrzeigersinn";
    }
 

Anschließend geben wir unsere Ergebnisse auf dem seriellen Monitor aus:

Serial.print("Drehrichtung ");
Serial.print(currentDirection);
Serial.print(" | Zähler: ");
Serial.println(counter);
 

Außerhalb der if-Anweisung aktualisieren wir lastValue_CLK mit dem aktuellen Zustand von CLK. Der neue Wert wird zum alten Wert:

//Letzten CLK-Zustand merken
lastStateCLK = currentStateCLK;
 

Zu guter Letzt sehen wir uns die Logik zum Lesen und Entprellen des Tasters an. Zuerst lesen wir den aktuellen Zustand des Tasters, wenn er LOW ist, warten wir 50ms, um den Taster zu entprellen. Wenn der Taster länger als 50ms im Zustand LOW bleibt, wird die Meldung "Taster gedrückt" auf dem seriellen Monitor ausgegeben. Somit ignorieren wir eventuelle Fehlsignale, die eine kürzere Dauer einnehmen. Üblicherweise entstehen bei mechanischen Tastern mehrere Signale, da die Kontaktflächen nicht sauber anliegen und viele kleine Signale entstehen.

  //Den Zustand des Tasters lesen
  int buttonState = digitalRead(SW);

  //Wenn der Taster ein LOW-Signal sendet (=Taste wird gedrückt)
  if (buttonState == LOW) 
  {
    //Wenn seit dem letzten LOW-Impuls 50 ms vergangen sind, 
    //bedeutet dies, dass die Taste gedrückt, losgelassen 
    //und erneut gedrückt wurde
    if (millis() - lastButtonPress > 50) 
    {
      Serial.println("Taster gedrückt!");
    }

    //Letztes Tastendruck-Ereignis speichern
    lastButtonPress = millis();
  }
 

Aus Stabilitätsgründen wird in der letzten Zeile ein sehr kurzer delay eingefügt. Dies ist eine gängige Methode in Sketchen, die im Hauptprogramm-Ablauf keine delays enthalten.

//Zeitverzögerung => leichtes Debouncing des Tasters
delay(1);
 

Die Genauigkeit durch Interrupts steigern

Der obere minimale Sketch mag für sehr einfache Anwendungen gut funktionieren, aber wenn Präzision gefragt ist, müssen wir einen anderen Ansatz verfolgen. Außer der Präzision gibt es ein anderes Problem: Der Sketch kann keine anderen Aufgaben sinnvoll ausführen. Das Lesen der DT und CLK Signale muss kontinuierlich überwacht werden. Werden mehrere Aufgaben gleichzeitig ausgeführt, kann es passieren, dass Signale verloren gehen. Stell dir vor, du lässt eine LED und zwei Motoren ansteuern. Wenn der Drehgeber in dem Moment gedreht wird, in dem die LED oder die Motoren angesteuert werden, gehen die Signale verloren. Es entsteht eine Latenzzeit zwischen dem Eintreten des Ereignisses und dem Zeitpunkt der Überprüfung. Die Betätigung des Drehgebers wird völlig übersehen, falls die Betätigungsdauer zu kurz ist.

Du kannst diesen Effekt mit dem minimal Sketch ausprobieren, in dem du sehr schnell den Drehgeber drehst. Auf dem seriellen Monitor wirst du sehen, dass der Zähler einige Werte überspringen wird. Eine weit verbreitete Lösung ist die Verwendung eines Interrupts. Mit Interrupts müssen wir das spezifische Ereignis nicht ständig abfragen. Dadurch kann das Arduino andere Aufgaben erledigen, ohne das Drehgeber-Ereignis zu verpassen. Interrupts haben in der Laufzeit des Programms die höchste Ausführungspriorität. Wo auch immer der Programmablauf im Moment steckt, das Interrupt stoppt jeden Prozess und führt die ihm zugeführten Aufgaben aus und springt in die Ausgangssituation wieder zurück. So können wir sicherstellen, dass der Drehgeber, die entsprechende Aufmerksam bekommt.

 

Der Aufbau

Da das Arduino Uno nur zwei Hardware-Interrupts besitzt, können wir nur das DT und CLK Signal hierfür nutzen. Für den Taster SW fehlt uns ein zusätzliches Interrupt. Mit einem Arduino Mega kannst du auch den SW benutzen, da dieser bis zu acht Interrupts besitzt. Die beiden Interrupts des Arduino Uno befinden sich auf den Pins 2 und 3.

 


Der folgende Sketch demonstriert die Verwendung der Interrupts beim Lesen des Drehgebers. Er ähnelt dem Aufbau im minimal Sketch. Du wirst bemerken, dass void loop leer ist und die Berechnungen zur Bestimmung der Drehrichtung in eine externe Funktion ausgelagert ist.

/********************************************************
A03|01 Der Drehrichtungs- und Impuls-Sensor B
Die Genauigkeit durch Interrupts steigern
Mr Robot UXSD / www.mrrobotuxsd.com
*********************************************************/

//Drehgeber-Eingänge
#define CLK 2
#define DT 3

int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDirection ="";


void setup() 
{  
  //Encoderpins als Eingänge festlegen
  pinMode(CLK,INPUT);
  pinMode(DT,INPUT);

  //Initialisiere die Serielle Kommunikation mit 9600 Bits pro Sekunde:
  Serial.begin(9600);

  //Lesen des Ausgangszustands von CLK
  lastStateCLK = digitalRead(CLK);
  
  //Aufruf von updateEncoder(), wenn ein High/Low-Änderung auf 
  //Interrupt 0 (Pin 2) oder Interrupt 1 (Pin 3) gelesen wird
  attachInterrupt(0, updateEncoder, CHANGE);
  attachInterrupt(1, updateEncoder, CHANGE);
}


void loop() 
{
  //...
}


void updateEncoder()
{
  //Lesen des aktuellen Zustands von CLK
  currentStateCLK = digitalRead(CLK);

  //Wenn der letzte und der aktuelle Zustand von CLK unterschiedlich sind, 
  //dann erfolgt ein Impuls. && Reagiere nur auf 1 Zustandsänderung, 
  //um Doppelzählung zu vermeiden.
  if (currentStateCLK != lastStateCLK  && currentStateCLK == 1)
  {
    // Wenn der DT-Zustand anders ist als der CLK-Zustand, dann
    // dreht sich der Geber gegen den Uhrzeigersinn, also runterzählen.
    if (digitalRead(DT) != currentStateCLK) 
    {
      counter --;
      currentDirection ="Gegen Uhrzeigersinn";
    } 
    else 
    {
      //Geber dreht sich im Uhrzeigersinn, also hochzählen
      counter ++;
      currentDirection ="Im Uhrzeigersinn";
    }

    Serial.print("Drehrichtung ");
    Serial.print(currentDirection);
    Serial.print(" | Zähler: ");
    Serial.println(counter);
  }

  //Letzten CLK-Zustand merken
  lastStateCLK = currentStateCLK;
}
 

Folgenden zwei Zeilen rufen die Interrupt-Funktionen auf.

//Aufruf von updateEncoder(), wenn ein High/Low-Änderung auf 
//Interrupt 0 (Pin 2) oder Interrupt 1 (Pin 3) gelesen wird
attachInterrupt(0, updateEncoder, CHANGE);
attachInterrupt(1, updateEncoder, CHANGE);

Die Funktion attachInterrupt() braucht in unserem Fall mehrere Parameter. Der erste Parameter 0 ruft den internen Interrupt 1 auf. Der Parameter 1 ruft entsprechend den Interrupt 2 auf. Je nach Arduino-Board fällt die Pin-Positionen der Interrupts unterschiedlich aus. Auf dem Arduino Uno gilt Folgendes:

Interrupt 0 = Pin 2
Interrupt 1 = Pin 3

Indem wir die Werte 0 oder 1 eintragen, weiß das Arduino automatisch, dass es sich um die Pins 2 und 3 handelt. Der zweite Parameter enthält den Funktionsnamen, der aufgerufen wird, sobald der Auslöser auftritt. Der Auslöser wird im dritten Parameter beschrieben. Mit dem Wort CHANGE wird das Programm darauf angewiesen, den Interrupt auszulösen, sobald ein Signalwechsel auftritt. Mit anderen Worten, es sucht nach einer Signaländerung, die von HIGH zu LOW oder LOW zu HIGH wechselt. Dies passiert, wenn der Drehgeber gedreht wird.

 

Verwante Themen

A03|02 Der Potentiometer

 

Falls du die hier beschriebenen Elektronik-Module nicht hast, kannst du sie in meiner Einkaufsliste finden. Warum ich selber hauptsächlich mit Modulen der Marke Keyestudio arbeite, erläutere ich unter diesem Blog-Artikel.

In dieser Übersicht äußere ich Empfehlungen aller Art.

Hier kommst du wieder zur Übersicht aller Module.