Taster-Abfrage

und was dazu gehört


Dieses Modul wird verwendet bei der immer wiederkehrenden Aufgabe der Taster-Abfrage. Der C-Code ist projekt- und hardware­unabhängig – zumindest was die Taster­auswertung selbst angeht. Die Erzeugung einer Zeitbasis ist natürlich system­abhängig.

Ebenso wie beim Countdown-Zähler und der DCF77-Software wird die Arbeitsroutine für die zu erledigende Aufgabe im 10 Millisekunden-Rhythmus ausgeführt. Diese langsame Abtastrate liefert die Entprellung der Taster frei Haus.

In taster.h wird die Struktur taste_t definiert. Im Hauptprogramm legt man von diesem Typ so viele Variablen an, wie man Taster abzufragen hat. Ein Beispiel dazu findet sich ganz unten.

Die Struktur taste_t hat folgende, für den Anwender interessante Komponenten:

.mode
Dieser Eintrag legt das Verhalten des jeweiligen Tasters fest. Soll jedes Mal gemeldet werden, wenn der Taster gedrückt wird, schreibt man KM_SHORT in dieses Feld. Soll unterschieden werden zwischen langen und kurzen Tastendrucken, schreibt man KM_LONG hinein. Soll der Taster eine Auto-Repeate-Funktion haben, ist der Wert KM_REPEAT.
.key
Der Wert dieser Komponente wird nach taster kopiert, wenn diese Taste gedrückt wurde. Bei einem langen Tastendruck und falls .mode=KM_LONG ist, wird der Wert .key+KEY_LONG nach taster geschrieben.

Die Routine, die sich um die Tasterabfrage kümmert, heisst get_taster. Sie hat zwei Parameter. Der erste Parameter ist die Adresse des Tasters, der abgefragt werden soll. Der zweite Parameter informiert die Funktion über den Zustand des Ports, an den der Taster angeschlossen ist. Ist der Taster gedrückt, wird als zweiter Parameter 0 übergeben. Ist er nicht gedrückt, wird ein Wert ungleich 0 übergeben. get_taster hat keinen Rückgabewert, stattdessen wird gegebenenfalls die globale Variable taster auf einen neuen Wert gesetzt.

Verwendet man zwei Taster, dann könnte eine Deklaration so aussehen:

taste_t tasten[] =
{
     { .key = 1, .mode = KM_SHORT  }, // erster Taster
     { .key = 2, .mode = KM_REPEAT }  // zweiter Taster (auto-widerholen)
};

Es wird also ein Arrey mit zwei Taster-Strukturen angelegt und initialisiert. Der Aufruf von get_taster sieht damit folgendermassen aus (falls der erste Taster an Port B1 und der zweite an Port D3 angeschlossen ist):

    get_taster (& tasten[0], PINB & (1 << PB1));
    get_taster (& tasten[1], PIND & (1 << PD3));

Der Codeverbrauch des Moduls liegt bei ca. 130 Bytes. Werden einfach nur Tastendrucke ohne Sonderfunktion empfangen, also keine langen Drucke oder Auto-Repeate (TASTER_SAME_MODE=KM_SHORT), dann sinkt der Codeverbrauch auf ca. 40 Bytes.


taster.h


Hier muss eigentlich nichts angepasst werden, es sei denn die Zeiten ab wann ein Tastendruck als "lang" gilt oder die Wiederholrate für den Auto-Repeate. Die Stellen, die gegebenenfalls anzupassen sind, sind dokumentiert und leicht zu finden.

#ifndef TASTER_H
#define TASTER_H

#include <stdint.h>

// 'taster' bekannt machen
// Wurde keine Taste gedrückt, dann enthält diese Variable den Wert 0.
// Wurde eine Taste gedrückt, enthält diese Variable die Komponente
// .key des gedrückten Tasters (bzw. den um KEY_LONG vergrößerten
// von .key, wenn es sich um einen langen Tastendruck handelt).
// Es werden nur dann neue Tastendrucke angenommen, wenn taster=0 ist.
// Hat man in der Anwandung alse ein Taster-Ereignis verarbeitet,
// muss man taster=0 setzen. 'taster' berhält sich also wie ein
// Tastaturpuffer der Länge 1.
extern volatile uint8_t taster;

// Die möglichen Betriebsmodi eines Tasters
enum
{
    KM_SHORT,
    KM_LONG,
    KM_REPEAT,
    KM_SKIP
};

// Ein Taster
typedef struct
{
    // Betriebsmodus
    // KM_SHORT   einfach nur Tastendruck melden
    // KM_LONG    Tastendruck mit Unterscheidung lang/kurz
    // KM_REPEAT  bleibt die Taste gedrückt, so wird immer wieder der gleiche
    //            Taster-Wert geliefert, ähnlich wie bei einer PC-Tastatur
    // KM_SKIP    Verwendet in get_taster() den vormaligen Wert von 'tast' für
    //            den Fall, daß momentan kein Wert verfügbar ist
    uint8_t mode;

    // Wird der Taster gedrückt, wird dieser Wert in 'taster' geschrieben.
    // bzw. bei einem langen Tastendruck .key + KEY_LONG
    // Dieser Wert muss ungleich Null sein.
    uint8_t key;

    // Wird intern gebraucht
    uint8_t delay;
    uint8_t old;
} taste_t;

// Alle 10ms (oder so) aufrufen
extern void get_taster (taste_t * const, uint8_t);

// Zeiten (in Einheiten von 10 Millisekunden) für:
// Anfangsverzögerung bis Auto-Repeate loslegt (.mode = KM_REPEAT)
#define KEYREPEAT_DELAY 50
// Zeitverzögerung zwischen zwei Auto-Repeates (.mode = KM_REPEAT)
#define KEYREPEAT       30
// Ab dieser Dauer ist ein Tastendruck nicht mehr "kurz",
// sondern "lang" (.mode = KM_LONG)
#define KEYDELAY_LONG   50

// Wird auf .key draufaddiert, wenn es sich bei (.mode = KM_LONG)
// um einen langen Tastendruck handelt.
// Bei mehr als 4 Tastern muss dieser Wert vergrößert werden!
#define KEY_LONG 4

#endif /* TASTER_H */


taster.c


Diese Datei braucht nicht angepasst zu werden.

#include "taster.h"

volatile uint8_t taster;

#define PRESSED (2|1)
#define PRESS   (0|1)
#define RELEASE (2|0)

// Alle 10ms aufrufen. Auf diese Zeitbasis von 10ms beziehen sich die
// Defines in taster.h.
// 'ptast' ist die Adresse des auszuwertenden taste-Objekts.
// 'tast' ist der (Port-)Zustand des Tasters (low-aktiv):
// == 0 wenn der Taster gerade gedrückt (aktiv) ist.
// != 0 wenn der Taster gerade nicht gedrückt (inaktiv) ist.
// Ausgabe ist die Variable 'taster'.
void get_taster (taste_t * const ptast, uint8_t tast)
{
    // Nichts tun, falls die Anwendung den Wert aus 'taster' noch nicht
    // abgehohlt hatden
    if (taster)
        return;

    // ist Taster gedrückt? (low aktiv)
    tast = !tast;

    // alten Taster-Zustand lesen
    uint8_t taster_old = ptast->old;
    uint8_t mode  = ptast->mode;

#ifdef TASTER_SAME_MODE
    // Let GCC throw away superfluous code if there is some
    // extra knowledge.
    mode = TASTER_SAME_MODE;
#endif // TASTER_SAME_MODE

    // skip?
    // Für Sonderanwendung: Wenn kurzzeitig kein sinnvoller Wert
    // am Taster-Port anliegt, wird so kein Phanton-Tastendruck
    // registriert und die Zeit (delay) läuft dennoch weiter.
    if (KM_SKIP == mode)
        tast = taster_old;

    // Taster-Zustand merken
    ptast->old = tast;

    // state = 1: eben gedrückt
    // state = 2: eben losgelassen
    // state = 3: bleibt gedrückt
    uint8_t state = taster_old << 1;
    state |= tast;

    tast = 0;

    uint8_t delay = ptast->delay;
    uint8_t key   = ptast->key;

    if (state == PRESS)
    {
        // Taste wurde eben gedrückt

        if (mode != KM_LONG)
            tast = key;

        delay = 0;
    }
    else if (state == PRESSED)
    {
        // Taste bleibt gedrückt

        if (delay < 0xfe)
            delay++;
    }
    else if (state == RELEASE)
    {
        // Taste wurde losgelassen
        if (mode == KM_LONG && delay != 0xff)
            tast = key;
    }

    if (mode == KM_LONG)
    {
        // Langer Tastendruck

        if (delay == KEYDELAY_LONG)
        {
            tast = KEY_LONG + key;
            delay = 0xff;
        }
        ptast->delay = delay;
    }
    else if (mode == KM_REPEAT)
    {
        // Auto-Repeate

        if (delay == KEYREPEAT_DELAY)
        {
            tast = key;
            delay = KEYREPEAT_DELAY - KEYREPEAT;
        }
        ptast->delay = delay;
    }

    // Tasten-Code speichern
    taster = tast;
}


main.c


Ein Beispiel wie es aussehen könnte auf einem ATtiny2313. Es wird Timer1 so grogrammiert, daß er 5000 IRQs pro Sekunde auslöst. In der Interrupt-Routine wird die Anzahl der IRQs mitgezählt, und wenn 10 Millisekunden voll sind, wird get_taster() aufgerufen. Da zwei Taster angeschlossen sind, wird die Funktion auch zweimal aufgerufen – einmal für jeden Taster. Dabei werden die Aufrufe nicht in ein und derselben IRQ erledigt, sondern auf zwei aufeinanderfolgende IRQs verteilt.

Die Hauptschleife deutet an, wie man die Tasterabfrage macht und die Tastenwerte aus taster abhohlt.

Im Beispiel geschieht die Verarztung der Taster-Routine(n) aus der Timer-ISR heraus. Ebenso ist vorstellbar, den Aufruf in die Hauptschleife auszulagern und die ISR so zu entlasten. In diesem Falle würde man in der ISR nur einen Merker setzen (oder hochzählen), daß 10 Millisekunden voll sind, und dies in der Hauptschleife abhandeln.

Dies ist ein Ausschnitt aus dem Quellcode zu meiner Eieruhr.

#include <avr/io.h>
#include <avr/interrupt.h>

#include "taster.h"

#define IRQS_PER_SECOND     5000
#define IRQS_PER_10MS       (IRQS_PER_SECOND / 100)

static void timer1_init (void);

// Ein paar Werte festlegen um "magische Zahlen" aus dem Code rauszuhalten.
enum
{
    NO_KEY = 0,

    KEY_EINER   = 1,
    KEY_ZEHNER  = 2,

    KEY_EINER_L  = KEY_EINER  + KEY_LONG,
    KEY_ZEHNER_L = KEY_ZEHNER + KEY_LONG
};

// Zwei Tasten, .mode ist KM_LONG
taste_t tasten[] =
{
     [0] = { .key = KEY_EINER,  .mode = KM_LONG }
   , [1] = { .key = KEY_ZEHNER, .mode = KM_LONG }
};

ISR (SIG_OUTPUT_COMPARE1A)
{
    static uint8_t interrupt_num_10ms;

    uint8_t irq_num = interrupt_num_10ms;

    // interrupt_num_10ms erhöhen und mit Maximalwert vergleichen
    if (++irq_num == IRQS_PER_10MS)
    {
        // 10 Millisekunden sind vorbei
        // interrupt_num_10ms zurücksetzen
        irq_num = 0;
    }

    // Alle 10ms die Taster abfragen (ginge auch zusammen, mach ich aber net ;-)
    if (irq_num == 0)       get_taster (& tasten[0], IS_SET (PORT_EINER));
    if (irq_num == 1)       get_taster (& tasten[1], IS_SET (PORT_ZEHNER));

    interrupt_num_10ms = irq_num;
}

void timer1_init()
{
    // Timer1 ist Zähler: Clear Timer on Compare Match (CTC, Mode #4)
    // Timer1 läuft @ F_CPU: prescale = 1
    TCCR1A = 0;
    TCCR1B = (1 << WGM12) | (1 << CS10);

    // OutputCompareA für gewünschte Timer1 IRQ-Rate
    OCR1A = (unsigned short) ((unsigned long) F_CPU / IRQS_PER_SECOND-1);

    // OutputCompare-Interrupt A für Timer1
    TIMSK = (1 << OCIE1A);
}

int main()
{
    // Taster sind Input mit PullUp (low active)
    SET (PORT_EINER);
    SET (PORT_ZEHNER);

    // Timer1 macht IRQs
    timer1_init();

    // IRQs zulassen
    sei();

    // Hauptschleife
    while (1)
    {
        uint8_t tast = taster;

        if (tast)
            taster = NO_KEY;

        if (tast == KEY_EINER)
        {
            // Taste "Einer" (kurz)
        }

        if (tast == KEY_EINER_L)
        {
            // Taste "Einer" (lang)
        }

        if (tast == KEY_ZEHNER)
        {
            // Taste "Zehner" (kurz)
        }

        if (tast == KEY_ZEHNER_L)
        {
            // Taste "Zehner" (lang)
        }

        ...
    } // main Loop
}