Forum: Projekte & Code AVR - Drehgeber auslesen - optimiert für präzise Benutzereingaben


von m4444x (Gast)


Lesenswert?

Drehgeber Codeschnipsel gibts ja schon jede Menge hier im Forum. 
Allerdings sind diese Methoden meiner Meinung nach alle eher für Motor 
Positionierungs Aufgaben geeignet und weniger für eine präzise 
Menüsteuerung.

Hier deshalb mal eine etwas andere Variante speziell optimiert für 
rastende Drehgeber mit einem Zwischenschritt. Eventuell gibt es auch 
Drehgeber mit mehr als einem Zwischenschritt zwischen den Rastpunkten. 
Mir sind bisher aber nur solche mit einem Zwischenschritt untergekommen. 
Trotzdem sollte man - bevor man jetzt einfach den Code kopiert und sich 
dann wundert warum es nicht funzt - den Drehgeber einmal ganz genau 
untersuchen.

Also bitte als erstes einfach einmal die Pins einlesen und über LCD oder 
serielle ausgeben.
1
  // drehgeber an PD2 und PD3
2
  DDRD &= ~ ( 1 << PD2 ) | ( 1 << PD3 );
3
  PORTD |= ( 1 << PD2 ) | ( 1 << PD3 );
4
  
5
  uint8_t z = 0;
6
  
7
  while ( 1 )
8
  {
9
    uint8_t y = ( PIND >> 2 ) & 3;
10
    
11
    if ( y != z & 3 )
12
    {
13
      z = ( z << 2 ) | y;
14
      
15
      lcd_gotoxy( 0, 0 );
16
      
17
      for ( int i = 0; i < 8; i++ )
18
      {
19
        if ( z & ( 1 << ( 7 - i ) ) )
20
        {
21
          lcd_write( '1' );
22
        }
23
        else
24
        {
25
          lcd_write( '0' );
26
        }
27
      }
28
    }
29
  }

Dreht man nun den Geber sollte ein erkennbares Muster angezeigt werden: 
0010110100... oder 0001111000... je nach dem in welche Richtung gedreht 
wird

Bei meinen Drehgebern hat sich nun folgendes ergeben:
- Rastpunkte liegen auf 00 und 11
- dazwischen ist immer ein Zwischenschritt 01 oder 10

Sollte der Drehgeber auf 01 und 10 rasten ist das nicht weiter tragisch 
und kann über XOR 1 korrigiert werden.

Falls der Drehgeber nur drei verschieden Zustände ausgibt anstatt vier, 
sind die Anschlüsse höchstwahrscheinlich vertauscht. TIP: Hat man den 
Geber aus einer alten Stereoanlage ausgelötet oder aus einem anderen 
Grund kein Datenblatt zur Hand, kann man den Geber and drei IO Pins 
anschließen. Dann der Reihe nach einen Pin auf Masse ziehen und die 
beiden anderen Pins testen. Bei einer Variante sollte sich das korrekte 
Graycode Muster mit vier Zuständen ergeben.

Schaut man sich das ausgegebene Bitmuster genau an sollte nun eigentlich 
auch klar sein wie man das Muster in + und - Impulse umwandeln kann:

aus 001011 und 110100 wird -
aus 000111 und 111000 wird +
1
int8_t rotary = 0;
2
3
void rotary_check( void )
4
{
5
  static uint8_t z = 0;    // shift register, speichert die letzten drei zustände
6
  
7
  uint8_t y = ( PIND >> 2 ) & 3; // beide drehgeber pins einlesen ( hier PD2 und PD3 )
8
  
9
  if ( ( z & 3 ) != y )    // testen ob sich etwas geändert hat
10
  {
11
    z = ( ( z << 2 ) | y ) & 0x3f; // die zwei neuen bits reinshiften und ergebnis auf 6 bits begrenzen
12
    
13
    // muster vergleichen und ausgabe wert gegebenfalls hoch oder runter zählen
14
    if ( z == 0b001011 || z == 0b110100 ) 
15
      rotary--;
16
    else if ( z == 0b000111 || z == 0b111000 )
17
      rotary++;
18
  }
19
}


Das Praktische ist dass bei dieser Methode einfaches Kontaktprellen 
automatisch ignoriert wird. (Wirklich!) Tiefpass filter sind trotzdem zu 
empfehlen damit man auch bei Gewitter (o.Ä.) möglichst keine Störungen 
bekommt.
Außerdem gibt's keine Halbschritt- und Rundungsfehler (wie bei der 
Tabelle-und-dann-einfach-durch-zwei-teilen-Methode) da hier automatisch 
immer mit den Rastpunkten syncronisiert wird.

Wie und wann man die Abfrage Routine nun aufruft bleibt einem selbst 
überlassen. Bei den neueren Atmels bietet sich natürlich der Pin Change 
Interrupt an. Ansonsten kann man aber auch entweder im Hauptprogramm 
oder über einen Timer Interrupt pollen. Ich habe hier gerade einen alten 
M16 deshalb habe ich den geber an PD2/3 (INT0/1).
1
// alter atmega16 ohne pinchange interrupt, deshalb drehgeber an INT0 und INT1
2
ISR( INT0_vect )
3
{
4
  rotary_check();
5
}
6
7
ISR( INT1_vect )
8
{
9
  rotary_check();
10
}
11
12
int main(void)
13
{
14
  DDRD = ( 1 << PD7 ); // led
15
  PORTD = ( 1 << PD2 ) | ( 1 << PD3 ) | ( 1 << PD7 ); // pullups für drehgeber und led ein
16
  
17
  MCUCR |= ( 1 << ISC00 ) | ( 1 << ISC10 ); // INT0 + INT1 any edge
18
  GICR |= ( 1 << INT0 ) | ( 1 << INT1 );
19
  
20
  asm volatile ( "sei" );
21
  
22
  .
23
  .
24
  .
25
}

Wenn man einen Interrupt Handler verwendet zur Abfrage (Timer oder 
PinChange egal) sollte man übrigens nicht direkt auf den Ausgabe Wert 
zugreifen. Sondern - damit auch wirklich keine Schritte verloren gehen - 
besser folgenden Schnipsel verwenden:
1
// zum sicheren auslesen des drehgeber wertes im hauptprogram
2
int8_t rotary_get_and_clr( void )
3
{
4
  int8_t r;
5
  asm volatile (
6
    "in __tmp_reg__, __SREG__  \n\t"  // cpu status register sichern
7
    "cli                       \n\t"  // interrupts aus
8
    "lds %0, rotary            \n\t"  // wert laden
9
    "sts rotary, __zero_reg__  \n\t"  // und löschen
10
    "out __SREG__, __tmp_reg__ \n\t"  // interrupts wieder zulassen
11
    : "=r" (r) : );
12
  return r;
13
}
14
15
int main( void )
16
{
17
  .
18
  .
19
  .
20
  
21
  int8_t r = rotary_get_and_clr();
22
  
23
  if ( r > 0 )
24
    ...
25
  else if ( r < 0 )
26
    ...
27
    
28
  .
29
  .
30
  .
31
}

von F.G. (Gast)


Lesenswert?

m4444x schrieb:
> Das Praktische ist dass bei dieser Methode einfaches Kontaktprellen
> automatisch ignoriert wird.

Zum Glück macht das der Gray-Code schon von sich aus, weil der 
Hammingabstand benachbarter Code-Worte 1 ist. Dafür wurde der Code 
erfunden ;-)

von m4444x (Gast)


Lesenswert?

F.G. schrieb:
> m4444x schrieb:
>> Das Praktische ist dass bei dieser Methode einfaches Kontaktprellen
>> automatisch ignoriert wird.
>
> Zum Glück macht das der Gray-Code schon von sich aus, weil der
> Hammingabstand benachbarter Code-Worte 1 ist. Dafür wurde der Code
> erfunden ;-)

Ja schon, aber.. :)

Angenommen ich habe hier einen richtig heftig prellenden Drehgeber. Das 
Muster an den Pins des µc könnte dann ungefähr so aussehen:

00 01 00 01 00 01 11 01 11 01 11 01 11

Wertet man nun die jeweils vier letzten Bits aus ergibt sich daraus:

   ++ -- ++ -- ++ ++ -- ++ -- ++ -- ++
00 01 00 01 00 01 02 01 02 01 02 01 02

Jeder Preller ergibt also einen Puls. Natürlich kann man das 
kompensieren mit einer Division durch 2. Problematisch wird es dann 
allerdings trotzdem wenn irgendwo ein Puls verloren gehen sollte. Dann 
stimmt die Synchronisierung mit den Rastpunkten des Drehgebers nicht 
mehr überein. Das äußert sich dann z.B so dass zwar ein Wert eingestellt 
werden kann, nimmt man allerdings die Hand vom Knopf, springt der Wert 
einen hoch oder runter. Oder aber man drückt den Knopf runter um eine 
Eingabe zu bestätigen und schwups quasi gleichzeitig mit der Bestätigung 
verspringt der Wert um +-1.

Also aufpassen bei der Division dass kein Bit unter den Tisch fällt. 
Ebenso beim Abfragen des Wertes und anschließendem Nullsetzen. Funkt der 
Interrupt dazwischen ist die Synchronisierung weg.

Die oben vorgestellte Routine vermeidet alle diese Probleme automatisch.

Gleiche Sequenz an den Eingängen:

00 01 00 01>00 01 11<01 11 01 11 01 11

ergibt:

                  ++
00 00 00 00 00 00 01 01 01 01 01 01 01

Wie man sieht kann ein Bit vor- und zurück prellen soviel es will. Ein 
Impuls wird erst generiert wenn auch das zweite Bit kippt. Eine Division 
mit Auswertung des Carry Bits ist nicht nötig. Außerdem kann die 
Synchronisierung mit den Rastpunkten nicht verloren gehen.

Einen Nachteil gibt es natürlich auch: Funktioniert nur mit rastenden 
Drehimpulsgebern mit zwei Bit Schritten pro Rastung bzw mit einem 
"Zwischenschritt".
Sind es mehr Schritte pro Rastung müsste man wieder durch zwei 
dividieren. Oder man erweitert die Muster Erkennung auf 10 bit (zB 
0010110100) was aber keine Vorteile bringen würde. Im Gegenteil bei mehr 
als 6 Bits ist das ganze nicht mehr automatisch immun gegen 
Kontaktpreller.

von F.G. (Gast)


Lesenswert?

m4444x schrieb:
> Jeder Preller ergibt also einen Puls.

Genau genommen führt jeder Preller (z.B. 00 01 00) zu zwei 
Zustandsänderungen, die sich gegenseitig aufheben.

von m4444x (Gast)


Lesenswert?

F.G. schrieb:
> m4444x schrieb:
>> Jeder Preller ergibt also einen Puls.
>
> Genau genommen führt jeder Preller (z.B. 00 01 00) zu zwei
> Zustandsänderungen, die sich gegenseitig aufheben.

Bei großen Wertebereichen spielt das sicher keine Rolle, ob es zwei 
Pulse gibt die sich zu 0 addieren oder ob es gar keinen Puls gibt. 
(Motorsteuerung, meinetwegen auch Volume Regler einer Stereoanlage)

Nervig wird es aber unter Umständen, wenn man einen von drei Menu 
Punkten selektieren will...

von m4444x (Gast)


Lesenswert?

Hier nochmal der Code im Context. Eine kleine Beispiel-App, bei der ein 
geheimer Code zum Öffnen eines Safe abgefragt wird. Der Code wird dabei 
ganz klassisch über links und rechts Drehung des Drehgebers eingestellt.

von m4444x (Gast)


Angehängte Dateien:

Lesenswert?

ok irgendwas ist da wohl schiefgelaufen...

anbei die (hoffentlich) richtige datei

von m4444x (Gast)


Lesenswert?

Habe jetzt spaßeshalber mal bei Reichelt noch ein paar Drehimpulsgeber 
bestellt. Ergebnis:


STEC11B02 :: ALPS STEC11B Drehimpulsg., 15/30, horiz., OT

rastet auf 00 und 11, also jeweils ein Zwischenschritt pro Rastung
bestens geeignet für die oben vorgestellte Methode (außerdem ca. 50 Cent 
günstiger)


STEC11B09 :: ALPS STEC11B Drehimpulsg., 20/20, horiz., MT

rastet nur auf 11, also jeweils drei Zwischenschritte pro Rastung

Wozu das gut sein soll, die drei Zwischenschritte, ist mir noch nicht so 
ganz klar. Ein einfaches Vergleichen mit 1101000111 (+1) bzw. 1110001011 
(-1), bringt hier nichts, da Kontaktpreller im mittleren Bereich (bei 
00) dazu führen, dass das Muster nicht erkannt wird. Wahrscheinlich sind 
diese Geber eher geeignet für die andere Methode, bei der die 
Zwischenschritte addiert werden und anschließend durch die Anzahl der 
Schritte pro Rastung geteilt wird.


Der Panasonic von Pollin ist übrigens auch einer mit einem 
Zwischenschritt. Rastpunkte liegen ebenfalls bei 00 und 11. Allerdings 
neigt dieser Geber scheinbar etwas zum Wackeln. Ich habe zumindest einen 
Rastpunkt gefunden, bei dem ich durch leichtes Antippen zwischen 00 und 
01 hin- und herschalten konnte. Sollte aber kein Problem darstellen, 
wenn - wie bei der hier gezeigten Methode - automatisch nur auf 00 und 
11 synchronisiert wird.

von Heiko J. (heiko_j12)


Angehängte Dateien:

Lesenswert?

Moin,
ich bin noch ein Neuling, was die Programmierung mit dem Atmel angeht. 
Daher bitte ich um Nachsicht. Ich habe ein ähnliches Problem mit dem 
Drehgeber 
http://www.reichelt.de/STEC12E08/3/index.html?&ACTION=3&LA=446&ARTICLE=73923&artnr=STEC12E08&SEARCH=STEC12E08 
gehabt. Ich konnte mit den hier vorgestellten Codeschnipsel einfach 
nicht präzise genug einstellen. Vor dem 'einrasten' wurde bereits 
gestellt.
Daher habe ich den mal einfach 'ausgelesen', indem ich entsprechende 
Routinen programmiert habe und bin auf diese beiden Bitmuster gestoßen:
#define RIGHT    0b11010010
#define LEFT     0b11100001

Ich hänge einfach mal meinen C-Code ran und ein Bild von der Hardware. 
Es klappt soweit ganz gut. Nur bin ich mir ziemlich sicher, dass gerade 
am Code noch ordentlich was zu optimieren gibt. Neben einigen magic 
numbers können da bestimmt noch ganz böse Anfängerfehler wegoptimiert 
werden, weil ich durch die ganzen Register noch nicht richtig 
durchblicke. Eure (hoffentlich nicht vernichtende) Meinung würde mich 
sehr interessieren.
Danach würde ich das Ganze dann mal mit Assembler angehen....

LG, Heiko

von Heiko J. (heiko_j12)


Angehängte Dateien:

Lesenswert?

Ich habe den Quellcode nochmal etwas aufgeräumt und versucht, die 
Interrupt Routinen kleiner zu machen.

von Heiko J. (heiko_j12)


Angehängte Dateien:

Lesenswert?

Als ich den Quellcode mit Atmel Studio kompiliert habe, wurden 
Linksdreher nicht mehr erkannt. Problem war, dass ich für 'enc_delta' 
negative Werte verwendet hatte, die nicht in einer Switch-Anweisung 
erkannt werden können.
Daher habe ich den Code erneut angepasst.
Nun muss ich nur noch die richtigen Kondensatoren für den Uhrenquarz 
finden. Läuft ziemlich ungenau. Auf dem Breadboard mit einem anderen 
ATmega88pa und einen anderen Uhrenquarz des gleichen Fabrikats und mit 
keiner 4x7 Segment lief es sehr genau. Vielleicht nehme ich die Teile 
vom Breadboard und löte sie auf die Lochplatine.

von Rainer V. (Gast)


Lesenswert?

Hi,

gibt es vielleicht assembler-code? Bin Neueinsteiger und habe diverse 
Probleme...und kann schon ger keinen High-Level-Code....
Gruß Rainer

von Rufus Τ. F. (rufus) Benutzerseite


Lesenswert?

Heiko J. schrieb:
> Problem war, dass ich für 'enc_delta' negative Werte verwendet hatte,
> die nicht in einer Switch-Anweisung erkannt werden können.

Auch wenn's schon 'ne Weile her ist:

Natürlich können in einer Switch-Anweisung auch negative Werte verwendet 
werden -- da geht alles, was int ist.

von Falk B. (falk)


Lesenswert?

@Rufus Τ. Firefly (rufus) (Moderator) Benutzerseite

>Natürlich können in einer Switch-Anweisung auch negative Werte verwendet
>werden -- da geht alles, was int ist.

INT-eressant!

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.