Forum: Mikrocontroller und Digitale Elektronik Dynamische "Beschleunigung" bei Encoder-Eingabe_2


von Marc H. (bauerpilot)


Lesenswert?

Hallo,
ich habe den Originalthread gelesen und komme nicht weiter.
Ich habe einen ATMega8 mit 8 Encodern und alles läuft bestens mit Peters 
Code. Jetzt möchte ich für einen 4-Schritt Encoder die 
Beschleunigungsfunktion integrieren.
Wer kann mir helfen welcher Codeteil wohin muß und was es hiermit auf 
sich hat:
"Dazu wird noch ein 10ms Timer-Interrupt benötigt für die 50ms Meßzeit."
Ich bin noch ziemlicher Anfänger und habe es bisher nur geschafft 
bestehende Programme anzupassen. Gerade mit Interrupts und ähnlichen 
Dingen habe ich "noch" meine Probleme. Bin für jede Hilfe dankbar.

Gruß Marc

von SteppRider (Gast)


Angehängte Dateien:

Lesenswert?

Den folgenden Code verwende ich in Verbindung mit zum Drehgeber 
umfunktionierten Schrittmotoren (aus 5-1/4 Zoll-Laufwerken, Teac-FD55). 
Zwei Komparatoren mit einer auf ca. 3 bis 6 mV symmetrisch zum den 
Nullpunkt eingestellten Hysterese am Eingang dienen als Impulsformer.

An den Eingängen des AVRs stehen somit Rechtecksignale zur Verfügung, 
wie bei den meisten anderen Drehgebern - damit sollte der Code daher 
grundsätzlich auch funktionieren.

Im angehängten Beispiel habe ich der Übersichtlichkeit zuliebe alles 
wegelassen, was nicht zur Rotary-Decodierung gehört - und hoffe, nichts 
wesentliches vergessen zu haben.

Für die Beschleunigungsfunktion ist kein zweiter Timer erforderlich.

Wird im Interrupt-Handler eine Bewegung des Drehgebers erkannt, wird 
einfach ein Beschleunigungs-Zähler (die Variable "EncAccel") erhöht 
("das Gaspedal weiter durchgetreten"), wird dagegen Stillstand erkannt, 
wird der Beschleunigungs-Zähler wieder Stück für Stück dekrementiert 
("das Gaspedal langsam zurückkommen gelassen").

"ENCODER_ACCEL_INC" und "ENCODER_ACCEL_DEC" bestimmen die Be- und 
Entschleunigungskurve, "ENCODER_ACCEL_TOP" begrenzt die maximale 
Geschwindigkeit im beschleunigten Zustand. Diese Werte müssen nicht nur 
dem eigenen Gefühl entsprechend angepasst werden, sondern auch bei 
anderer Interrupt-Frequenz und abweichender Impulszahl pro Drehung des 
Gebers.

Um Veränderungen des Drehgebers zu erfassen, muss das Hauptprogramm die 
Funktion "HandleRotaryEncoder()" zyklisch aufrufen - oft genug, bevor 
die in der Zwischenzeit im Timer-Interrupt-Handler in der Variablen 
"EncSteps" gesammelten Drehbewegungen übergelaufen sind. Ganz änhlich 
wie bei den meisten hier vorgestellten Drehgeber-Implementationen. Die 
Variable "RotaryCount" enthält den jeweils aktuellen Stand des 
Drehgebers.

Anmerkung: Bei Werten von "ENCODER_ACCEL_DEC" grösser als 1 müsste 
eigentlich vor dem mit dieser Konstanten erfolgenden Dekrementieren von 
"EncAccel" abgefragt werden, wieviel noch von EncAccel abgezogen werden 
darf, ohne das die Variable negativ wird. Oder "EncAccel" müsste als 
signed int angelegt werden. Beides vergrössert aber den Code ganz 
unnötig. Die Randbedingung, daß dies wie hier vorgestellt funktioniert 
(genau hier: "if (EncAccel & 0b1000000000000000) EncAccel = 0; // Zero 
if overflowed), ist, daß "ENCODER_ACCEL_DEC" kleiner als 32768 gwählt 
wird. Das sollte aber problemfrei sein, da sinnvolle Werte dafür um 
Größenordnungen kleiner sind.

von Marc H. (bauerpilot)


Lesenswert?

Besten Dank für die schnelle und sehr umfassende Antwort, besonders auch 
für die Erklärungen zum Code. Da brauche ich etwas um alles zu 
verstehen, besonders weil die Programmierstile sich für mich sehr stark 
unterscheiden.

Als erstes habe ich Probeleme den exakten Sinn der "Rotary Encoder State 
Table" zu verstehen. Irgendwie hängt das mit der switch-Anweisung 
zusammen und gibt die Grundlage für die Encoderauswertung, oder?

Das nächste Problem ist, welche Header Dateien müssen noch zugefügt 
werden?
Aktuell müssten es folgende sein:
#include <avr/pgmspace.h>
#include <stdio.h>
#include <avr/interrupt.h>

Das die Timervariablen für den ATMega8 angepasst werden müssen ist klar, 
das bekomme ich auch hin, hoffe ich.

Was ich definitiv nicht verstehe ist folgendes:
"Um Veränderungen des Drehgebers zu erfassen, muss das Hauptprogramm die
Funktion "HandleRotaryEncoder()" zyklisch aufrufen"
Diese Funktion kann ich im ganzen Listing nicht finden. Nach meinem 
Verständnis und im Vergleich mit der Routine von Peter D. müsste es 
eigentlich die Funktion "void AccumulateRotaryCount()" sein.

Ich bedanke mich auf jeden Fall für die sehr umfassende Info und werde 
mal probieren, ob ich einen lauffähigen Code zusammen bekomme.
Über Antwort zu den erwähnten Fragen würde ich mich sehr freuen. Man 
will ja immer etwas dazu lernen.

von SteppRider (Gast)


Lesenswert?

Marc Höner schrieb:
> Als erstes habe ich Probeleme den exakten Sinn der "Rotary Encoder State
> Table" zu verstehen. Irgendwie hängt das mit der switch-Anweisung
> zusammen und gibt die Grundlage für die Encoderauswertung, oder?

Richtig. Hier:  http://www.mikrocontroller.net/articles/Drehgeber  wird 
das viel ausführlicher erklärt, ich versuche es nur einmal in Kurzform.

Mit zwei Leitungen von Encoder werden je Interrupt-Aufruf zwei Bit 
eingelesen. Die aktuellen beiden Bit sowie die beim letzten 
Interrup-Aufruf gelesenen, werden in diesen Zeilen:

  LastEnc <<= 2;
  LastEnc |= CurrEnc;
  LastEnc &= 0b00001111;

so miteinander verknüpft, daß sie "schön" in Reihe stehen, und einen 
4-Bit-Wert bilden, der verwendet wird, eines der 16 Bytes aus der 
"EncStates"-Tabelle zu adressieren.

Anhand von jeweils zwei aufeinander folgend abgetasteten 
"2-Bit-Zuständen" des Drehgebers kann ermittelt werden, was inzwischen 
geschehen ist. Bspw.: Wurde der Encoder nach links oder nach rechts 
gedreht, oder - im einfachsten Fall - wurde er gar nicht bewegt? Ganz zu 
Anfang des oben verlinkten Wiki-Artikels wird der Zusammenhang zwischen 
Drehbewegungen und der Zustandsänderung dieser Bit ausführlich erklärt.

Hier nur noch so viel: Anstatt jetzt alle möglichen 16 Bit-Kombinationen 
mit "einem Haufen" if- oder switch/case-Anweisungen abfragen, steht in 
jedem Slot der Tabelle ein Byte, dessen Wert unmittelbar der Art der 
Bewegung entspricht. 0=Stillstand, 1/2=vorwärts/rückwärts, 3=ungültig.

Ungültig?

Bestimmte Bit-Folgen treten im Idealfall nicht auf, wohl aber in der 
Realität, durch prellende Schalter, oder - in meinem Fall, bei einem 
magnetischen Drehgeber - z.B. bei extrem langsamer Drehung. Dann ensteht 
nur wenig Induktionsspannung, und die Komparatoren können schon einmal 
einen Schritt "übersehen", oder auch Störimpulse bei "manuellem Zittern" 
an der Achse prduzieren.

In diesem Fall ("case 3:") setze ich alles zurück, und fange mit den 
aktuellen Bit vom Drehgeber neu an. Die Folge ist, daß sich bei äußerst 
langsamer Drehung (Größenordnung 1-2 U/min!) gar nichts mehr ändert, was 
mir aber viel angenehmer ist, als dabei auf und ab "wackelnde" Werte für 
den Encoder-Stand.


Deine Vermutung bez. der fehlenden Header-Dateien ist genau richtig! 
(...war ja klar, daß ich beim Rauskopieren doch etwas wichtiges 
vergessen "musste"... ;-) )


Und auch diese Interpretation ist völlig richtig: 
"HandleRotaryEncoder()" muß lauten: "void AccumulateRotaryCount()" - 
diese Routine habe ich "schnell im Blindflug" dazugeschrieben (da gab's 
in meinem Code nichts griffiges zum herauskopieren). Ich wollte aber 
nicht ganz offen zu lassen, wie man die im Interrupt-Handeler 
gesammelten Schritte weiterverarbeiten kann.

Marc Höner schrieb:

> Das die Timervariablen für den ATMega8 angepasst werden müssen ist klar,
> das bekomme ich auch hin, hoffe ich.

Die Eckdaten bei mir sind ein ATmega328P mit 16 MHz Taktfrequenz, ohne 
jeden System-Clock-Divider, so daß bei alle 256 us ein Interruptaufruf 
erzeugt wird.

Fast 4 kHz Interruptfrequenz sind wohl schon recht hoch - ich glaube 
hier im Forum wird meistens weniger empfohlen oder für erforderlich 
gehalten. Aber auch meine Werte erzeugen gerade einmal 2 bis 4 % 
"Grundlast", je nachdem was sonst noch im Interrupt-Handler erledigt 
wird.

Falls Du die "normale" Encoder-Decodierung bereits selbst implementiert 
hast, und nur noch ein Beschleunigungs-"Addon" suchst, kanst Du auch 
versuchen, die dazu gehörenden Teile meines Codes bei Dir an die 
passenden Stellen zu "pflanzen":

Diese drei Zeilen müssen bei jedem Interrupt ausgeführt werden. Das ist 
die "Ent"-Schleunigung:

  EncAccel -= ENCODER_ACCEL_DEC;
  if (EncAccel & 0b1000000000000000)
    EncAccel = 0; // Zero if overflowed

Diese Zeilen dort, wo die Vorwärts/Rückwärts-Drehung behandelt wird. 
Hier werden zum einen die erfassten Drehgeber-Schritte um von der 
aktuellen Beschleunigung abhängige Werte erhöht, und die 
"Be"-Schleunigung selbst wird auch erhöht (bis zu einem Grenzwert):

  EncSteps @= 1+(EncAccel >> 8);
  if (EncAccel <= (ENCODER_ACCEL_TOP-ENCODER_ACCEL_INC))
    EncAccel += ENCODER_ACCEL_INC;

Anstelle des "@" muß ein Plus- oder Minuszeichen stehen, je nachdem, 
welche Drehrichtung dort gehandelt wird.


Falls Du ebenfalls ungültige Schritte erkennst/behandelst, ist es 
ratsam, in diesem Fall die Beschleunigung auf Null setzen:

  EncAccel = 0;

Mehr ist kaum nötig, um ein haptisch hübsch auf schnelleres und 
langsameres Drehen reagierendes Beschleunigungsverhalten zu 
implementieren. Die Anpassung der Beschleunigungs-Konstanten an die 
eigenen Gegebenheiten erfordert dann noch ein wenig Geduld.

von Marc H. (bauerpilot)


Lesenswert?

Du bist ja schneller als der Schall und dazu sind Deine Erläuterungen 
sehr ausführlich, dafür herzlichen Dank.
Ich bin jetzt schon wieder einen deutlichen Schritt weiter und gerade 
dabei das ganze in meinen laufenden Code zu integrieren, also die 
"Addon-Version".
Leider ist mir gestern Abend mein optischer Drehencoder abgeraucht, man 
sollte doch etwas sorgfältiger arbeiten. Sobald ein neuer da ist werde 
ich es damit ausprobieren, solange kann ich nur den Code 
vervollständigen und hoffen, dass das Ganze auch funktioniert.
So ganz klar ist mir das Prinzip aber noch nicht.
Hier meine ISR, wo die "Ent-Schleunigung" integriert werden muss:

  neu_1 = 0;
  if( ENC_1_A )
    neu_1 = 3;
  if( ENC_1_B )
    neu_1 ^= 1;                             // convert gray to binary
  diff_1 = last_1 - neu_1;                  // difference last - new
  if( diff_1 & 1 ){                         // bit 0 = value (1)
    last_1 = neu_1;                         // store new as next last
    enc_delta_1 += (diff_1 & 2) - 1;        // bit 1 = direction (+/-)
  }

Allerdings finde ich keine Unterscheidung zwischen Vorwärts und 
Rückwärts-Drehung, nach meinem Verständnis ist enc_delta_1 
vorzeichenbehaftet und die Drehrichtung ergibt sich aus dem Vorzeichen. 
Muss ich hier eine if-Abfrage einbauen, ob enc_delta_1 positiv oder 
negativ ist und dann je nach Ergebnis:

  EncSteps @= 1+(EncAccel >> 8);
  if (EncAccel <= (ENCODER_ACCEL_TOP-ENCODER_ACCEL_INC))
    EncAccel += ENCODER_ACCEL_INC;

einfügen? enc_delta_1 entspricht Deinem EncSteps, oder?

von SteppRider (Gast)


Lesenswert?

Marc Höner schrieb:
> Du bist ja schneller als der Schall und dazu sind Deine Erläuterungen
> sehr ausführlich, dafür herzlichen Dank.

Gerne geschehen. Ich schaue hier unregelmäßig vorbei, und wenn ich dann 
ein wenig Zeit habe...  ...schließlich habe ich hier auch unbezahlbar 
viel gelernt!

Marc Höner schrieb:
> ... nach meinem Verständnis ist enc_delta_1 vorzeichenbehaftet ...
> ... Muss ich hier eine if-Abfrage einbauen ...

Zweimal ja.

Und zusätzlich ist noch zu beachten, daß die Inkrement-/Dekrement-Werte 
nicht mehr fix -1 bzw. +1, sondern je nach Beschleunigung größere 
Beträge sein können.

1
enc_delta_1 += (diff_1 & 2) - 1; // bit 1 = direction (+/-)

So ist das hier im Artikel-Wiki im ersten Beispiel-Code zu finden, 
elegant sparsam codiert. Ohne Beschleunigung soll "enc_delta_1" bei 
jedem Schritt einfach nur um 1 erhöht oder erniedrigt werden. Nachdem 
mit über Bit 0:

1
if( diff_1 & 1 )

ermittelt wird, ob überhaupt ein Schritt detektiert wurde, muß nur noch 
das Richtungsbit (Bit 1) ausgewertet werden, um "enc_delta_1" zu 
inkremetieren oder zu dekrementieren. Dazu wird alles ausser Bit 1 
maskiert (mit "& 2" auf "0" gesetzt), sodaß je nach Richtung der Wert 
"0", oder der Wert "2" übrigbleibt. Davon wird jeweils noch 1 
subtrahiert, und fertig ist die "1" mit richtigem Vorzeichen:
1
  00000000  = 0,     -1   ergibt:  11111111 = -1
2
  00000010  = 2,     -1   ergibt:  00000001 = +1

Leider ist dieser Code- und eine Verzweigung sparende Trick nur bei 
fixer Schrittweite, wie +/-1 möglich, aber nicht mehr so leicht, wenn 
zusätzlich ein dynamischer Beschleunigungswert zu berücksichtigen ist. 
Beispielsweise so könnten beide Codes zusammengeführt werden:
1
01 ------------------------------------------
2
02  EncAccel -= ENCODER_ACCEL_DEC;      // wird hier zuviel subtrahiert,
3
03  if (EncAccel & 0b1000000000000000)  // wird EncAccel negativ, und
4
04    EncAccel = 0;                     // genau dann hier auf 0 gesetzt
5
05 ------------------------------------------
6
06  neu_1 = 0;
7
07  if( ENC_1_A )
8
08    neu_1 = 3;
9
09  if( ENC_1_B )
10
10    neu_1 ^= 1;             // convert gray to binary
11
11  diff_1 = last_1 - neu_1;  // difference last - new
12
12
13
13  if (diff_1 & 1)
14
14  {// diff_1: xxxxxxx1
15
15    last_1 = neu_1;
16
16 --------------------------------------------------------------
17
17    if (diff_1 & 2)
18
18      enc_delta_1 += 1+(EncAccel >> 8)  // diff_1: xxxxxx11
19
19    else
20
20      enc_delta_1 -= 1+(EncAccel >> 8); // diff_1: xxxxxx01
21
21 --------------------------------------------------------------
22
22    if (EncAccel <= (ENCODER_ACCEL_TOP-ENCODER_ACCEL_INC))
23
23      EncAccel += ENCODER_ACCEL_INC;
24
24 --------------------------------------------------------------
25
25 }

Jetzt wird nach der Detektierung der Richtung (Zeile 17) im if- bzw. 
else-Zweig "enc_delta_1" um mindestens 1 zuzüglich des 
Beschleunigungswertes verändert.

Der Anstieg der Beschleunigung (Zeilen 22,23) muß nur hier einmal 
codiert werden - er ist ja Richtungs-unabhängig.

Die Funktion hinzugefügten Abscnitte würde ich etwa so beschreiben:
1
Zeilen 02-04 = Ent-Schleunigung (bei jedem Interrupt)
2
Zeile  14    = Entscheidung Schritt oder nicht?
3
Zeile  17    = Richtungsentscheidung 
4
Zeilen 18,19 = Inkrementierung/Dekrementierung je nach Richtung
5
Zeilen 22-23 = Be-Schleunigung (falls ein Schritt erkannt wurde).


VORSICHT! Aktuell ausprobiert habe ich diese 25 Zeilen jetzt nicht...

...aber vor ca. 5/6 Wochen eine Reihe unterschiedlicher Encoder-Routinen 
auf Tauglichkeit für meinen magnetischen Geber getestet, und diese dabei 
auch zusätzlich mir einer Beschleunigungsfunktion versehen. Ganz 
Änhliches wie oben aufgeführt war auch darunter...

Marc Höner schrieb:
> enc_delta_1 entspricht Deinem EncSteps, oder?

Ganz genau.

von Marc H. (bauerpilot)


Lesenswert?

Das macht richtig Spass Deine ausführlichen Erklärungen zu lesen. Habe 
wieder viel dazu gelernt.
Werde den Code, wie von Dir vorgeschlagen, implementieren und über das 
Ergebnis berichten.
Herzlichen Dank.

von Marc H. (bauerpilot)


Lesenswert?

Alles Bestens, habe Deinen Code verstanden und integriert. Jetzt läuft 
es Super. Herzlichen Dank.
Mit
1
#define ENCODER_ACCEL_TOP    1536
wird die maximale Beschleunigung angegeben?
Und mit
1
#define ENCODER_ACCEL_INC    96
der Anstieg der Beschleunigung?
Man wird ja ehrgeizig und ich versuche die Haptik noch etwas zu 
optimieren.
Nochmals herzlichen Dank.
Gruß Marc

von SteppRider (Gast)


Lesenswert?

Marc Höner schrieb:
> Alles Bestens, habe Deinen Code verstanden und integriert. Jetzt läuft
> es Super. Herzlichen Dank.

Das freut mich & Gerne geschehen!

Marc Höner schrieb:
> Mit
       #define ENCODER_ACCEL_TOP    1536
  wird die maximale Beschleunigung angegeben?
> Und mit
        #define ENCODER_ACCEL_INC    96
  der Anstieg der Beschleunigung?

Richtig.

> Man wird ja ehrgeizig und ich versuche die Haptik noch etwas zu
> optimieren.

Ganz sicher - ich habe übrigens auch 'mal Andere "Probedrehen" lassen - 
das war durchaus hilfreich.

Noch ein paar Anmerkungen zur den Beschleunigungs-Konstanten in meiner 
Software-/Hardware-Umgebung.

Vorraussetzungen: Bei mir wird knapp 4000mal pro Sekunde der 
entsprechende Interrupt ausgelöst, und mein Drehgeber liefert 50 
Schritte pro Umdrehung.

Die Beschleunigung wird wird bei jedem erkannten Schritt um 
"ENCODER_ACCEL_INC" erhöht. Drehe ich mit 1U/sec also um 50*96 = 4800. 
(1)

Gleichzeitig wird aber pro Interrupt die Beschleunigung wieder um 
"ENCODER_ACCEL_DEC" verringert. Also pro Sekunde um 4000*1 = 4000. (2)

Damit bleiben bei 1U/sec 4800-4000 = 800 "Beschleunigungpunkte" übrig.

Mit "(EncAccel >> 8)" werden diese "Beschleunigungspunkte" durch 256 
dividiert und zur Mindestschrittweite 1 addiert - der akkumulierte 
Drehgeberwert ändert sich dannt um jeweils 4 pro Schhritt: 
(int)(800/256) = 3 + 1.

(Das ist natürlich nur eine grobe Betrachtung der Verhältnisse nach 
einer Sekunde, in der der Drehgeber mit konstanter Geschwindigkeit um 
360 Grad gedreht wurde.)

Bereits um nur meine Beschleunigungskurve nachzubilden, musst Du die 
Konstanten an Deine Gegebenheiten anpassen. Macht Dein Drehgeber 25 
statt 50 Schritte also bspw. "ENCODER_ACCEL_DEC" verdoppeln (siehe 1).

Tastest Du mit 1000 Hz Interrupt-Frequenz ab, muss "ENCODER_ACCEL_DEC" 
vervierfacht werden (siehe 2).

Und wie man sieht, liegt bei mir "ENCODER_ACCEL_DEC" bereits am unteren 
Limit, "1". Da weniger nicht geht, müsste falls erforderlich, der 
Shiftwert in "(EncAccel >> 8)" vergrößert werden, und bspw. mit 
Shiftwerten von 9 bis 12 durch 512 bis 4096 dividiert werden (selbst im 
letzten Fall bleiben noch 4 Bit für Beschleunigungswerte zwischen 0 und 
15 übrig). Alle Konstannten müssen dann aber entsprechend angepasst 
werden:

Wird bspw. 10mal geshiftet, d.h. durch 1024 (= 256 * 4) dividiert erhält 
man mit:

  ENCODER_ACCEL_TOP    6144
  ENCODER_ACCEL_INC    384
  ENCODER_ACCEL_DEC    4

wieder die gleichen Bedingungen, wie in meiner ursprünglichen Software.

Da bei mir "(EncAccel >> 8)" gut passt, habe ich das so gelassen - 
anstelle von 8 Shifts verwendet der Compiler in diesem Fall nämlich 
einfach und Code-sparend das High-Byte von "EncAccel" zum Weiterrechnen.

MfG

von Marc H. (bauerpilot)


Lesenswert?

Herzlichen Dank für Deine umfassenden Informationen, jetzt habe ich ein 
ausgezeichnetes haptisches Gefühl.

Gruß Marc

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.