Amateurfunk verbindet die Welt

Taster-Interruptsteuerung

Erstellt: DL6GL, 31.10.2015, letzte Änderung 

« Taster mit BASCOM
TOP » Taster am ADC

Taster mit externem Interrupt

Anders als im Beispiel mit dem Drehencoder sind externe Interrupts bei Tastern mit wenig Aufwand einzusetzen. Hört sich erst einmal verwegen an, da die gängigen AVR wie der ATmega8 oder ATmega16/32 nur zwei interruptfähige Ports, INT0 und INT1 zur Verfügung stellen. Hier schafft die Kopplung mit Dioden Abhilfe, so dass nur ein Interrupt-Port benötigt wird, im nachfolgenden Testfall INT1. Im Si570-LO in "Selbstbau-TRX" auf dieser Website wurde eine vergleichbare Schaltung verwendet, inzwischen (07.12.2013) aber durch die Dannegger-Methode ersetzt.

Taster mit externem Interrupt

Abb. 1: Taster mit externem Interrupt an Port INT1.

Die Ports liegen über interne Pullups auf logisch H (+5V). Die Betätigung der Taster zieht die Ports nach logisch L (GND). Damit der Interrupt-Port INT1 das mitbekommt und einen Hardware-Interrupt erzeugt, sind alle Taster über Dioden mit ihm verbunden. Das Betätigen eines beliebigen Tasters zieht also neben dem zugeordneten Port auch gleichzeitig INT1 nach logisch Low. Mit dem nunmehr ausgelösten INT1-Interrupt kann abgefragt werden, an welchem Port der Taster gedrückt wurde. INT0 ist natürlich auch verwendbar. Die Taster können auch an anderen Ports liegen.

Als Besonderheit wurde für den Key2-Taster eine Doppelfunktion realisiert: kurz oder lang gedrückt.

Nachfolgend werden die wesentlichen Softwareteile beschrieben. Der komplette Code (Quellen und .hex) ist im Download zu finden.

Erklärung der Variablen und der verwendeten Routinen:

'Variables keys 
Dim bitKey0 As Bit                                '=1 if key0 is pressed
Dim bitKey1 As Bit                                '=1 if key1 is pressed
Dim bitKey2 As Bit                                '=1 if key2 is pressed
Dim bytPressLong As Byte                          '=1: pressed long, =0: short
Const KeyCounter = 100                            '~ 1 sec, pressed long


Declare Sub ClearLCDLine(byVal bytLine As Byte)
Declare Function KeyPressLong(byVal KeyNum As Byte) As Byte

Konfigurieren des Taster-Ports für 3 Taster (die Belegung ist - bis auf INT1 - beliebig):

'Configure PortD (Key port, 3 keys @ Port D.0 ... D.2) -------------------------
'3 test keys @ Port D.0, D.1 and D.2 (key0, key1, key2)
DDRD = &B00000000                                 'PortD = Input
PortD = &B11111111                                'activate Pullup
Key_Port Alias PinD                               'Input-Port Keys
Key0 Alias PIND.0                                 'Key0
Key1 Alias PIND.1                                 'Key1
Key2 Alias PIND.2                                 'Key2

Konfigurieren des externen Interrupts INT1:

'Configure INT1 interrupt for keys  
Config INT1 = Falling                             'PIND.3=INT1 any key
On Int1 Keys_isr                                  'INT1 Interrupt service routine
Enable INT1                                       'Interrupt keys
Enable Interrupts                                 'enable all Interrupts

INT1 reagiert mit "Falling" auf ein von H nach L fallendes Signal (Taster betätigt). Damit wird die Interrupt-Service-Routine angesprungen:

Keys_isr:
'Interrupt on INT1 (all keys, connected by diodes to INT1)
'Entered only if any key is low (=0V, pressed)
'Output: bitKey0 = 1        :key0 is pressed, =0: else
'        bitKey1 = 1        :key1 is pressed, =0: else
'        bitKey2 = 1        :key2 is pressed, =0: else

   bitKey0 = Not Key0                             'key0 pressed: =1
   bitKey1 = Not Key1                             'key1 pressed: =1
   bitKey2 = Not Key2                             'key2 pressed: =1
Return

Wenn ein Taster gedrückt ist, liegt der zugehörige Port auf logisch 0. Mit beispielsweise "bitKey1 = Not Key1" wird in diesem Falle bitKey1 auf 1 gesetzt. Auf weitere Maßnahmen zum Entprellen der Taster wurde verzichtet.

Im Hauptprogramm werden die Tastervariablen bitKeyx ausgewertet, hier testweise mit Displayanzeigen:

Do                                                'Start of main program ******

   If bitKey0 = 1 Then                            'key0 was pressed
      Call ClearLCDLine(1)
      LCD "Key 0"
      bitKey0 = 0                                 'Reset key status
   End If

   If bitKey1 = 1 Then                            'key1 was pressed
      Call ClearLCDLine(1)
      LCD "Key 1"
      bitKey1 = 0                                 'Reset key status
   End If

   'Key2 to be checked: pressed short or long
   If bitKey2 = 1 Then                            'key2 was pressed
      bytPressLong = KeyPressLong(2)              'Key2 pressed short/long?
      Call ClearLCDLine(1)
      Select Case bytPressLong
         Case 0                                   'Pressed short
            LCD "Key 2 short"
         Case 1                                   'Pressed long
            LCD "Key 2 long"
      End Select
      bitKey2 = 0                                 'Reset key status
   End If

Loop                                              'End of main program *******

Jeweils nach der Auswertung des Tasterstatus "bitKeyx" wird dieser auf "0" zurückgesetzt.

Sonderbehandlung für Key2 in der Function "KeyPressLong": Wurde kurz oder lange gedrückt? Alle 10 msec wird Key2 abgefragt. Die Do-Schleife wird so lange durchlaufen, bis entweder der Taster losgelassen wurde (Key_Port.KeyNum = 1, High-Potenzial) oder der Zähler intTmp0 den Wert KeyCounter (mit 100 vorbesetzt) erreicht hat. Wurde länger als 1 Sekunde gedrückt, so war es ein langer Druck (KeyPressLong = 1, sonst = 0). Die Nummer des Keys im Key_Port, hier 2, wird der Function als Parameter übergeben.

Function KeyPressLong(byVal KeyNum As Byte) As Byte
'Check if Key Key_Port.KeyNum is pressed short or long
'KeyPressLong = 1: pressed long (~ 1 sec), = 0: short

   intTmp0 = 0
   Do                                             'Wait for key to be released
      Incr intTmp0
      Waitms 10
   Loop Until Key_Port.KeyNum = 1 Or intTmp0 = KeyCounter
   If intTmp0 < KeyCounter Then
      KeyPressLong = 0                            'key was pressed short
   Else
      KeyPressLong = 1                            'key was pressed long
   End If
End Function

Die Entprellung der Taster funktioniert zumeist, aber nicht wirklich zuverlässig. Problem ist, dass mit dem Prellen des Tasterkontaktes der externe Interrupt mehrfach ausgelöst werden kann und somit die Tasterfunktion ungewollt mehrfach anstößt. Im nächsten Kapitel wird eine "kugelsichere" Methode gezeigt.


Taster mit Timer-Interrupt (Dannegger-Methode)

Ist ein mehr oder weniger kompletter Port für mehrere Taster verfügbar, zeigt [1], "Tasten entprellen – Bulletproof", basierend auf einer Assembler-Routine von Peter Dannegger, eine andere gut funktionierende Methode mit Timer-Interrupts. Wie beim Drehencoder hat Tom Baer die Mimik in Bascom beigesteuert. Im Antennentuner auf dieser Website wurde diese Methode verwendet.

Die Taster liegen alle an einem Port, wie im vorherigen Beispiel an PortD. Sie schalten das durch die internen Pullups erzeugte H-Potenzial nach Masse. Die Schaltung ist vergleichbar mit der oben gezeigten; die Koppeldioden entfallen, damit die Verbindung zu INT1, die Kondensatoren zum Entprellen bleiben. Das Abfragen der Schaltsignale erfolgt über einen internen Timer-Interrupt, hier Timer0. Steht ein vollständiger Port zur Verfügung, können so 8 Taster bedient werden.

Die im Aufmacherbild oben gezeigten C's an jedem Taster-Port können entfallen. Sie stören aber auch nicht.

Erklärung der Variablen für die Taster und der verwendeten Routinen:

'Variables Keys
Dim bytIwr0 As Byte
Dim bytKey_ct0 As Byte
Dim bytKey_ct1 As Byte
Dim bytKey_state As Byte
Dim bytKey_press As Byte
Dim bytKey_save As Byte

Declare Sub ClearLCDLine(byVal bytLine As Byte)

Konfigurieren des Taster-Ports D:

'Configure PortD (Key port, 8 keys @ Port D.0 ... D.7)
'3 test keys @ Port D.0, D.1 and D.2 (key0, key1, key2)
DDRD = &B00000000                                 'PortD = Input
PortD = &B11111111                                'activate Pullup
Key_port Alias PinD                               'Input-Port Keys
Const Key0 = 0                                    'Key0 @ PIND.0
Const Key1 = 1                                    'Key1 @ PIND.1
Const Key2 = 2                                    'Key2 @ PIND.2
Const Key3 = 3                                    'Key3 @ PIND.3
Const Key4 = 4                                    'Key4 @ PIND.4
Const Key5 = 5                                    'Key5 @ PinD.5
Const Key6 = 6                                    'Key6 @ PIND.6
Const Key7 = 7                                    'Key7 @ PIND.7

Dies gilt für den Glücksfall, dass ein kompletter Port, hier Port D, für Taster zur Verfügung steht. Wie wir uns für die Taster noch freie Pins verschiedener Ports zusammenstellen können, ist weiter unten erklärt.

Initialisieren der Taster-Variablen:

'Initialize Key Data
bytKey_ct0 = 0
bytKey_ct1 = 0
Decr bytKey_ct0
bytKey_ct1 = bytKey_ct0
bytKey_state = 0
bytKey_press = 0

Konfigurieren und Starten des Timer0:
Der 8 bit-Timer0 zählt ohne Voreinstellung von 0 bis 2^8-1 = 255. Die vom Quarz abgeleitete Zählfrequenz kann mit einem Prescale grob eingestellt werden. Eine Feinjustierung kann mit dem PresetTimer0 vorgenommen werden, so dass der Timer0 nicht mit "0", sondern mit dem PresetTimer0 zu zählen beginnt, was die Zeit zwischen zwei Overflows verkürzt.

'Configure Timer0-Interrupt for Keys (Timer0 = 8bit, 2^8=256 counts) ----------
'Overflow time: Overflow-Counts * Prescale / crystal frequency
'= 256 * 1024 / 16.000.000 = 16,3 msec = ~ 61 Hz (crystal 16 MHz)
'Possible Prescales: 8, 64, 256, 1024, here 1024
Config Timer0 = Timer , Prescale = 1024
Const Presettimer0 = 56                           'Initialize Timer0 (shorten time, >0)
On Timer0 Timer0_isr                              'On Overflow go to Timer0_isr
Enable Timer0 'Enable Timer0 interrupt
Enable Interrupts                                 'enable all interrupts
Timer0 = Presettimer0                             'initialize Timer0

Mit jedem Timer0-Überlauf wird ein Interrupt erzeugt und zur Interrupt-Service-Routine gesprungen. Mit der Quarzfrequenz 16 MHz, dem Vorteiler 1024 und dem Prescale 56, der den 8-Bit-Timer0 nur noch 256-56=200 Takte zählen lässt, passiert dies alle 200*1024/16.000.000=12,8 msec. In der Interrupt-Service-Routine erfolgt die Tasterauswertung:

Timer0_isr:
'Interrupt Service Routine for Timer0 (Keys)
'got from Thomas Baer in microcontroller.net
'Original (ASM) by Peter Dannegger
'http://www.mikrocontroller.net/topic/tasten-entprellen-bulletproof
'Output: bytKey_press bit pattern, key which was pressed
'        bytKey_state bit pattern, Key state, 1:pressed or 0:not pressed
'                     bits 0...7: keys 0...7 @ Key_port

   Timer0 = Presettimer0                          'Set Timer0
   bytKey_save = bytKey_state                     'save old status before keys were pressed
   bytIwr0 = Key_port                             'get Bits of the KeyPorts
   bytIwr0 = Not bytIwr0
   bytIwr0 = bytIwr0 Xor bytKey_state
   bytKey_ct0 = bytKey_ct0 And bytIwr0
   bytKey_ct1 = bytKey_ct1 And bytIwr0
   bytKey_ct0 = Not bytKey_ct0
   bytKey_ct1 = bytKey_ct1 Xor bytKey_ct0
   bytIwr0 = bytIwr0 And bytKey_ct0
   bytIwr0 = bytIwr0 And bytKey_ct1
   bytKey_state = bytKey_state Xor bytIwr0
   bytIwr0 = bytIwr0 And bytKey_state
   bytKey_press = bytKey_press Or bytIwr0
Return

Die Auswertemimik ist tricky. Peter Dannegger hat sie ursprünglich, wenn ich das richtig verstanden habe, aus der Verknüpfung logischer Funktionen wie AND, NAND, OR und XOR in Assembler realisiert, was sich in der obigen Umsetzung in BASCOM durch Tom Baer wiederfindet. Einfach genial. Einzelheiten in [1], auch mit Varianten in C.

Die Timer0_isr beobachtet immer alle 8 Bits des Key_port. Dadurch, dass wir im Hauptprogramm nur die einzelnen Bits von bytKey_state auswerten, hier z.B. mit "bytKey_state.2" das Bit 2, das den Taster an PD.2 darstellt, soll uns nicht stören, was auf Key_port.6 und ...7 los ist. Beim obigen Alias auf einen gesamten Port es es uninteressant, was auf den nicht ausgewerteten Pins passiert.

Die Timer0-isr gibt drei Byte-Variablen zurück: bytKey_press, bytKey_state und bytKey_save.

  • Die 8 bits in der Byte-Variablen "bytKey_press" zeigen an, welcher der (maximal 8) Taster gerade gedrückt wurde, also bei der letzten Abfrage durch die Timer0_isr von ungedrückt nach gedrückt gewechselt hat, 1 = wurde soeben gedrückt, 0 = ist unverändert.
  • Die 8 bits in der Byte-Variablen "bytKey_state" zeigen entsprechend den aktuellen Zustand der Taster im Augenblick der Abfrage in der Timer0_isr an, 1 = im Augenblick gedrückt gehalten, 0 = nicht gedrückt.
  • Die 8 bits in der Byte-Variablen "bytKey_save" geben den Zustand der Taster vor der Auswertung in der Timer0_isr an. Damit lässt sich also feststellen, ob zwischen zwei Aufrufen der Timer0_isr Taster überhaupt betätigt wurden.

Wie in folgenden Kapiteln gezeigt wird, lässt sich mit bytKey_state feststellen, wann ein Taster gerade wieder losgelassen wurde. Damit lässt sich ermitteln, ob der Tastendruck kurz oder lang war.

Die Zuordnung ist oben mit "Const Keyx = x" festgelegt, z.B. "Key2 = 2" ist der Taster an PortD.2.

Im Hauptprogramm gilt es dann nur noch, " bytKey_press" und " bytKey_state" auszuwerten und entsprechend zu reagieren.

Do                                                'Start of main program ******

   'Disable/enable timer0 may be omitted, test it
   If bytKey_state <> bytKey_save Then            'Any key was pressed
      Disable timer0                              'Stop Timer0 for display
      'Check Key Key0 --------------
      If bytKey_press.Key0 = 1 Then               'key0 is pressed
         Call ClearLCDLine(1)
         LCD "Key 0"
      End If
      'Check Key Key1 --------------
      If bytKey_press.Key1 = 1 Then               'key1 is pressed
         Call ClearLCDLine(1)
         LCD "Key 1"
      End If
      'Check Key Key2 -------------
      If bytKey_state.Key2 = 1 Then               'key2 is pressed
         Call ClearLCDLine(1)
         LCD "Key 2"
      End If
      bytKey_press = 0                            'reset Key_press
      Enable Timer0                               'Restart Timer0
   End If

Loop                                              'End of main program *******

Hier wurden die zwei Möglichkeiten, einen Taster abzufragen, angewandt, an Key0 und Key1 mit "bytKey_press" und an Key2 mit "bytKey_state"

  1.  Abfrage "If bytKey_state.Key2 = 1 Then"
    Dieses Ereignis tritt dann ein, wenn der Taster im Augenblick der Abfrage tatsächlich gedrückt ist.
    Wurde er zwar gedrückt, aber gerade kurz vor der Abfrage wieder losgelassen, ist bytKey_state.Key2 = 0. Dann erfolgt keine Reaktion auf den Tastendruck. In der Regel wird die Do...Loop aber so schnell durchlaufen, dass dieser Zustand eher unwahrscheinlich ist.
  2. Abfrage "If bytKey_press.Key0 = 1 Then"
    Dieses Ereignis tritt dann ein, wenn der Taster irgendwann einmal gedrückt wurde und dieses Flag noch nicht gelöscht wurde.
    bytKey_press.Key0 wird dann auf 1 gesetzt, wenn der Zustand des entsprechenden Tasters von ungedrückt nach gedrückt wechselt, und behält diesen Zustand bei bis zu einem Reset.
    Um sicher zu gehen, dass immer eine aktuelle Tastenbetätigung erfasst wird, muss nach der Auswertung im Hauptprogramm mit "bytKey_press.Key0 = 0" ein Reset erfolgen. Fallweise auch, wie hier gezeigt, ein Reset des ganzen Bytes (aller Taster) mit "bytKey_press = 0".

Taster an verschiedenen Ports

Was, wenn kein vollständiger Port für die Taster zur Verfügung steht? Kann ja mal passieren bei kleinen AVR oder wenn die Leitungsführung auf der Platine nur mit unschönen Brücken zu bewerkstelligen wäre.

Die Timer0_isr wertet das Byte Key_port aus. Im obigen Beispiel ist Key_port mit dem Alias (Key_port Alias PinD) auf den gesamten Port D gelegt. Wenn z.B. die AVR-Pins PC.0, PC.1, PD.6 und PD.7 noch für Taster frei sind, lässt sich Key_port daraus bitweise konfigurieren. Merkwürdigerweise geht das nicht mit einem Alias im Erklärungsteil des Programms, z.B. mit Key_port.0 Alias PINC.0, entsprechend bitweise für die anderen Port-Pins. Key_port muss nun im Erklärungsteil als Byte dimensioniert werden. Zur Laufzeit werden in der Timer0_isr die die Pins den Bits 0...7 von Key_port zugewiesen, etwa so:

Dim Key_port As Byte                              'Declare Key_port

'...

Timer0_isr:
'...
bytKey_save = bytKey_state                     'save old status before keys were pressed

'Supplement: assingment of keys at different ports to key_port bits
Key_port.0 = PINC.0                            'Key store   @ PINC.0
Key_port.1 = PIND.7                            'Key up      @ PIND.7
Key_port.2 = PIND.6                            'Key down    @ PIND.6
Key_port.3 = PINC.1                            'Key encoder @ PINC.1


bytIwr0 = Key_port                             'get Bits of the KeyPort
'...

Das geht in den nachfolgenden Beispielen mit der Kurz-/Lang-Erkennung genau so.

Taster an gemischt genutzten Ports

Was, wenn sich die Taster einen Port mit anderen Funktionen, etwa Steuerleitungen eines LCD, teilen müssen? In der Timer0_isr würden dann die Kontrollbytes, etwa "bytKey_press", von den anderen Funktionen zugemüllt. Dann filtern wir eben die Taster-Pins aus dem Port heraus.

Nehmen wir mal an, am Port B.2 bis B.4 sind drei Taster, "Up", "Down" und "OK" angeschlossen.

Dim bytKey_Pressed As Byte                        '>0 if any key  is pressed
Dim bytKey_mask As Byte 'Mask for keys
bytKey_Mask = &B00011100 '=1: key at pin 2, 3, 4
Const Key_Up = 2 'Key Up @ PinB.2
Const Key_Down = 3 'Key Down @ PINB.3
Const Key_OK = 4 'Key OK @ PinB.4

Wir basteln uns eine Maske bytKey_Mask, die an den Bitpositionen, die am Controller-Port einem Tasteranschluss entsprechen, hier also 2, 3, 4, eine "1" haben. Mit einem logischen "AND" können wir damit das von der obigen Timer0_isr gelieferte bytKey_press-Byte bitweise so maskieren, dass nur noch die Bits 2, 3, und 4 was tun. Alle anderen sind konstant Null.

      bytKey_Pressed = bytKey_press AND bytKey_mask       'Mask out keys on port
If bytKey_Pressed > 0 Then 'Any key was pressed
If bytKey_press.Key_OK = 1 Then 'Key OK was pressed

'Do something usefull

bytKey_press.Key_OK = 0 'Reset Key OK pressed flag
End If
If bytKey_press.Key_Up = 1 Then 'Key Up was pressed

'Do something usefull

bytKey_state.Key_Up = 0 'Reset Key Up status flag
End If
If bytKey_press.Key_Down = 1 Then 'Key Down was pressed

'Do something usefull

bytKey_state.Key_Down = 0 'Reset Key Down status flag
End If
End If

Wenn mindestens eines der ausmaskierten Bits gesetzt ist, wird bytKey_Pressed > 0, d.h. irgendeiner der 3 Taster wurde betätigt. Über die Bits im bytKey_press-Byte können wir dann prüfen, welcher der Taster es denn war.

 

Referenzen

[1]  http://www.mikrocontroller.net/topic/tasten-entprellen-bulletproof

 

Download

tasterextern_100.zip

tasterintern_100.zip

« Taster mit BASCOM
TOP » Taster am ADC