Forum: Projekte & Code Drehgeber/Encoder 1-, 2- oder 4-schrittig


von Peter D. (peda)


Angehängte Dateien:

Lesenswert?

Ich hab jetzt nochmal den Code schön gemacht und hier reingestellt, weil 
der Ursprungsthread doch recht unübersichtlich ist.
Und auch, weil regelmäßig Threads zu Problemen mit anderen Codes im 
Forum auftauchen. Das Thema ist also immer noch sehr aktuell.
Auch wenn dieser Code sehr kurz ist, ist das Thema keineswegs trivial.


Besonders habe ich nun das Problem der 2- und 4-schrittigen Drehgeber 
behandelt.

Es sind jetzt 3 Auslesefunktionen drin, brauchen tut man aber nur die 
eine zu Deinem Drehgeber passende.
Man hätte das auch per Defines machen können, aber ich wollte auch 
Anfänger nicht zu sehr verwirren.

Der besondere Kniff an den Mehrschritt-Routinen ist, daß die nicht 
verwendeten 1 oder 2 unteren Bits erhalten bleiben müssen. Nur dadurch 
ist weiterhin die Entprellung gewährleistet.


Das Prinzip ist immer noch das gleiche, also Abtastung im 
Timerinterrupt. Damit ergibt sich eine automatische Entprellung.

Eine Entprellung in Hardware mit RC-Gliedern hat demgegenüber den 
Nachteil, daß sie schwer zu dimensionieren ist.
Wählt man die Zeitkonstante zu kurz, können Preller durchkommen, wählt 
man sie zu lang, kommt es zu Fehlern bei schnellem Drehen.


Ich habe das Programm mit nem Drehgeber mit 96 Stellungen und 15mm 
Drehknopf getestet. Man muß schon sehr schnell drehen, damit Schritte 
verloren gehen.


Wichtig ist, daß die Auslesefunktion "encode_read*()" so oft aufgerufen 
wird, daß nicht mehr als 127 Schritte dazwischen liegen.
Wenn das nicht gehen sollte, kann man aber die Schrittvariable und die 
Auslesefunktion einfach auf 16 Bit erweitern (int16_t), dann sinds 32767 
Schritte, das sollte dann reichen.


Peter

von Sven S. (schwerminator)


Lesenswert?

Moin Peter,
ich nutze deinen Code um einen 2-schrittigen Drehgeber auszulesen. Das 
klappt auch hervorragend, wenn ich zum Beispiel einen Wert hoch- und 
herunterzähle. Nun möchte ich aber den Drehgeber zur Navigation in einem 
Menu einsetzen. Die Hauptschleife in meiner main.c sieht wie folgt aus:
1
while(1){
2
  if(input_read_encoder() > 0)
3
    menu_next_entry(&menu_context);
4
  else if(input_read_encoder() < 0)
5
    menu_prev_entry(&menu_context);
6
  else if(input_get_key())
7
    menu_select(&menu_context);
8
}

Es wird also immer die Funktion, die bei dir encode_read2() heißt 
aufgerufen und überprüft ob man mit oder gegen den Uhrzeigersinn gedreht 
hat. Das Ganze klappt aber nicht wirklich zuverlässig. Theoretisch soll 
bei jedem Einrasten entweder die Funktion menu_next_entry() oder 
menu_prev_entry() aufgerufen werden. Dem ist aber nicht so, denn 
manchmal muss ich den Drehgeber 4-5 Rastungen weit drehen, damit auf den 
nächsten Menüpunkt gesprungen wird. Manchmal reicht aber auch eine 
Rastung. Setze ich deinen Code möglicherweise falsch ein?

Vielen Dank, Sven

von Peter D. (peda)


Lesenswert?

Wenn Du die Funktion nicht an mehreren Stellen gleichzeitig aufrufst, 
sollte es funktionieren.


Gib dochmal die Variable "val" aus meinem Beispiel auf das LCD aus, ob 
sie richtig zählt.

Ich hab den Code noch leicht verbessert:

http://www.mikrocontroller.net/articles/Drehgeber#Beispielcode_in_C

damit er schon nach der Initialisierung richtig zählt und das "volatile" 
hinzugefügt, damit er auch bei Inlining funktioniert.


Peter

von Sven S. (schwerminator)


Lesenswert?

Hallo Peter,
der Tipp mit dem mehrmaligen Aufrufen war schonmal Gold wert, viel 
besser, aber dennoch brauche ich manchmal noch 2-3 Dreher, um zu 
springen. Woran könnte es jetzt noch liegen?
1
while(1){
2
  val = input_read_encoder();
3
    
4
  if(val > 0)
5
    menu_next_entry(&menu_context);
6
  else if(val < 0)
7
    menu_prev_entry(&menu_context);
8
  else if(input_get_key())
9
    menu_select(&menu_context);
10
}

Und ja, wenn ich val einfach hoch- und runterzählen lasse, klappts 
perfekt.

mfG, Sven

von Bill (Gast)


Lesenswert?

>> der Ursprungsthread
welcher?

>> den Code
welcher?

von D. E. (stb)


Lesenswert?

Ursprungsthread:
Beitrag "Drehgeber auslesen"


Code: http://www.mikrocontroller.net/attachment/40597/ENCODE.C

Siehe "Dateianhang: ..." im ersten Beitrag dieses Threads.

von Sven S. (schwerminator)


Lesenswert?

Ich glaube ich habe jetzt die Geschichte gelöst: Vorher war die 
Abtastfrequenz bei 100Hz, ich dachte das reicht bei langsamen Drehen. 
Nun habe ich 1kHz und es läuft problemlos. Tja da dachte ich, ich könnte 
das Interruptaufkommen reduzieren, geht aber anscheinend nicht. Egal.

Danke nochmal.

von Peter D. (peda)


Lesenswert?

Versuch mal, ob es so besser geht:
1
while(1){
2
  val += input_read_encoder();
3
    
4
  if(val > 0){
5
    val--;
6
    menu_next_entry(&menu_context);
7
  }else if(val < 0){
8
    val++;
9
    menu_prev_entry(&menu_context);
10
  }else if(input_get_key())
11
    menu_select(&menu_context);
12
}


Peter

von Stephan H. (stephan-)


Lesenswert?

@Peter ( danni )

wenn ich Deinen Code im µVision compilieren will, hagelt es Fehler ohne 
Ende.
Ich meine die aus dem Ursprungsthread.
Wo Du beide Varianten ( Tabelle und Umwandlung ) darstellst.

Fehlermeldung von Keil:

compiling Gray.c51...
GRAY.C51(25): error C141: syntax error near 'INT_T0', expected 'const'
GRAY.C51(26): error C132: 'INT_T0': not in formal parameter list
GRAY.C51(26): error C141: syntax error near '{'
Target not created
etc.pp.

Ich habe keine Ahnung von "C" . Wo liegt das Problem ???

Hintergrund:
Ich habe mit meiner ASM Routine einige Sorgen.

Beitrag "Probleme mit dem drehgebr 8051 in ASM"

Daher will ich mal sehen was Deine Routine draus macht.


Danke

von Peter D. (peda)


Lesenswert?

Stephan Henning wrote:
> Ich meine die aus dem Ursprungsthread.

Dann poste nächstes mal auch in dem richtigen Thread.
Denke mal an Deine Mitmenschen, die vielleicht auch Anfänger sind und 
dann völlig konfusioniert sind, weil Du was im falschen Thread postest 
:(


> GRAY.C51(25): error C141: syntax error near 'INT_T0', expected 'const'

Heißt, er kennt 'INT_T0' nicht.
Ich hab mir die Interrupt-Nummern in verstehbare Symbole definiert:
1
#define INT_EX0         0
2
#define INT_T0          1
3
#define INT_EX1         2
4
#define INT_T1          3
5
#define INT_UART        4
Einfügen, dann gehts.


> Ich habe keine Ahnung von "C" . Wo liegt das Problem ???

Das ist in der Tat ein Problem.
Ohne C-Kenntnisse wirst Du ja nicht verstehen, was Du machst.
Ich kann Dich auch nicht ans Händchen nehmen und Dir alles beibringen.
Ich würde Dir erstmal raten, C aufm PC zu lernen, z.B. mit men alten 
Borland-C im DOS-Fenster, damit man nicht den ganzen Windows-Schrunz 
mitmachen muß.


Peter

von Gast (Gast)


Lesenswert?

Hallo,
mal ne Frage:
muss ich bei zwei Drehgebern auch zweimal die gleiche Routine verwenden?
Gruß

von Peter D. (peda)


Lesenswert?

Gast
wrote:
> muss ich bei zwei Drehgebern auch zweimal die gleiche Routine verwenden?
> Gruß

Ja, Du mußt die benötigten Routinen kopieren und umbenennen, natürlich 
mit verschiedenen Pins und Variablen.


Peter

von Gast (Gast)


Lesenswert?

Hi Peter,
danke für die Antwort!
Gruß

von clonephone82 (Gast)


Lesenswert?

Hallo Peter,

In deinem Ursprungsthread hast du den Pin Change Interrupt erwähnt, ganz 
am Schluss.

Ich kenne die AVRs nicht so gut aber damit meinst du einen pin Interrupt 
der auf beide Flanken reagiert oder.

=> Also statt über einen Timer IRQ zu pollen - verwendet man einfach die 
pin change Interrupts => aber dann halt für die A-Spur und B-Spur sonst 
wird es ja nicht gehen - oder?

... also so pseudo code:

IRQ(A pin-schange)
{
   dein_enocde_code();
}


IRQ(B pin-change)
{
   dein_enocde_code();
}

Ich möchte auf einem 80MHz Controller ein Drehgeber damit auslesen. 
Maximale Drehfrequenz 70kHz.

Ich hoffe das es geht ohne die CPU zu sehr zu belasten.

sg
Danke

von Durchgeknobelt (Gast)


Lesenswert?

Ins Wiki 
https://www.mikrocontroller.net/articles/Drehgeber#Solide_L.C3.B6sung:_Beispielcode_in_C 
sollte unbedingt ein Link zur Erklärung in 
Beitrag "Re: Drehgeber auslesen" 
mit rein.

Leider habe ich ein Problem mit meinem Vier-Schritt-Encoder: es geht nur 
aufwärts. Abwärts wird sofort (d.h. bereits bei der ersten Flanke) in 
encode_read(4) ein Wert zurückgegeben.

Ersetze ich

    case  4: enc_delta = val & 3; val >>= 2; break;

durch

    steps = val % 4;
    val /= 4;

funktioniert es.
Kann es sein, dass das Maskieren vom int8_t (Zweierkomplement) die 
Ursache ist (Vorzeichen wird entfernt)? Oder wird das durch C++ anders 
behandelt?

von Durchgeknobelt (Gast)


Lesenswert?

Sorry: "enc_delta" heisst bei mir "steps".

von Durchgeknobelt (Gast)


Lesenswert?


von Uwe K. (ukhl)


Angehängte Dateien:

Lesenswert?

Ich habe das Beispiel von Peter Dannegger mal mit einem neuen 
Algorithmus versehen. Bitte nicht als Kritik verstehen, sondern als 
mögliche alternative, da es ein paar Vorteile hat.

Um es vergleichen zu können, war ich so frei und habe den Rahmen von 
Peter übernommen. Leider steht mir kein ATmega16 zu Verfügung. Das 
Beispiel ist auf ein ATtiny2313, mit nur leichten Anpassungen (Timer und 
PINs), portiert worden.

Vorgestellt wurde der Algorithmus schon im Beitrag 
Beitrag "Drehgeber (Rotary Encoder) hochauflösend und prellfrei decodieren"

Im angehängten Beispiel wurde mein Algorithmus integriert. Damit sind 
beide Varianten leicht vergleichbar.

Vorteile:
- Der Algorithmus ist immer prellfrei auch in der höchsten Auflösung.
- Drehgeber mit wackeligen Rastpunkten machen keine Probleme.
- Die Position der Schaltpunkte bei Einzel- und Doppelschritt Encoders 
ist besser.
- Alle Eigenschaften des ursprünglichen Codes bleiben erhalten.
- Es ist trotzdem sehr einfach aufgebaut und leicht verständlich.

Da es definitv Prellfrei ist, kann man auch ein Interrupt für beide PINs 
nehmen. Der Timer ist aber vorzuziehen und man sollte wissen was man 
tut. Eine bei Bedarf höhere Abtastrate gefährdet die Entprellung beim 
Drehgeber nicht.

Eine Anpassung für mehrere Drehgeber stelle ich gerne bei Bedarf zur 
Verfügung. Eine zweite Kopie der "encode" Funktion ist dann nicht 
notwendig.

von Veit D. (devil-elec)


Lesenswert?

Hallo,

schön das du dir Gedanken zum Encoder gemacht hast. Nur schlägt deine 
Beschreibung fehl. Alle genannten Vorteile hat der Code von Peter schon. 
Demnach kann es kein Vorteil gegenüber Peters Code sein. Du müsstest 
beschreiben was dein Code anders macht gegenüber Peters Code und welchen 
Vorteil du genau darin siehst. Sonst verläuft das ins Leere. Aktuell 
liefert er wohl das gleiche Ergebnis, gut, hat damit aber noch keinen 
Vorteil. Verstehste?

Übrigens was mir gar nicht gefällt ist ein uint8_t Rückgabeparameter von 
encode() und return +1 und -1. Und warum ist dgA signed und dgB 
unsigned? Sieht in allen etwas fraglich aus, sprich nicht 
Vertrauenswürdig. Getestet ist der Code aber?  ;-)

von Uwe K. (ukhl)


Angehängte Dateien:

Lesenswert?

Erstmal danke für deine konstruktive Kritik.

Du hast vollkommen recht, dass die Wahl der Typen falsch ist. Richtig 
muss es so aussehen:
1
int8_t encode(uint8_t dgA, uint8_t dgB)
Aber ja, es ist getestet! Sonst hätte ich es nicht für den ATtiny2313 
umschreiben müssen. In diesem Fall ist das kompilierte Ergebnis 
identisch.

> ... Alle genannten Vorteile hat der Code von Peter schon.

Da kann ich dir nicht zustimmen. Die genannten Vorteile existieren 
wirklich. Ich beschreibe es mal ausführlicher:
- Der Algorithmus ist immer prellfrei auch in der höchsten Auflösung.

Das Entprellen in Original entsteht durch den Abtastintervall, der 
idealerweise länger als das Prellen ist. In einem Datenblatt habe ich 
eine Prellzeit von max. 3ms gefunden. Hier wird mit 1ms abgetastet. Das 
ist zu schnell, und lässt den einen oder anderen Preller durch. Da das 
Prellen dennoch sehr stark reduziert wird, ist es in der Praxis nicht 
von Bedeutung.

Prellen beim Drehgeber erzeugt immer ein Richtungswechsel, da sich immer 
nur ein PIN ändert (Gray-Code). Dieser Richtungswechsel wird bei mir 
herausgefiltert. Fast alle Lösungen basieren auf das Speichern des 
vorherigen Zustands. Damit kann man zwar die Richtung, aber nicht den 
Richtungswechsel erkennen. Dieser Lösung speichert ZWEI vorherige 
Zustände, und kann damit den Richtungswechsel erkennen. Es wird nur eine 
Sequenz von ZWEI Änderungen in die gleiche Richtung zugelassen. Dadurch 
wird jedes Prellen ignoriert. Das Abtastintervall darf deutlich kürzer 
sein als die maximal zu erwartende Prellzeit, da es für das Entprellen 
nicht notwendig ist.
- Drehgeber mit wackeligen Rastpunkten machen keine Probleme.

Das ist ein signifikanter Vorteil. Das Problem tritt beim Original 
nachgewiesen auf und wird im Artikel auch behandelt. 
https://www.mikrocontroller.net/articles/Drehgeber#Dekoder_f%C3%BCr_Drehgeber_mit_wackeligen_Rastpunkten

Hier wird auch darauf hingewiesen, dass die Spur A und B nicht beliebig 
vertauschen kann und man die Auflösung halbieren muss.

In meinem Programm ist das keine Problem. Auch der wackelnde Rastpunkt 
ist ein einzelner PIN-Wechsel, wie es beim Prellen auftritt. Und dieser 
wird immer herausgefiltert. Auch in der höchsten Auflösung. Der PIN muss 
nicht beachtet werden und eine Halbierung der Auflösung ist nicht 
notwendig.
- Die Positionen der Schaltpunkte bei Einzel- und Doppelschritt Encoder
sind besser.

Ich versuche es mal grafisch darzustellen. Die halbe Sequenz:

Vorwärts:
1
Rastung  |       |       |       |       |
2
A       ---|___|___|---|---|___|___|---|---|
3
B       ---|---|___|___|---|---|___|___|---|
4
Peter          |       |       |       |
5
Uwe            |       |       |       |
Das ist identisch und sieht gut aus. Jetzt rückwärts:
1
Rastung  |       |       |       |       |
2
B       ---|---|___|___|---|---|___|___|---|
3
A       ---|___|___|---|---|___|___|---|---|
4
Peter      |       |       |       |
5
Uwe            |       |       |       |

Hier sieht man, dass bei Peter der Schritt schon beim ersten Impuls 
gemacht wird. Idealerweise sollte es beim zweiten Impuls erfolgen. Damit 
reduziert man die Wahrscheinlichkeit beim Drücken des Drehgebers weiter 
zu drehen.

Jetzt das ganze bei einer ganzen Sequenz:

Vorwärts:
1
Rastung  |               |               |
2
A       ---|___|___|---|---|___|___|---|---|
3
B       ---|---|___|___|---|---|___|___|---|
4
Peter                  |               |
5
Uwe                |               |
Hier wird bei Peter etwas später geschaltet. Das ist unproblematisch. 
Und rückwärts:
1
Rastung  |               |               |
2
B       ---|---|___|___|---|---|___|___|---|
3
A       ---|___|___|---|---|___|___|---|---|
4
Peter      |               |
5
Uwe                |               |
Es wird bei Peter sehr früh geschaltet. Wehe man hat einen wackeligen 
Drehgeber. Und die Gefahr des versehentlichen Drehens beim Drücken ist 
hoch.

Idealerweise schaltet man mit dem Impuls nach dem Durchschreiten der 
Mitte. Das ist bei mir der Fall.
- Alle Eigenschaften des ursprünglichen Codes bleiben erhalten.

OK. Das ist kein Vorteil. Aber auch kein Nachteil.
- Es ist trotzdem sehr einfach aufgebaut und leicht verständlich.

Technisch kein Vorteil. Für Einsteiger ist es aber leichter zu verstehen 
als der Code von Peter. Das hat Peter doch tief in die Trickkiste 
gegriffen, was für seine hervorragenden Fähigkeiten spricht. Für ein 
Einsteiger ist es aber nicht so einfach zu verstehen (schon genial, wie 
aus einem Gray-Code eine Binärzahl wird). Die Portierung in andere 
Sprachen ist bei mir etwas einfacher.

Welche Lösung man bevorzugt, kann natürlich jeder für sich entscheiden. 
Die Lösung von Peter ist "gut abgehangen" und hat sich über Jahre 
bewährt. Es schadet aber nicht meine Lösung zu verstehen und in die 
Auswahl mit einzubeziehen. Vorteile sind durchaus vorhanden.

von Veit D. (devil-elec)


Lesenswert?

Hallo,

ich versuche es gerade zu verstehen und habs getestet. Ich versuche es 
einmal zu beschreiben.

> mit 1ms Leseraster:
Dein Code unterdrückt das pendeln des aktuellen Wertes, was man beim 
Code von Peter im Ergebnis ganz selten sieht. Beschreibt Peter ja auch 
in seiner Doku. Peters Code wertet alles aus und korrigiert sich solange 
selbst bis die Kontakte nicht mehr prellen. Das ist Peters Trick damit 
trotz prellen am Ende, wenn es sich ausgeprellt hat, der korrekte Wert 
erhalten bleibt. Dieses seltene sichtbare "pendeln" unterdrückt dein 
Code. Ich weiß noch nicht genau wie, aber das ist meine Beobachtung. So 
wie du es beschreibst.

> ohne 1ms Zeitraster:
Maximale Aufrufgeschwindigkeit. Das ist der Härtetest. Hier sieht man 
ein zurückspringen eines Wertes deutlicher trotz weiterdrehen am 
Encoder. Das ist bei Peters Code ausgeprägter zu beobachten. Entspricht 
seiner Beschreibung. Bei deinem Code ist das weniger der Fall, es ist 
sozusagen seltener und weniger der Fall.

Also aktuell zählt dein Code sichtbar schöner mit erforderlichen 1ms 
Zeitraster, weil man das "auspendeln" nicht mehr sieht. Dafür bekommste 
ein Bienchen.  :-)

Aktuell noch ein Kommentar zum Code.
Bitte die Kommentare "1:1" und "1:4" vertauschen und dein enc_delta muss 
volatile sein.
Ich bin gerade am überlegen wie man die 3 case switch wegbekommt ...

: Bearbeitet durch User
von Veit D. (devil-elec)


Lesenswert?

Hallo,

habe etwas Kosmetik betrieben und einen Raster Parameter eingesetzt.
Jetzt könnte man noch überlegen die Arbeit in der ISR zu reduzieren. 
Muss das alles in der ISR erfolgen? Man könnte doch nach dem auslesen 
der Phasen und der letzten digitVal Zuweisung die "Rechenlast" der ISR 
beenden und alles weitere in der mainloop berechnen lassen? Abhängig 
davon ob sich digitVal geändert hat oder nicht?

Die unteren Leerzeilen im Code sind nicht von mir. ;-) Muss irgendwie 
die Forumssoftware einfügen.
1
/*
2
  https://www.mikrocontroller.net/topic/112603?goto=7084956#7084956
3
  02.06.2022
4
  - Arduino Mega2560
5
  - billiger Alps 11 Encoder
6
  - funktioniert soweit
7
  - switch case Umbau und Raster Parameter
8
  - mit Timer
9
*/
10
11
volatile int8_t encDelta;        // -128 ... 127
12
int32_t value;
13
int32_t absolutCounter;
14
15
const uint8_t encoderRaster {2};  // gewöhnlich 1, 2 oder 4
16
const uint8_t pinPhaseA {18};
17
const uint8_t pinPhaseB {19};
18
#define PHASE_A (PIND & _BV(3))
19
#define PHASE_B (PIND & _BV(2))
20
21
void setup (void)
22
{
23
  pinMode(pinPhaseA, INPUT_PULLUP);
24
  pinMode(pinPhaseB, INPUT_PULLUP);
25
  initTimer1();
26
  Serial.begin(250000);
27
  Serial.println("\nuC Reset ### ### ###");
28
}
29
30
void loop (void)
31
{
32
  absolutCounter = absolutCounter + encodeRead();
33
  value = absolutCounter >> (encoderRaster / 2);
34
  seriellerMonitor (Serial, value);
35
}
36
37
void seriellerMonitor (Stream &out, const int32_t &value)
38
{
39
  static int32_t valueOld {987654321};
40
41
  if (valueOld != value)
42
  {
43
    out.println(value);
44
    valueOld = value;
45
  }
46
}
47
48
49
int8_t encodeRead(void)      // read single step encoders
50
{
51
  const uint8_t saveSREG {SREG}; 
52
  int8_t val {encDelta};
53
  SREG = saveSREG;
54
  encDelta = 0;
55
  return val;          // counts since last call
56
}
57
58
59
// return +1 for forward and -1 for backward
60
int8_t encode(const uint8_t phaseA, const uint8_t phaseB)
61
{
62
  static uint8_t digitHist {0};          // values history
63
  uint8_t digitVal         {0};          // current value Bit 1 and 2
64
65
  digitVal = 0;
66
  if (phaseA != 0) digitVal |= 0b01;
67
  if (phaseB != 0) digitVal |= 0b10;
68
69
  if ( ( digitHist & 0b11 ) == digitVal) return 0; // no change --> return
70
71
  digitHist = digitHist << 2;
72
  digitHist = ( digitHist | digitVal ) & 0b00111111;
73
74
  ////////////////////////////////////////////////////////////////////
75
  // Uncomment one of the switch statement to select the resolution //
76
  ////////////////////////////////////////////////////////////////////
77
78
  int8_t returnValue {0};
79
80
  switch (digitHist) // 1:1 four step encoders, wird noch laut Rastung passend geschiftet
81
  {
82
    case 0b111000: returnValue =  1; break;
83
    case 0b100001: returnValue =  1; break;
84
    case 0b000111: returnValue =  1; break;
85
    case 0b011110: returnValue =  1; break;
86
    case 0b110100: returnValue = -1; break;
87
    case 0b010010: returnValue = -1; break;
88
    case 0b001011: returnValue = -1; break;
89
    case 0b101101: returnValue = -1; break;
90
    default: break;
91
  }
92
93
  return returnValue;
94
}
95
96
void initTimer1(void)
97
{
98
  TCCR1B = 0;
99
  TCCR1A = 0;                    
100
  TCNT1  = 0;                             
101
  OCR1A  = 249;                                // 1ms            
102
  TIMSK1 = _BV(OCIE1A);           
103
  TCCR1B = _BV(WGM12) | _BV(CS11) | _BV(CS10); // CTC, Prescaler 64
104
}
105
106
ISR( TIMER1_COMPA_vect )        // 1ms for manual movement
107
{
108
  encDelta = encDelta + encode(PHASE_A, PHASE_B);
109
}

von Uwe K. (ukhl)


Angehängte Dateien:

Lesenswert?

Die drei Case alternativen wurden nicht willkürlich gewählt.
Die Masken sind speziell abgestimmt, um den Schaltpunkt zu wählen.

Speziell
1
  value = absolutCounter >> (encoderRaster / 2);
zerstört die Schaltpunkte. Jetzt hast Du wieder die gleichen wie bei 
Peter.

Wähle immer das richtige CASE Konstrukt für deine Anforderung. Hier ist 
eine bedingte Kompilierung nützlich. Ich habe meine Aktuelle h-Datei 
angehängt. Diese enthält noch einen Parameter für bis zu 8 Encoder.

> Muss das alles in der ISR erfolgen?
Ein klares JA. Das muss sein. "encDelta" muss im Interrupt aktualisiert 
werden. Die Dauer der Main-Loop ist unbestimmt. So könnten dir 
Änderungen verloren gehen.

von Veit D. (devil-elec)


Lesenswert?

Uwe K. schrieb:
> Die drei Case alternativen wurden nicht willkürlich gewählt.
> Die Masken sind speziell abgestimmt, um den Schaltpunkt zu wählen.
>
> Speziell
>
1
>   value = absolutCounter >> (encoderRaster / 2);
2
>
> zerstört die Schaltpunkte. Jetzt hast Du wieder die gleichen wie bei
> Peter.

Okay. Funktioniert jedoch weiterhin so gut wie vor meiner Änderung. :-)

> Wähle immer das richtige CASE Konstrukt für deine Anforderung. Hier ist
> eine bedingte Kompilierung nützlich. Ich habe meine Aktuelle h-Datei
> angehängt. Diese enthält noch einen Parameter für bis zu 8 Encoder.

Gut, dass würde ich dann mittels Lib und angelegten Objekten erschlagen.

>> Muss das alles in der ISR erfolgen?
> Ein klares JA. Das muss sein. "encDelta" muss im Interrupt aktualisiert
> werden. Die Dauer der Main-Loop ist unbestimmt. So könnten dir
> Änderungen verloren gehen.

Nicht das wir uns hier falsch verstehen. Ich wollte nicht die gesamte 
ISR abschaffen. Ich wollte nur das Pin auslesen bis
1
if ( ( digitHist & 0b11 ) == digitVal) return 0; // no change --> return
in der ISR lassen. Alles danach könnte doch außerhalb der ISR erfolgen?

von Uwe K. (ukhl)


Lesenswert?

> Gut, dass würde ich dann mittels Lib und angelegten Objekten erschlagen.
Ich habe so eingebunden:
1
#define RE_DETENT 2
2
#include "rotaryEncode.h"
RE_DETENT ist die Anzahl der Rastungen pro Zyklus. 1, 2 oder 4.

Und dann wird es so aufgerufen:
1
encDelta = encDelta + rotaryEncode(PHASE_A, PHASE_B, 0);
oder auch:
1
encDelta += rotaryEncode(PHASE_A, PHASE_B, 0);
Der dritte Parameter ist die Encoder Nummer und kann von 0 bis 7 gehen. 
Praktisch, wenn man mehrere Encoder angeschlossen hat.

> ... Ich wollte nur das Pin auslesen bis
>
1
> if ( ( digitHist & 0b11 ) == digitVal) return 0; // no change --> return
2
>
> in der ISR lassen. Alles danach könnte doch außerhalb der ISR erfolgen?
So viel kommt dann nicht mehr. Ein klares nein.

P.S.: Meine erste Erfahrung mit Arduino. Ich benutze sonst C mit 
Atmel-Studio (nicht C++). Das ist nicht meins... 🙄

von LostInMusic (Gast)


Lesenswert?

>Alles danach könnte doch außerhalb der ISR erfolgen?

Ja, natürlich. Die einzige Aktion, die unverhandelbar in der ISR 
stattfinden muss, ist das Abspeichern der aktuellen Pegel der 
Encoder-Pins ins SRAM.

Zwiespältig wird die Sache auch erst, wenn die Encoder-ISR öfter (z. B. 
16 mal so oft) aufgerufen wird als die Main. Dann gibt es zwei 
grundsätzliche Möglichkeiten:

(1) Man möchte die ISR so kurz wie möglich halten. Dann ist man 
gezwungen, für das Abspeichern einen (hier 16-fach-) Puffer 
einzurichten, der von der ISR beschrieben und von der Main ausgelesen 
wird. Nachteil: Puffer = mehr Code, mehr CPU-Zeit, mehr Gehirnschmalz, 
mehr Fehlerquelle.

(2) Man möchte sich das Geraffel mit dem Puffer sparen. Dann ist man 
gezwungen, direkt in der ISR zu rechnen. Nachteil: Die ISR braucht 
länger.

Auf die Frage, welche dieser beiden Optionen "besser" ist, gibt es keine 
allgemeingültige Antwort. Das muss man für einen konkreten 
Anwendungsfall individuell entscheiden.

von DerEinzigeBernd (Gast)


Lesenswert?

1
  switch (digitHist) // 1:1 four step encoders, wird noch laut Rastung passend geschiftet
2
  {
3
    case 0b111000: returnValue =  1; break;
4
    case 0b100001: returnValue =  1; break;
5
    case 0b000111: returnValue =  1; break;
6
    case 0b011110: returnValue =  1; break;
7
    case 0b110100: returnValue = -1; break;
8
    case 0b010010: returnValue = -1; break;
9
    case 0b001011: returnValue = -1; break;
10
    case 0b101101: returnValue = -1; break;
11
    default: break;
12
  }

Das geht übersichtlicher:
1
  switch (digitHist) // 1:1 four step encoders, wird noch laut Rastung passend geschiftet
2
  {
3
    case 0b111000: 
4
    case 0b100001: 
5
    case 0b000111: 
6
    case 0b011110: 
7
      returnValue =  1; 
8
      break;
9
    case 0b110100: 
10
    case 0b010010: 
11
    case 0b001011: 
12
    case 0b101101: 
13
      returnValue = -1; 
14
      break;
15
  }

von Veit D. (devil-elec)


Lesenswert?

Hallo,

@ Uwe:

denke bitte daran das dein enc_delta volatile sein muss.
Und diese Zeile fiel mir noch auf.
> TCCR0B = 1<<CS01^1<<CS00;
Das sollte
1
 
2
TCCR0B = (1<<CS01) | (1<<CS00);  
3
oder
4
TCCR0B = _BV(CS01) | _BV(CS00);
lauten.

Weil ich gerade bei Kritik bin. Habe bei mir auch was wichtiges 
vergessen.
Nach dem
const uint8_t saveSREG {SREG};
muss natürlich
cli() aufgerufen werden, sonst ist das sinnlos. :-)

> P.S.: Meine erste Erfahrung mit Arduino. Ich benutze sonst C mit
> Atmel-Studio (nicht C++). Das ist nicht meins...

Ist kein Problem. Solange wir verstehen was der andere sagen möchte ist 
alles i.O., egal ob C, C++ oder andere Sprachen.

Danke dir für den Encoder Code und Erklärungen, auch Danke an 
LostInMusic für die Hinweise. Ich werde das weiter verfolgen und 
umsetzen.

von Uwe K. (ukhl)


Lesenswert?

> @ Uwe:
> denke bitte daran das dein enc_delta volatile sein muss.
> Und diese Zeile fiel mir noch auf.
>> TCCR0B = 1<<CS01^1<<CS00;
> Das sollte
>
1
> TCCR0B = (1<<CS01) | (1<<CS00);
2
> oder
3
> TCCR0B = _BV(CS01) | _BV(CS00);
4
>
> lauten.

Die Intension mich an diese Diskussion anzuhängen, war es meinen 
Algorithmus in eine bekannte Umgebung zu bringen. Damit wird die 
Funktion vergleichbar.

Meinen Teil habe ich dort nochmal separat angehängt.
Beitrag "Re: Drehgeber/Encoder 1-, 2- oder 4-schrittig"

Beide Anmerkungen sind nicht Teil meines Algorithmus, sondern eine Kopie 
des Originals. Aber deine Hinweise sind zu empfehlen. Wahrscheinlich 
wird es auch ohne die Anpassung problemlos funktionieren.

Es geht mir nicht darum, den Code von Peter zu optimieren. Sondern 
meinen Algorithmus vorzustellen.

: Bearbeitet durch User
von Veit D. (devil-elec)


Lesenswert?

Hallo,

okay
TCCR0B = 1<<CS01^1<<CS00;
ist nur eine ungewöhnliche Schreibweise zum darstellen wie man die 
Wertigkeiten der Bits miteinander verknüpft. Schreibt kaum jemand so.

Aber das enc_delta volatile sein muss sollte jeden klar sein. Solche 
Fehler sind absolut schwer zu finden, weil sie sporadisch auftreten.
Hier ist es von Peter richtig. Diese Doku ist das eigentliche Original.
https://www.mikrocontroller.net/articles/Drehgeber
In dem verlinkten Thread sicherlich nur ein Flüchtigkeitsfehler.

von DerEinzigeBernd (Gast)


Lesenswert?

Veit D. schrieb:
> ist nur eine ungewöhnliche Schreibweise

So kann man das auch nennen. ^ ist EXOR, das hier anstelle eines OR zu 
verwende, funktioniert zwar, ist aber wirklich ... sehr schräg.

von Apollo M. (Firma: @home) (majortom)


Lesenswert?

DerEinzigeBernd schrieb:
> Das geht übersichtlicher:
>   switch (digitHist) // 1:1 four step encoders, wird noch laut Rastung
> passend geschiftet
>   {
>     case 0b111000:
>     case 0b100001:
>     case 0b000111:
>     case 0b011110:
>       returnValue =  1;
>       break;
>     case 0b110100:
>     case 0b010010:
>     case 0b001011:
>     case 0b101101:
>       returnValue = -1;
>       break;
>   }

one step further towards simplification :-)

switch (digitHist) {
   case 0b111000: case 0b100001:
   case 0b000111: case 0b011110:
   return 1;
   case 0b110100: case 0b010010:
   case 0b001011: case 0b101101:
   return -1;
}

von Uwe K. (ukhl)


Lesenswert?

Reine Kosmetik. Dein erster Vorschlag, in dem man alles "CASE" 
untereinander scheibt ist leichter zu lesen. Aber stimmt, ein BREAK 
braucht es nicht, wenn man direkt RETUNed.

Und die Variable returnValue finde ich nicht gut. Man braucht sie nicht. 
Ist in meinen Code auch nicht enthalten.

Ich bleibe auch dabei, dass es die 1:4 Abfrage ist. Schließlich werden 4 
Änderungen gezählt für ein Zyklus.

Schon alles richtig so:
Beitrag "Re: Drehgeber/Encoder 1-, 2- oder 4-schrittig"

Einfach einbinden und Finger weg.

: Bearbeitet durch User
von LostInMusic (Gast)


Lesenswert?

Warum werden eigentlich die Pegel der Leitungen A und B vom Encoder
nicht einfach gleich in eine Variable "EncPosition" verrechnet?
Also ohne "EncDelta" und immer sofort, nachdem auf A oder B irgendeine
Änderung festgestellt wurde. EncPosition enthält dann schlicht jederzeit 
die korrekte Position des Encoders. Handelt man sich damit irgendein 
Problem
ein? Kann mich mal jemand darüber aufklären, wozu der Umweg über das 
EncDelta gut sein soll?

von Carsten W. (eagle38106)


Lesenswert?

Die Kontakte des Encoders prellen - wie jeder Schaltkontakt. Das willst 
Du nicht in dem Zähler haben.

von Uwe K. (ukhl)


Lesenswert?

LostInMusic schrieb:
> Warum werden eigentlich die Pegel der Leitungen A und B vom Encoder
> nicht einfach gleich in eine Variable "EncPosition" verrechnet?

EncPosition ist eine absolute Position. Die Position ist sehr abhängig 
von der Anwendung. Wenn es z.B. 10 Menu-Eintrage, gibt, ist 10 die 
höchste Zahl. Es darf nicht weiter zählen. Deshalb ist es besser die 
Position vom individuellen Programm zu setzen. Es geht auch die 
Information der Drehrichtung verloren. Das Problem ist gerade aktuell:

Beitrag "STM32 Encoder als +/- Wert auswerten"

EncDelta ist eine relative Position. Dort sind alle Änderungen des 
Drehgebers seit der letzten Abfrage addiert. Dadurch ist man nicht 
gezwungen jede Bewegung sofort zu verarbeiten, da diese im EncDelta 
summiert wird. Die Information der Drehrichtung bleibt erhalten. Um 
Limits im unteren und oberen Bereich muss man sich noch nicht kümmern. 
Manchmal benötigt man den absoluten Wert nicht.

EncDelta ist viel flexibler zu verarbeiten.

von LostInMusic (Gast)


Lesenswert?

>Die Kontakte des Encoders prellen - wie jeder Schaltkontakt.
>Das willst Du nicht in dem Zähler haben.

Ach so. Was da aber eigentlich passiert, ist eine simple Unterabtastung, 
und die ist über das "EncDelta" nur unnötig kompliziert programmiert. 
Könnte man ja genausogut erreichen, indem man gleich EncPosition 
berechnet und den Wert zu Beginn der Main einfach cacht, also mit "=" in 
eine zweite Positionsvariable kopiert.

von LostInMusic (Gast)


Lesenswert?

>EncDelta ist viel flexibler zu verarbeiten.

OK, ist ein Argument. Man kann tatsächlich fragen, wozu man die 
Absolutposition des Gebers berechnen soll, wenn die nirgendwo benötigt 
wird.

Allerdings: Mit Entprellen hat das nichts zu tun. Wenn der Geber um 1 
wackelt (mechanischer Kontakt prellt oder Maschine mit optischem Geber 
vibriert), dann wackelt auch jedes EncPosition, das man durch eine 
Unterabtastung daraus gewinnt, um 1, nur weniger oft.

Danke fürs Feedback.

von Uwe K. (ukhl)


Lesenswert?

> Allerdings: Mit Entprellen hat das nichts zu tun.
So ist es. Rein gar überhaupt nichts.

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.