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
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.
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.
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.
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?
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
02EncAccel-=ENCODER_ACCEL_DEC;// wird hier zuviel subtrahiert,
3
03if(EncAccel&0b1000000000000000)// wird EncAccel negativ, und
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:
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.
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.
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