Quadraturencoder auslesen

hallo,
ich möchte Quadraturencoder mit dem Uno auslesen,
ich habe 4 Litzen: 1x a, 1x b, VCC ist 4.3 V, GND,
Flanken/Schwellen sind nicht bekannt.
Ich möchte möglichst wenig Kabel/Pins dafür verbrauchen.

Hat wer eine Idee für die Herangehensweise? (ein Oscilloskop habe ich leider nicht!)

440px-AB_Signal_and_Ref.svg.png

Man kann Pollen wie in dem ersten Link, aber am zuverlässigsten ist die Auswertung per Timer-Interupt wie im zweiten. Vor allem wenn man sonst noch viel macht.

dankeschön! :slight_smile:
zu den Interrupts:
wieviele Quadratur Encoder (je a+b) kann ich mit 1 Uno auslesen?
kann ich sie schnell genug mit dem Uno auslesen (Motoren drehen sich mit
(edit:) max. 1 Impuls/ms = 1 KHz ( a oder b )
welche Interrupt-pins kann/soll ich am besten verwenden?

  1. a: b:
  2. a: b:
  3. a: b:
  4. ... ?

Externe Interrupts für sowas zu verwenden ist Schwachsinn. Hier geht es um Timer-Interrupts. Man lässt einen Timer laufen damit die Pins regelmäßig abgefragt werden.

10kHz ist aber ziemlich schnell. In dem Code oben wird alle 1ms ein Interrupt ausgelöst. Das ist dann schon problematisch wenn die Impulse so schnell kommen. Aber so viel Erfahrung habe ich damit auch nicht.

hatte mich verschrieben, sorry, musste ntl heißen: 1 KHz (habs berichtigt ).

Timer interrupts habe ich doch aber nur 1x, nämlich pin2 (+pin3 für den b-Encoderteil), oder täusche ich mich?

wie kann ich dann mehrere Encoder parallel abfragen, die ja auch alle je 2 Timer-interrupts brauchen?

gibt es eine Möglichkeit, über shields zu insgesamt 8-12 schnellen Interrupts (für 4-6 Quadraturencoder) zu kommen, die alle unabhängig mit ca. 1 kHz die Encoderwerte auslesen können?

HaWe:
Timer interrupts habe ich doch aber nur 1x, nämlich pin2 (+pin3 für den b-Encoderteil), oder täusche ich mich?
wie kann ich dann mehrere Encoder parallel abfragen, die ja auch alle je 2 Timer-interrupts brauchen?

Hallo,

ich vermute, Du hast da irgendwas durcheinander gebracht. Interrupts haben per se erst mal nichts mit den Pins des Controllers zu tun. Der Timer-Interrupt basiert auf einem Zähler im Controller, der bis 255 zählt und jedes mal wenn er überläuft, also wieder auf Null springt, wird intern im Controller ein bestimmter Teil des Programmes aufgerufen. Das ist der Timer-Interrupt. - Um meine Aussage von oben zu relativieren, da können dann doch die Pins ins Spiel kommen, wenn z.B. ein Pin durch den Interrupt LOW oder HIGH gesetzt wird, oder aber die Impulse die gezählt werden von aussen an den Controller gelangen, die liegen dann an einem Pin. (Simplifizierte Darstellung zwecks leichterem Verständnis)

Zum Einlesen der Encoder benötigst Du einen Timer-Interrupt, der aber nicht mit einem Pin in Verbindung stehen muss, auch nicht mit den Encodern.

Die Encoder schließt Du ganz normal an VCC und GND an und die beiden Datenleitungen (A und B) an zwei Pins des Controllers. Wenn jetzt der Timer überläuft und der Interrupt ausgelöst wird, müssen in dem Programmteil der durch den Interrupt aktiviert wird (die sog. Interrupt-Service-Routine, ISR), die beiden Pins an denen der Drehgeber hängt abgefragt werden und die Zustände der Geber-Kontakte ausgewertet werden.

Wenn Du mehr Encoder als einen benötigst, sagen wir jetzt mal zwei, dann schließt Du beide je an zwei Datenpins an. In der ISR fragst Du dann nacheinander die Pins der Encoder ab und wertest sie aus. Es wird also nur ein Interrupt für mehrere Encoder benötigt, allerdings braucht jeder Encoder zwei Datenpins.

Schau bitte auch mal hier, da wird die Auswertung im Timer-Interrupt beschrieben: meinduino.de

Hoffe, das hilft Dir etwas weiter.

Gruß,
Ralf

hallo,
danke für die Antwort und den Link!

Ich probiers jetzt mal nach diesem Bild:


aus: meinduino.de

mit diesem Code:

/************************************************************
*
* Demo-Programm zur Auswertung eines händisch betriebenen
* Drehencoders (Quadraturencoder) mit dem Arduino im 
* Polling-Verfahren
*
* Kann von jederman frei verwendet werden, aber bitte den 
* Hinweis: "Entnommen aus http://www.meinDUINO.de" einfügen
*
************************************************************/

#define encoderA 2
#define encoderB 3

int8_t altAB = 0;

// 1/1 Auflösung
int8_t schrittTab[16] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; 

// 1/4 Auflösung
// int8_t schrittTab[16] = {0,0,0,0,0,0,0,-1,0,0,0,0,0,1,0,0}; 


int8_t readEncoder(int pinA, int pinB) {
  altAB <<= 2;
  altAB &= B00001100;
  altAB |= (digitalRead(pinA) << 1) | digitalRead(pinB);
  return(schrittTab[altAB]);
}

void setup() {
  pinMode(encoderA, INPUT);
  pinMode(encoderB, INPUT);

  Serial.begin(9600);
}


void loop() {
  int wert = 0;
  int wertAlt = 0;
  
  while(true) {
    wert += readEncoder(encoderA, encoderB);

    if(wert != wertAlt) {
      Serial.println(wert);
      wertAlt = wert;
    }
  }
}

Die beiden externen PullUp-Widerstände kannst Du ggf. weglassen, wenn du nur kurze Leitungen verwendest.

Stattdessen könntest Du die im Atmega vorhandenen internen PullUp-Widerstände aktivieren mit:

  pinMode(encoderA, INPUT_PULLUP);
  pinMode(encoderB, INPUT_PULLUP);

nochmal danke! :slight_smile:

soo....
gemacht.

Ergebnis:

  • wenn ich seeeehr laaaaangsaaaam drehe, bekomme ich 720 counts auf 360° Drehung - so soll es sein.
  • aber wenn ich normal schnell drehe (ca. 100-150°/100ms), dann erhalte ich nur ca. 220 counts, da werden also mehr 2/3 aller Werte nicht erfasst - das ist sehr schlecht.

Dann geht's wohl nicht, oder?

HaWe:
Dann geht's wohl nicht, oder?

Na mit der lahmen Serial-Baudrate wohl nicht.

Mit dem Code:

Serial.println(wert);

läuft Dir bei langsamer Baudrate und schnellem Drehen ja ständig der Serial-Ausgangspuffer voll.

Und wenn der Serial-Ausgangspuffer voll ist und Du neues hineinstopfen möchtest, dann BLOCKIERST Du das Programm genau so lange, wie es dauert, die Zeichen zu senden, die Du neu in den Ausgangspuffer schieben möchtest.

Probier's mal mit einer Baudrate von 115200.

hahaa - jawohl...! :smiley:

supi, klappt prima, danke! :smiley:

jurs:
läuft Dir bei langsamer Baudrate und schnellem Drehen ja ständig der Serial-Ausgangspuffer voll.

@HaWe: Natürlich kann man nicht erwarten, dass der Encoder durch Polling noch korrekt gelesen wird, wenn man die Schleife, die eigentlich den Encoder abfragen soll, mit anderen Dingen zu einem großen Teil auslastet. Es handelt sich ja hier um ein Demo-Programm, bei dem es darum geht zu zeigen, wie man den Encoder abfragen kann. Wenn der Sinn des endgültigen Programms sein soll, den Encoder abzufragen und gleichzeitig eine enorme Menge an Daten mit einer langsamen Geschwindigkeit an den PC zu übertragen, sollte man einen anderen Ansatz wählen.

Und nebenbei, Du solltest auch nicht Bilder von einer Website hier ins Forum oder sonst wohin kopieren, schon gar nicht, wenn Sie einen Copyright-Hinweis haben.

Gruß,
Ralf

ja, danke, werde ich beachten - und ersetze das verlinkte Bild auch sofort durch einen reinen Link!

In der Tat möchte ich 3 QEnc auslesen, auf dem Arduino verarbeiten, und nebenher die aktuellen Encoderwerte + einige weitere Daten in 20ms-Takt an einen i2c-Host übertragen (dann ntl nicht mehr in Echtzeit, da sind also dann quasi Lücken erlaubt).
Außerdem werde ich dann auch keinen seriellen Monitor mehr brauchen, das ist nur jetzt zum Debuggen.

hallo,
ich habe noch eine Nachfrage zu dem Code im Post Quadraturencoder auslesen - #8 by system - Deutsch - Arduino Forum :

Im Prozedur-Kopf werden arrays als Maske für die Auslesung definiert:

int8_t altAB = 0;
// 1/1 Auflösung => ergibt  720 counts pro 360° Drehung
int8_t schrittTab[16] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; 

// 1/4 Auflösung => ergibt  180 counts pro 360° Drehung
// int8_t schrittTab[16] = {0,0,0,0,0,0,0,-1,0,0,0,0,0,1,0,0};

Zum Einen verstehe ich nicht recht, was da passiert,
zum Anderen möchte ich allerdings auch 1/2 Auflösung einrichten, da dann bei meinen Encodern 1 Encodercount genau 1° Drehung entspräche.
Wie könnte man die Maske auf 1/2 Auflösung einstellen?

ich:
es geht nicht ohne Multitasking -
wenn du einen Lego Encodermotor besitzt, probier es aus -
mit Lesen von nur 1 QE: geht es,
kommt ein 2. dazu: keine Chance.

Der Grund:
zwischen den einzelnen Encoder-Lesefunktionen müssen zu viele andere Aktionen erledigt werden, und bis das nächste Mal der QE ausgelesen werden kann, wurden schon zu viele Signale übersehen.

#define MAXMOTORS 2

#define OUT_A  0
#define OUT_B  1

// set motor pins
#define pinmenc0A  2
#define pinmenc0B  3
#define pinmot0d1  4
#define pinmot0d2  5
#define pinmot0pwm 6

#define pinmenc1A  7
#define pinmenc1B  8
#define pinmot1d1  9
#define pinmot1d2  10
#define pinmot1pwm 11

long motenc[MAXMOTORS]={0,0},
    oldenc[MAXMOTORS]={0,0};
   
byte pinmotdir[MAXMOTORS][2]={ {pinmot0d1, pinmot0d2},
                              {pinmot1d1, pinmot1d2} };
                   
int  pinmotpwm[MAXMOTORS]={pinmot0pwm, pinmot1pwm};

//------------------------------------------------------------------------------------
// read encoder: "Entnommen aus http://www.meinDUINO.de"
int8_t altAB = 0;

// 1/1 Auflösung
//int8_t schrittTab[16] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};

// 1/4 Auflösung
int8_t schrittTab[16] = {0,0,0,0,0,0,0,-1,0,0,0,0,0,1,0,0};

//----------------------------------------------------------------------------------------
int8_t readEncoder(int pinA, int pinB) {
  altAB <<= 2;
  altAB &= B00001100;
  altAB |= (digitalRead(pinA) << 1) | digitalRead(pinB);
  return(schrittTab[altAB]);
}

//-----------------------------------------------------------------------------------------
void idle(unsigned int ms) {
  for (int t=0; t<(ms*5); ++t) {                      // to be adjusted !
    motenc[0] += readEncoder(pinmenc0A, pinmenc0B);   
    motenc[1] += readEncoder(pinmenc1A, pinmenc1B);
   
    if( motenc[0] != oldenc[0] )      oldenc[0] = motenc[0];
    if( motenc[1] != oldenc[1] )      oldenc[1] = motenc[1];
   
    delayMicroseconds(100);
  }
 
}

//-----------------------------------------------------------------------------------------

//-----------------------------------------------------------------------------------------

inline void motoroff(byte motnr) {
  digitalWrite( pinmotdir[motnr][0],LOW);
  digitalWrite( pinmotdir[motnr][1],LOW);
  analogWrite(pinmotpwm[motnr],0);
}

//-----------------------------------------------------------------------------------------
inline void motorcoast(byte motnr) {
  digitalWrite( pinmotdir[motnr][0],LOW);
  digitalWrite( pinmotdir[motnr][1],LOW);
  analogWrite(pinmotpwm[motnr],0);
}

//-----------------------------------------------------------------------------------------
inline void motoron(byte motnr, int power) {
  if(power>0) {
    digitalWrite( pinmotdir[motnr][0],HIGH);
    digitalWrite( pinmotdir[motnr][1],LOW);
  }
  else
  if(power<0) {
    digitalWrite( pinmotdir[motnr][0],LOW);
    digitalWrite( pinmotdir[motnr][1],HIGH);
  }
  else
  if(power==0) {
    digitalWrite( pinmotdir[motnr][0],LOW);
    digitalWrite( pinmotdir[motnr][1],LOW);
  }
 
  power = abs(power);
  if(power>254) power=254;
  analogWrite(pinmotpwm[motnr],power);
}

//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------

void setup(){
 
  pinMode(pinmot0d1,OUTPUT);
  pinMode(pinmot0d2,OUTPUT);
  pinMode(pinmenc0A,INPUT_PULLUP);
  pinMode(pinmenc0B,INPUT_PULLUP);
  pinMode(pinmot0pwm,OUTPUT);
 
  pinMode(pinmot1d1,OUTPUT);
  pinMode(pinmot1d2,OUTPUT);
  pinMode(pinmenc1A,INPUT_PULLUP);
  pinMode(pinmenc1A,INPUT_PULLUP);
  pinMode(pinmot0pwm,OUTPUT);
 
  Serial.begin(115200); 
}

//----------------------------------------------------------------------------------------

void loop(){
  // motor 0 + motor 1
  for (int i=0; i<255; i+=5){   
    motoron(0, i);
    motoron(1, i); 
 
    motenc[0] += readEncoder(pinmenc0A, pinmenc0B);   
    motenc[1] += readEncoder(pinmenc1A, pinmenc1B);
   
    if( motenc[0] != oldenc[0] )      oldenc[0] = motenc[0];
    if( motenc[1] != oldenc[1] )      oldenc[1] = motenc[1];
   
    Serial.print(motenc[0]);  Serial.print(" - "); Serial.println(motenc[1]);
    idle(100);
  }
 
 
  motoroff(0);
  motoroff(1);
  idle(10);
 
  motenc[0] += readEncoder(pinmenc0A, pinmenc0B);   
  motenc[1] += readEncoder(pinmenc1A, pinmenc1B);
   
  if( motenc[0] != oldenc[0] )      oldenc[0] = motenc[0];
  if( motenc[1] != oldenc[1] )      oldenc[1] = motenc[1];
  Serial.print(motenc[0]);  Serial.print(" - "); Serial.println(motenc[1]);
 
  idle(500);
 
 
for (int i=0; i>-255; i-=5){
    motoron(0, i);
    motoron(1, i);   
   
    motenc[0] += readEncoder(pinmenc0A, pinmenc0B);   
    //motenc[1] += readEncoder(pinmenc1A, pinmenc1B);
   
    if( motenc[0] != oldenc[0] )      oldenc[0] = motenc[0];
    if( motenc[1] != oldenc[1] )      oldenc[1] = motenc[1];
   
    Serial.print(motenc[0]);  Serial.print(" - "); Serial.println(motenc[1]);
   
    idle(100);
  }
 
  // ohne bremsen
 
  Serial.print(motenc[0]);  Serial.print(" - "); Serial.println(motenc[1]);
  idle(500);
 
}

Rabenauge:
Dafür gibts Interrupts. 8)

:?:
das klingt hoffnungsvoll -
dann zeig doch mal bitte, wie das geht, damit man keine Impulse an den digipins der Encoder (2/3 + 7/8) übersieht und daraus auch den Encoderstand korrekt berechnen kann!

Steht doch schon dort:

Der untere Code. Für einen Drehgeber, aber das lässt sich schnell erweitern indem man jedem Encoder eigene geber/geberAlt Variablen spendiert. Startet einfach einen Timer der alle 1ms einen Interrupt auslöst und die Pins abfragt.
Alternativ könnte man den CTC Modus verwenden. Damit spart sich das ständige manuelle setzen des Counter/Timer Registers, aber das macht nicht viel aus.

Warnung:
Du kannst keine Pins für PWM verwenden die auf Timer1 laufen. Das sind auf dem UNO 9 und 10. digitalWrite() geht auf denen natürlich noch

puuuh - das ist aber schwäääre Kost.
ich verwende ja als Pins
2+3
7+8
für 2x QE

wie stricke ich das denn jetzt in meinen aktuellen Code um und ein?
grübel :fearful:

//
// STEC12E07 - Überarbeitete Version
// ---------------------------------
//
// Auswertung des ALPS STEC12E07 Drehimpulsgebers
// mit Arduino Diecimila oder Duemilanove mit
// ATMega168 oder ATMega328
//
// www.Elektronik-Bastelkeller.de / 09.2013
// Der Code darf unter Hinweis auf die Quelle
// weitergegeben und/oder verwendet werden.
//
// -----------------------------------------------------

// Globale Variablen
const int geberPin1 = 2; // An Pins DP2 und DP3 ist der
const int geberPin2 = 3; // Encoder angeschlossen
int signalA, signalB;
int geber, geberAlt;
int diff;
volatile int schritt; 

//
// Initialisierungen
// -----------------
// 
// Hier hauptsächlich der Timer-Interrupt
//
void setup(){
  noInterrupts();

  TIMSK1 &= ~(1<<TOIE0); // Für ATMega8 TIMSK verwenden

  TCCR1A = 0; 
  TCCR1B = (1<<CS11) | (1<<CS10);

  TIMSK1 |= (1<<TOIE1); // Für ATMega8 TIMSK verwenden

  TCNT1H = 0xFF; // Werte für 1000 Interrupts/s
  TCNT1L = 0x06;

  // Diese beiden Zeilen verwenden, wenn keine externen Pullup-Widerstände
  // vorhanden sind
  pinMode(geberPin1, INPUT_PULLUP);
  pinMode(geberPin2, INPUT_PULLUP);

  // ansonsten, diese beiden Zeilen:
  // pinMode(geberPin1, INPUT);
  // pinMode(geberPin2, INPUT);

  interrupts();

  Serial.begin(9600);
}


//
// Interrupt Service Routine wird 1000 mal 
// pro Sekunde aufgerufen und aktualisiert
// die Variable schritt
//
ISR(TIMER1_OVF_vect) {
  TCNT1H = 0xFF;
  TCNT1L = 0x06;

  signalA = digitalRead(geberPin1);
  signalB = digitalRead(geberPin2);

  geber = 0;
  if(signalA == 0) geber = 3;
  if(signalB == 0) geber ^= 1;

  diff = geber - geberAlt;

  if(diff == -3 || diff == 1) schritt -= 1;
  if(diff == 3 || diff == -1) schritt += 1;

  if(diff & 1 == 1) {
    geberAlt = geber;
  }
}


// 
// Hauptprogramm
// -------------
//
// Gibt den Wert von pos auf der seriellen
// Schnittstelle aus, falls der Encoder
// gedreht wurde
//
void loop(){
  noInterrupts();
  if(schritt != 0) {
                             // Während der seriellen Übertragung ist der
    Serial.println(schritt); // Interrupt ausgeschaltet. In dieser Zeit
    schritt = 0;             // werden Impulse des Drehgebers übersehen. 
                             // Interrupts wieder an.
  }
  interrupts();
  delay(200); // Entfernen, wenn Ihr Programm tatsächlich etwas macht
}

HaWe:
wie stricke ich das denn jetzt in meinen aktuellen Code um und ein?

Hallo,

über dem von Dir angeführten Artikel steht doch deutlich in blau oben drüber, dass er veraltet ist und nicht mehr verwandt werden sollte. Nimm doch statt dessen die neue Version, der Link steht ja auch da.

In dem neuen Link - hier noch mal zur Kenntnisnahme: meinduino.de - steht doch genau beschrieben, wozu die Tabelle gut ist und wie sie zu verwenden ist.

Wenn Du Dir wirklich nicht die Mühe machen willst, das zu verstehen, dann mach es einfach so: In der Tabelle steht 4 mal +1 und 4 mal -1. Ersetze einfach zwei beliebige +1 durch 0 und zwei beliebige -1 durch 0, dann ist die Auflösung so, wie Du sie gerne hättest.

Außerdem kann ich nicht ganz verstehen, warum Du immer den kompletten Code hier ins Forum kopieren musst - reicht es denn nicht einen Link anzugeben? Sollte der Code noch einen Fehler haben, steht er falsch hier im Forum. Wenn Du einen Link angibst und der Fehler korrigiert wird, kann jeder die korrigierte Fassung sehen. Das ist doch wesentlich sinnvoller.

Übrigens steht in Teil II des Artikels in dem Absatz vor dem Sketch genau, was zu tun ist, um die Funktion in eigenen Programmen verwenden zu können.

Gruß,
Ralf