Forum: Mikrocontroller und Digitale Elektronik Encoder/Drehgeber zu langsam


von Martin G. (hb9tzw)


Lesenswert?

Ich habe den Encoder-Code von Peter Dannegger von hier:

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

ausprobiert und der funktioniert super, nur muss man (bei meiner 
Hardware) am Encoder nicht besonders schnell drehen (von Hand) bis 
Schritte verloren gehen bzw. nichts mehr ausgewertet wird bis man wieder 
langsamer dreht. Der Code in der Hauptschlaufe wo der Encoder aufgerufen 
wird sieht wie folgt aus:
1
signed char temp = encode_read2();
2
if(temp == 1) {
3
   // Value is increasing
4
   FREQ -= FREQSTEP;
5
} else if(temp == -1) {
6
   // Value is decreasing
7
   FREQ += FREQSTEP;
8
}
9
SetFont(FIX_FONT12X16);
10
LCDSoftText(dtostrf(FREQ,2,4,string),0,15);

Ich habe schon versucht, den Interrupt häufiger auftreten zu lassen als 
im Originalcode von Peter, aber ohne merkbaren Unterschied.

Sieht hier jemand Verbesserungspotential? Ich bin nicht einmal sicher, 
wo der Flaschenhals genau ist...

Grüsse
Martin

von DanVet (Gast)


Lesenswert?

Kannst du mal die komplette Funktion posten, es fehlen die 
Funktionsköpfe um den Zusammenhang zu erkennen.
falls du
SetFont(FIX_FONT12X16);
LCDSoftText(dtostrf(FREQ,2,4,string),0,15);
im Interrupt machst, dann hast du ein Problem.
Im Interrupt bitte nur zählen, sonst nix.
Falls du den Encoder pollst, dann ist da der Flaschenhals.
Hilft dir das weiter?

von docean (Gast)


Lesenswert?

wie lange brauch dein LCD Kram?

kannst du mal rausnnehmen und den wert über uart und/oder leds ausgeben?

von DanVet (Gast)


Lesenswert?

Hab mal kurz das Beispiel angeschaut. Da läuft ja ein Timer... whuaaahh, 
der den Encoder abfrägt.
Sinnigerweise setzt man einen Interrupt auf die Flanken der 
Encodersignale und wertet im Interrupt die Signale aus.

Was hast du denn für einen Encoder, wie oft kommen die Pulse?

von DanVet (Gast)


Angehängte Dateien:

Lesenswert?

Ich nochmal, die Encoder-Schaltung sieht bei mir so aus, wie im Anhang.

von Fabian B. (fabs)


Lesenswert?

Interrupt auf die Flanken des Encoders macht nur Sinn wenn dieser nicht 
prellt...sonst haste einen Haufen unnützer Interrupts.

Die Polling-Methode von Peda ist schon echt gut, und mit 1kHz 
Interrupt-Frequenz hatte ich dann auch keine verlorenen Pulse mehr 
(zumindest nicht bei schneller Handdrehung).

Wichtig ist halt nur die Zählung im Int zu machen, alles andere in der 
Hauptschleife... und LCDs können da die entscheidende Bremse sein.

Gruß
Fabian

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

>>>  in der Hauptschlaufe wo der Encoder aufgerufen wird
Dort ist die Encoder-Routine am flaschen Platz. Die gehört in den 
Timer-Interrupt. Das steht aber in dem angesprochenen Artikel... :-/

>> Da läuft ja ein Timer... whuaaahh, der den Encoder abfrägt.
Das funktioniert völlig problemlos, ich habe den code noch ein wenig 
erweitert, dass bei schnellem Drehen am Knopf der Wert sich schneller 
ändert (dynamische Geschwindigkeitsanpassung). Das fühlt sich dann super 
an... ;-)

> Ich nochmal, die Encoder-Schaltung sieht bei mir so aus, wie im Anhang.
Meines Erachtens unnötig viel Aufwand...

von Olaf (Gast)


Lesenswert?

> Sinnigerweise setzt man einen Interrupt auf die Flanken der
> Encodersignale und wertet im Interrupt die Signale aus.

Nein, das machen nur die Leute die das Prinzip nicht verstanden haben.
Interrupt ist nicht nur ungenauer, es verursacht eine nicht 
vorhersehbare Systemlast. Das will man vermeiden.


> Wichtig ist halt nur die Zählung im Int zu machen, alles andere in der
> Hauptschleife...

Yep. Ich habe so Motor mit Encoder laufen die selbst in monatelangen 
Dauerbetrieb keine Position verlieren.

> und LCDs können da die entscheidende Bremse sein.

Garantiert.

von Daniel V. (danvet)


Lesenswert?

Olaf schrieb:
> Nein, das machen nur die Leute die das Prinzip nicht verstanden haben.
> Interrupt ist nicht nur ungenauer, es verursacht eine nicht
> vorhersehbare Systemlast. Das will man vermeiden.

Das kann ich jetzt nicht nachvollziehen. Der Interrupt kommt doch nur, 
wenn der Encoder sich ändert und stellt sicher, dass ich alle Pulse 
mitbekomme. OK, die Entprellungsgeschichte muss man berücksichtigen, 
aber was nützt mir eine vorhersehbare Systemlast, wenn dann nicht mehr 
gezählt wird.

von Horst (Gast)


Lesenswert?

Ein incrementaler Encoder arbeitet meist in der Praxis erst dann 
zufriedenstellend, wenn an einem Eingang der Zustand auch "flackern" 
darf, ohne dass die Auswertung dabei durcheinander kommt. Mechanisch 
läßt sich das "Flackern" nämlich nicht vermeiden, einzig die Auswertung 
muß es beherrschen. Reine Interrupt-Lösungen, ohne eine geeignete 
Hardware davor, können dieses Problem nicht lösen. Als Folge wird der 
Zähler ungenau, oder er läuft plötzlich in eine Richtung los.

von Karl H. (kbuchegg)


Lesenswert?

Daniel V. schrieb:

> wenn der Encoder sich ändert und stellt sicher, dass ich alle Pulse
> mitbekomme. OK, die Entprellungsgeschichte muss man berücksichtigen,

Wie machst du denn das?
Wie berücksichtigst du denn die Entprellerei?

Schau dir doch mal die ganzen 'Entprell-Lösungen' an. Meistens bedeutet 
das: Im Interrupt ist ein delay, der das System ein paar Millisekunden 
blockiert, dann werden die Interrupt-Flags zurückgesetzt, der Puls 
gezählt und die ISR ist beendet.

Dreht dein Benutzer heftig am Rad und prellt der Encoder im vertretbaren 
Rahmen, dann befindet sich der µC praktisch nur noch im delay, den du 
brauchst um den Eingang zu entprellen. Dementsprechend steht dein 
restliches Programm still. Nichts geht mehr.

Baust du dein Programm hingegen so auf, dass der µC alle paar 
Zig-Nano-Sekunden mal kurz einen Blick auf die Eingänge wirft, muss 
nicht gewartet werden. Bei einem normalen Encoder kann dein Benutzer gar 
nicht so schnell kurbeln, als das dir das bei dem kurzen 
Nachseh-Intervall entgehen würde. Baust du ihm auch noch eine dynamische 
Geschwindigkeitsanpassung ein, dann will er auch gar nicht mehr schnell 
kurbeln.

Praktisch alles, was mit Benutzerinteraktion zu tun hat, ist aus Sicht 
eines µC eine extreme Zeitlupe. Es reicht völlig aus, ab und an wieder 
mal nachzusehen, ob der Benutzer irgendetwas gemacht hat. Wenn du wissen 
willst, ob du deinen Rasen mähen musst, schaust du ja auch nur 1 mal am 
Tag nach und bleibst nicht Rasenmäher bei Fuss neben der Grünfläche 
stehen um nur ja den Zeitpunkt nicht zu verpassen.

von Horst (Gast)


Lesenswert?

> Bei einem normalen Encoder kann dein Benutzer gar
> nicht so schnell kurbeln, als das dir das bei dem kurzen
> Nachseh-Intervall entgehen würde.

Das passiert leider doch, wenn der Encoder genau an der Stelle eines 
Flankenwechsels zum stehen kommt. Genau dann "flackert" ein Signal und 
würde Deinen Interrupt hoffnungslos überfordern. Wird dann vor dem 
Weiterdrehen der letzte Flankenwechsel nicht erkannt, dann macht Dein 
Zähler einen Fehler. - So geht es also nicht vernümftig.

von Martin G. (hb9tzw)


Lesenswert?

Ok das gab ja ganz schön viele Antworten. Will mal versuchen so gut wie 
möglich darauf einzugehen. Zuerst mal meine Interrupt-Routine:
1
ISR(TIMER0_COMP_vect) {          // 1ms for manual movement
2
3
  // encoder
4
  int8_t new, diff;
5
 
6
  new = 0;
7
  if(PHASE_A)
8
    new = 3;
9
  if(PHASE_B)
10
    new ^= 1;              // convert gray to binary
11
  diff = last - new;          // difference last - new
12
  if(diff & 1) {            // bit 0 = value (1)
13
    last = new;              // store new as next last
14
    enc_delta += (diff & 2) - 1;    // bit 1 = direction (+/-)
15
  }
16
17
  // taster
18
  get_taster(0, PIND & (1<<PD5));    // encoder-switch
19
  get_taster(1, PIND & (1<<PD0));    // sw1
20
  get_taster(2, PIND & (1<<PD1));    // sw2
21
  get_taster(3, PIND & (1<<PD2));    // sw3
22
  get_taster(4, PIND & (1<<PD3));    // sw4
23
}

die so initialisiert wird:
1
void encode_init(void) {
2
3
  int8_t new;
4
 
5
  new = 0;
6
  if(PHASE_A)
7
    new = 3;
8
  if(PHASE_B)
9
    new ^= 1;              // convert gray to binary
10
  last = new;              // power on state
11
  enc_delta = 0;
12
  TCCR0 = 1<<WGM01^1<<CS01^1<<CS00;    // CTC, XTAL / 64
13
  OCR0 = (uint8_t)(XTAL / 64.0 * 1e-3 - 0.5);  // 1ms
14
  TIMSK |= 1<<OCIE0;
15
}

Die Tasterabfrage ist nicht von Peda aber der Rest ist 1:1 übernommen.

Der Code, den ich im ersten Beitrag gepostet habe, also der Aufruf von 
encode_read2() und dessen Auswertung, steht im main() so wie Peda es 
auch im Beispiel gemacht hat. Das sieht dann komplett so aus:
1
int main(void) {
2
3
  // *********************************************
4
  // setup ports
5
  // *********************************************
6
  // PORTA CONFIG
7
  // A3 PTT        IN 
8
  DDRA =  0b00000000;                          // port a output 1=output
9
  PORTA = 0b00001000;                          // port a pullups 1=active
10
11
  // PORTB CONFIG
12
  // B5 ENC_B      IN 
13
  // B4 ENC_A      IN 
14
  // B3 MISO       IN
15
  // B2 MOSI       OUT
16
  // B1 SCK        OUT
17
  DDRB =  0b00000110;                          // port b output 1=output
18
  PORTB = 0b00110000;                          // port b pullups 1=active
19
  
20
  // PORTC CONFIG
21
  // C7 DC_DISP    OUT
22
  // C6 RES_DISP   OUT
23
  // C5 CS_DISP    OUT 
24
  DDRC =  0b11100000;                        // port c output 1=output
25
  PORTC = 0b00000000;                        // port c pullups 1=active
26
27
  // PORTD CONFIG
28
  // D7 CE_PLL     OUT
29
  // D6 LE_PLL     OUT
30
  // D5 ENCSW      IN
31
  // D3 SW4        IN
32
  // D2 SW3        IN
33
  // D1 SW2        IN
34
  // D0 SW1        IN
35
  DDRD =  0b11000000;                                       // port d output 1=output
36
  PORTD = 0b00101111;                        // port d pullups 1=active
37
  
38
  // PORTE CONFIG
39
  // CtcssPort = all out
40
  DDRE =  0b11111111;                        // port e output 1=output
41
  PORTE = 0b00000000;                        // port e pullups 1=active
42
43
  // PORTF CONFIG
44
  // F7 JTAG_TDI   IN
45
  // F6 JTAG_TDO   IN
46
  // F5 JTAG_TMS   IN
47
  // F4 JTAG_TCK   IN
48
  // F0 RSSI       IN
49
  DDRF =  0b00000000;                        // port f output 1=output
50
  PORTF = 0b11110000;                        // port f pullups 1=active
51
  // *********************************************
52
  // end setup ports
53
  // *********************************************
54
55
  // *********************************************
56
  // setup spi
57
  // *********************************************
58
  // SPI CONFIG
59
  // Bit7: SPIE -> Enables SPI interrupt
60
  // Bit6: SPE  -> setting it enables SPI  
61
  // Bit5: DORD -> if bit is set first comes LSB, if cleared MSB
62
  // Bit4: MSTR -> Master/Slave select, 1 = master
63
  // Bit3: CPOL -> clock polarity (refer to datasheet)
64
  // Bit2: CPHA
65
  // Bit1: SPR1 ->
66
  // Bit0: SPR0 -> Clock rate selection (refer to table in datasheet)
67
  // Enable SPI, Master, set clock rate fck/16
68
  SPCR = 0;
69
  SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR1)|(1<<SPR0);
70
  SPSR = (0<<SPI2X);
71
  // *********************************************
72
  // end setup spi
73
  // *********************************************
74
75
  // *********************************************
76
  // various inits
77
  // *********************************************
78
  //float display
79
  char string[7];
80
  // encoder
81
  encode_init();
82
  sei();
83
  // glcd
84
  GLCD_Init();
85
  GLCD_ClearScreen();
86
  SetFont(ARIAL_14_BOLD);
87
  LCDSoftText("HELLO WORLD!",0,0);
88
  // Taster konfigurieren (#define NUM_TASTER 3 in taster.h)
89
  tasten[0].mode = TM_SHORT;
90
  tasten[1].mode = TM_SHORT;
91
  tasten[2].mode = TM_SHORT;
92
  tasten[3].mode = TM_SHORT;
93
  tasten[4].mode = TM_SHORT;
94
  // *********************************************
95
  // end various inits
96
  // *********************************************
97
  
98
  while(1) {  
99
100
    // encoder
101
    signed char temp = encode_read2();
102
    if(temp == 1) {
103
       // Value is increasing
104
       FREQ -= FREQSTEP;
105
    } else if(temp == -1) {
106
       // Value is decreasing
107
       FREQ += FREQSTEP;
108
    }
109
    // end encoder
110
111
    // taster
112
    signed char tast = taster;
113
    switch(tast) {
114
       default:
115
       case NO_TASTER:
116
          break;
117
       case 0:
118
          /* Taster 0 */
119
          break;
120
       case 1:
121
          /* Taster 1 kurz gedrueckt */
122
          break;
123
       case 2:
124
          /* Taster 2 */
125
          break;
126
       case 3:
127
          /* Taster 3 */
128
          break;
129
       case 4:
130
          /* Taster 4 */
131
          break;
132
    }
133
    if(tast != NO_TASTER)
134
       taster = NO_TASTER;
135
    // end taster
136
137
    SetFont(FIX_FONT12X16);
138
    LCDSoftText(dtostrf(FREQ,2,4,string),0,15);
139
  }
140
}

Eine andere Möglichkeit der Ausgabe sehe ich nicht, es handelt sich 
nicht um einen Versuchsaufbau auf dem Steckbrett, wo einfach etwas 
geändert werden kann. Ausserdem gehört das Display zur Schaltung, 
letztendlich kann darauf auch nicht verzichtet werden. Dass das 
LCD-Schreiben etwas zeitaufwändig ist, besonders da noch ein Float 
geschrieben wird, wird schon so sein. Aber ich kenne keine Möglichkeit 
das anders zu lösen...

Einen externen Interrupt wollte ich eben nicht verwenden, weil ich nicht 
darauf angewiesen sein wollte, den Encoder unbedingt an einem 
Interrupt-Pin anzuschliessen.

Ich denke die Bedenken die ihr hattet wegen der Encoderroutine im 
Interrupt sind unbegründet, oder habe ich euch falsch verstanden?

Grüsse
Martin

von Karl H. (kbuchegg)


Lesenswert?

Martin Geissmann schrieb:

> Ich denke die Bedenken die ihr hattet wegen der Encoderroutine im
> Interrupt sind unbegründet, oder habe ich euch falsch verstanden?

Nein, das passt schon so.

XTAL hast du auf den richtigen Wert gesetzt?
Welche Prozessorfrequenz hast du eigentlich?

von Guido S. (flintstone)


Lesenswert?

Hallo!

Ich habe das Problem gelöst, indem ich das Rundensignal, dass mein 
Incrementalgeber ausgibt, genutzt habe. Es Synchronisiert die Impulse 
und setzt den Wert auf ein Vielfaches der Impulse je Umdrehung, die der 
Geber sonst ausgibt. Somit werden nur die Fehler, die in der letzten 
Runde auftraten, wirksam. Natürlich geht das ganze vorwärts und 
rückwärts. Ich habe übrigens auch die Möglichkeit der Interrupts für 
diese Aufgabe benutzt.
Das Programm ist in BASCOM geschrieben und erfüllt seinen Zweck als 
PID-Regler hervorragend.

Gruß
Guido

von Karl H. (kbuchegg)


Lesenswert?

1
   signed char temp = encode_read2();
2
    if(temp == 1) {
3
       // Value is increasing
4
       FREQ -= FREQSTEP;
5
    } else if(temp == -1) {
6
       // Value is decreasing
7
       FREQ += FREQSTEP;
8
    }

Was ist, wenn temp nicht +1 oder -1 ist, sondern höher? Die PeDa 
Encoder-Funktionen führen auch darüber Buch, wenn du nicht rechtzeitig 
zur Abfrage kommst (weil der µC zb mit lCD Ausgabe zu lange beschäftigt 
war) und der Benutzer in der Zwischenzeit 2 oder mehr Rastungen gedreht 
hat.

Also besser so
1
   int8_t temp = encode_read2();
2
   if( temp != 0 )
3
     FREQ -= temp * FREQSTEP;

von Petra Dannegger (Gast)


Lesenswert?

Martin Geissmann schrieb:
> signed char temp = encode_read2();
>     if(temp == 1) {
>        // Value is increasing
>        FREQ -= FREQSTEP;
>     } else if(temp == -1) {
>        // Value is decreasing
>        FREQ += FREQSTEP;
>     }

Du solltes bercksichtigen, dass temp auch andere Werte als -1, 0, 1 
annehmen kann.

von Karl H. (kbuchegg)


Lesenswert?

PS
So was
1
    if(tast != NO_TASTER)
2
       taster = NO_TASTER;
kannst du dir sparen. Auch Abfragen sind nicht kostenlos!
Weis einfach den Wert zu, den taster auf jeden Fall haben soll und gut 
ists.

Und versuche dich an übliche C Konventionen zu halten.
Namen ausschliesslich in Grossbuchstaben sind immer Makros und 
umgekehrt: Makros haben immer einen Namen ausschliesslich in 
Grossbuchstaben.
Eine Variable namens FREQ geht gar nicht.
Das erleichtert die Unterscheidung was ein Makro ist und was nicht. Und 
das wiederrum kann manchmal den Unterschied zwischen 'funktioniert 
problemlos' und 'Ich such jetzt seit 5 Stunden einen Fehler und finde 
ihn nicht' ausmachen.

von Peter D. (peda)


Lesenswert?

Martin Geissmann schrieb:
1
> ISR(TIMER0_COMP_vect) {          // 1ms for manual movement
2
> ...
3
>   get_taster(0, PIND & (1<<PD5));    // encoder-switch
4
>   get_taster(1, PIND & (1<<PD0));    // sw1
5
>   get_taster(2, PIND & (1<<PD1));    // sw2
6
>   get_taster(3, PIND & (1<<PD2));    // sw3
7
>   get_taster(4, PIND & (1<<PD3));    // sw4
8
> }

Wie sieht denn "get_taster" aus?

Hoffentlich nicht mit riesigen Delays.


Peter

von Micha (Gast)


Lesenswert?

Lothar Miller schrieb:
> ich habe den code noch ein wenig
> erweitert, dass bei schnellem Drehen am Knopf der Wert sich schneller
> ändert (dynamische Geschwindigkeitsanpassung)

Wäre das nicht eine schöne Erweiterung für den Artikel?

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

> Wäre das nicht eine schöne Erweiterung für den Artikel?
Irgendwo kursiert das da noch rum.... such, grübel, gruschtel, hmmm...
Ah, hier im Beitrag "Re: Dynamische "Beschleunigung" bei Encoder-Eingabe"

Interessant auch der letzte Satz zum Thema "Pin-Change-Interrupt" von 
Peter Dannegger im Beitrag "Re: Dynamische "Beschleunigung" bei Encoder-Eingabe"

von Martin G. (hb9tzw)


Lesenswert?

Die Prozessorfrequenz ist 8MHz. Das ist normalerweise meine Frequenz 
weil ich noch etwa 500 8MHz-Quarze habe...

@Karl Heinz: Das wird wohl das Problem bzw. die Lösung sein, da habe ich 
offenbar wirklich etwas nicht richtig verstanden. Nun habe ich die 
Auswertung
1
if(temp == 1) {
2
   // Value is increasing
3
   FREQ -= FREQSTEP;
4
} else if(temp == -1) {
5
   // Value is decreasing
6
   FREQ += FREQSTEP;
7
}

durch
1
FREQ -= temp*FREQSTEP;

ersetzt. Viel einfacher und tut auch wirklich schon viel besser! Ist das 
was du gemeint hast?

Grüsse
Martin

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.