Amateurfunk verbindet die Welt

Drehencoder mit BASCOM

Erstellt: DL6GL, 28.12.2011, letzte Änderung 

Drehencoder sind häufig verwendete Bauteile zur Bedienung von Microcontrollern, wenn es darum geht, analoge Bedienungsvorgänge dem Controller in digitaler Form anzubieten, z.B. die Frequenzeinstellung eines VFO. Der hier zunächst verwendete Encoder ist ein ALPS STEC11B13 (reichelt.de). Um die Sache rund zu machen, wurden weitere Encoder von pollin.de getestet, der ALPS EC11E15244B2 (nicht mehr im Programm), der PANASONIC EVEQDBRL416B und der Noble RE0124PVB17.7 (auch nicht mehr im Programm), dazu noch der ALPS STEC12E08 von reichelt.de.

BASCOM kennt den Befehl "Encoder" - nur besonders zuverlässig ist diese Funktion nicht.

Nach langem Suchen im Web und Testen hat sich folgende Variante ergeben, die offenbar mit verschiedensten Encodern zurechtkommt. Die Idee stammt von Tom Baer [1]. Man kann wirklich schnell am Encoder drehen, ohne dass sich der Controller verschluckt.

Die anscheinend naheliegende Lösung, einen externen Interrupt INT0 oder INT1 über die steigen­de oder fallende Impulsflanke eines Encoderschalters, z.B. A, zu erzeugen und den Zustand des anderen Schalters abzufragen, funktionierte nur "hakelig". Nicht jede Drehrastung wurde als solche erkannt. In [2] ist erklärt, warum diese Methode nicht zuverlässig funktioniert, Code-Beispiele in C.

Die hier gezeigte Lösung verwendet interne Interrupts über den Timer0. Der Encoder wird also mit jedem Timer0-Interrupt periodisch abgefragt (Polling).

Encoder und Controller Anschluss des Encoders an den Prozessor, z.B. an die Ports D3 und D4. Bei beiden Ports ist das Pullup aktiviert. Die beiden Kondensatoren, ursprünglich zum Entprellen der Encoderkontakte gedacht, können entfallen. Das unten gezeigte Programm braucht sie nicht. Ich verwende sie dennoch immer...

Der verwendete Encoder ALPS STEC11B13 schaltet mit drei Zwischenstellungen zwischen zwei Rastungen, aus denen sich die Drehrichtung ableiten lässt:

Rechtsdrehung ALPS STEC11B13 Linksdrehung
A B   A B
1 1 Raststellung 1 1
1 0 Zwischenstellung 1 0 1
0 0 Zwischenstellung 2 0 0
0 1 Zwischenstellung 3 1 0
1 1 Raststellung 1 1

Das Schaltverhalten wurde mit einem zweikanaligen Scope aufgenommen. Anschlüsse A und B über je 10k an +5V, Anschluss C an Masse. Bei behutsamem Drehen findet man die Zwischenstellungen zwischen zwei Rastungen. Vor der Verwendung anderer Encoder sollte man mit einer solchen Messung das Schaltverhalten prüfen und ggf. die Binärwerte für den Gray-Code in der Interrupt Service-Routine anpassen.

So geschehen mit dem im Si570-LO (an anderer Stelle auf dieser Website) eingesetzten Encoder. Pollin.de hatte ihn vor längerer Zeit im Angebot. Wenn man dem damals verfügbaren Datenblatt glaubt, handelt es sich um den ALPS EC11E15244B2. Dieser verhält sich ganz anders als der oben gezeigte ALPS STEC11B13.

Rechtsdrehung ALPS EC11E15244B2 Linksdrehung
AB
AB
1 1 Raststellung 1 1
0 1 Zwischenstellung 1 1 0
0 0 Zwischenstellung 2 0 0
1 0 Zwischenstellung 3 0 1
1 1 Zwischenstellung 4 1 1
0 1 Zwischenstellung 5 1 0
0 0 Raststellung 0 0

Dieser Encoder hat fünf Zwischenstellungen zwischen zwei Rastungen, zudem wechseln die Rastungen zwischen (0, 0) und (1, 1). Ein OM, der den Si570-LO mit dem Reichelt-Encoder nachgebaut hat, beklagte sich über das Schaltverhalten. Aus dem Vergleich des Schaltverhaltens beider Encoder kam dann doch Licht in die ganze Sache.

Mit den identischen Werten bei den Raststellungen (A=B=1) und der Zwischenstellung 2 (A=B=0) beim ALPS STEC11B13 als Bezugspunkte könnte man je Drehung zwischen zwei Rastungen  zweimal die Drehrichtung bestimmen, wenn währenddessen mindestens zwei Interrupts erzeugt werden. Tests mit dem Programm ergaben, dass dies bei langsamem Drehen möglich ist. Soll zwischen zwei Rastungen immer eine Änderung um den gleichen Betrag erfolgen, könnte dieses Verhalten eher verwirren. Bei schnellem Drehen wird die vorgegebene Änderung erzielt (1 Tick), bei langsamem Drehen zuerst bei der Zwischenstellung 2, dann noch einmal bei der Folgerastung (2 Ticks). Um das Verhalten einzustellen, gibt es eine Konstante "DoubleStep", die per Const-Anweisung auf "0" (1 Tick) oder auf "1" (2 Ticks) gesetzt werden kann.

Versuche mit den fünf Encodern ergaben:

Encoder-Typ DoubleStep Drehrichtung
ALPS STEC11B13 (reichelt.de) 0 1 Rechts
ALPS STEC12E08 (reichelt.de) 0 1 Links
ALPS EC11E15244B2 (pollin.de)   1 Links
Noble RE0124PVB17.7 (pollin.de) 0 1 Links
PANASONIC EVEQDBRL416B (pollin.de)   1 Links

Wo "DoubleStep" mit 0 und 1 angegeben ist, funktionieren die Encoder mit beiden Einstellungen im Testprogramm. Nach Rückmeldungen einiger OMs funktioniert der ALPS STEC11B13 doch besser mit DoubleStep = 0. Am besten ausprobieren, insbesondere, ob sich bei langsamem Drehen zusätzliche Ticks zwischen zwei Rastungen ergeben.
Drehrichtung "Rechts" bedeutet "Increment" aus der Routine "Timer0_isr" (s.u.) bei Drehung im Uhrzeigersinn. Drehrichtung "Links" entsprechend anders herum, hier Anschlüsse A und B vertauschen.

Ein liebevoll ausführlicher Artikel zum (recht schwergängigen) PANASONIC EVEQDBRL416B von pollin.de, ist in [3] zu finden.

Konfigurieren der Ports, gesamter Port B sei ein Input-Port

DDRB = &B00000000                            'PortB = input
PORTB = &B11111111 'Activate Pullup
Enc_A Alias PINB.0 'Encoder A
Enc_B Alias PINB.1 'Encoder B
Enc_K Alias PINB.2 'Encoder key

Damit liegen u.a. die Ports B0 und B1 mit Hilfe der prozessorinternen Pullups im Ruhezustand auf logisch 1 (+5V). Die Schalter im Encoder ziehen je nach Drehung die Pegel auf logisch 0 im Gray-Code. Zwischen zwei Rastungen ändert sich bei den Zwischenstellungen immer nur ein Bit.

Konfigurieren eines Timers für die Interrupts (periodisches Polling des Encoders)

'Configure Timer0 interrupt for encoder, Timer0 = 8 bit (2^8=256 counts)
'Overflow time: overflow counts * prescale / crystal frequency
'= 256 * 256 / 16.000.000 Hz = 4,1 msec = ~ 244 Hz (crystal 16 MHz)
'Here prescale = 256, possible prescales: 8, 64, 256, 1024
'The encoder is polled every 4,1 msec. To shorten time set PresetTimer0 > 0

Config Timer0 = Timer , Prescale = 256
Const PresetTimer0 = 0    'Timer preset value to initialize Timer0
On Timer0 Timer0_isr     'Timer0 Interrupt service routine on overflow

Das Timing lässt sich in zwei Stufen einstellen:

  1. Grob mit dem Prescale 8, 64, 256 oder 1024. Das sind die Teilerfaktoren für die vom Quarz vorgegebene Taktfrequenz.
  2. Fein mit dem PresetTimer0.
    Mit PresetTimer0 = 0 zählt der Timer0 von 0 bis 255,
    mit PresetTimer0 = 100 z.B. zählt der Timer0 nur von 100 bis 255, braucht also weniger Zeit bis zum Overflow, hier also
    156 * 256 / 16.000.000 Hz = 2,5 msec = ~ 400 Hz (16 MHz Quarz)

Wenn in der unten gezeigten Timer0_isr noch mehr als der Encoder bedient wird, z.B. auch noch Taster, kann es sein, dass die hier mit 4,1 msec, d.h. etwa 240 Hz, eingestellte Polling-Frequenz zu gering ist. Möglicherweise werden dann Schaltvorgänge nicht korrekt erkannt. Dann hilft es, mit dem PresetTimer0 die Interrupthäufigkeit zu erhöhen, z.B. auf 500 Hz oder höher. Fallweise auch noch schneller mit einem Prescale 64. Polling-Frequenzen im Bereich von 1 kHz sollten reichen. Ein Beispiel (Encoder plus Taster in der Timer0_isr) ist im Si570-VFO des Selbstbau-TRX zu sehen.

Erklärung der Variablen für den Encoder

Dim bytIncr As Byte                         '=2: Encoder incr., =1: decr., =0: ./.
Dim bytEnc_old As Byte 'Old encoder value before change
Dim bytEnc_new As Byte 'New encoder value after change
Const DoubleStep = 0                        '=1: intermediate position detected

Beispiel-Variablen für Frequenzanzeige

Dim lngFreq As Long                         'TRX frequency (Hz)
Dim lngStep As Long      'TRX frequency step (Hz)

Erklärung einer Beispiel-Routine zur Frequenzanzeige in LCD, Zeile bytRow

Declare Sub ShowFreq(ByVal lngF As Long , byVal bytRow As Byte)

Einbinden in das Hauptprogramm

Enable Timer0                               'Interrupt encoder
Enable Interrupts      'enable all interrupts
Do 'Start of Main program
... 'do something

'Processing of Encoder data
Disable Timer0 'Stop Timer0 interrupts
Select Case bytIncr
Case 1
... 'do something
lngFreq = lngFreq - lngStep 'Example: decrease frequency
Case 2
...            'do something
lngFreq = lngFreq + lngStep     'Example: increase frequency
End Select
If bytIncr > 0 Then 'Encoder was changed
Call ShowFreq(lngFreq, 1) 'Show frequency on LCD Row 1
bytIncr = 0 'Reset bytIncr after processing
End If
Enable Timer0 'Restart Timer0
...
Loop 'End of main program

Interrupt Service-Routine für Timer0 (Encoder)

Timer0_isr:
'Encoder interrupt on Timer0 overflow
'While data are displayed Timer0 should be stopped in main program
'Output: bytIncr = 2: increment
' = 1: decrement if encoder was changed
'Having processed the bytIncr data in the main program reset bytIncr = 0
'Switching pattern of ALPS STEC11B13 encoder:
'Turning clockwise              ... counter clockwise
'A    B                             A     B
'1    1  start detent position      1     1
'1    0  intermediate position 1    0     1
'0    0  intermediate position 2    0     0
'0    1  intermediate position 3    1     0
'1    1  next detent position       1     1

   Timer0 = PresetTimer0 'Initialize Timer0
   bytEnc_new.0 = Enc_A                           'Set bit 0 of bytEnc_new
   bytEnc_new.1 = Enc_B                           'Set bit 1 of bytEnc_new

   'Check encoder pattern if encoder is changed
   'bytIncr = 1: decremented (turned counter clockwise)
   'bytIncr = 2: incremented (turned         clockwise)
   If bytEnc_new <> bytEnc_old Then               'encoder is changed
      'from intermediate position 3 to detent position
      If bytEnc_new = &B00000011 And bytEnc_old = &B00000010 Then bytIncr = 2
      If bytEnc_new = &B00000011 And bytEnc_old = &B00000001 Then bytIncr = 1
      'from intermediate position 1 to intermediate position 2
      'DoubleStep = 1:  intermediate position 2 gives an additional tick
      '                 2 ticks between 2 detent positions on slow rotation
      '           = 0:  1 tick  between 2 detent positions
      #If DoubleStep = 1
         If bytEnc_new = &B00000000 And bytEnc_old = &B00000001 Then bytIncr = 2
         If bytEnc_new = &B00000000 And bytEnc_old = &B00000010 Then bytIncr = 1
      #EndIf
      bytEnc_old = bytEnc_new                     'old <- new for the next time
   End If
Return

In der Interrupt-Serviceroutine "Timer0_isr" wird nur ein wenig gerechnet, damit sie möglichst schnell abgehandelt wird. Die langsame Displayanzeige gehört ins Hauptprogramm. Während dieser Zeit muss der Timer0 mit "Disable Timer0" angehalten und nach Ausführung der Anzeige mit "Enable Timer0" wieder gestartet werden, damit nicht weitere Interrupts dazwischenfunken. Je nach Vorbesetzung in "Const DoubleStep" = 0 oder 1 erfolgt eine bedingte Compilierung der mit "#" markierten If-Anweisung.

Will man sich die Option DoubleStep = 0 oder 1 ohne Programmänderung offen halten, lässt sich das mit einem Jumper an einem AVR-Port flexibler bewerkstelligen, z.B. an Port C0:

DDRC.0 = 0                                        'Input PINC.0 (DoubleStep Jumper)
PORTC.0 = 1                                       'Activate Pullup PORTC.0
Pin_DoubleStep Alias PINC.0                       'Encoder DoubleStep Jumper
...
Timer0_isr:
...
If Pin_DoubleStep = 1 Then 'DoubleStep Jumper is out
         If bytEnc_new = &B00000000 And bytEnc_old = &B00000001 Then bytIncr = 2
         If bytEnc_new = &B00000000 And bytEnc_old = &B00000010 Then bytIncr = 1
   EndIf

...

Ist der Jumper gesteckt, gilt DoubleStep = 0, sonst = 1.

Das verwendete Testprogramm findet sich im Download.

Referenzen

[1]  http://www.mikrocontroller.net/topic/112155
[2]  http://www.mikrocontroller.net/articles/Drehgeber
[3]  http://www.rn-wissen.de/index.php/Drehencoder


Download

encoder_100.zip