Drehgeber-Auswertung

Ein Quadratur-Decoder.


Zur Drehgeber-Auswertung: Es gibt zwei digitale Eingangs-Ports; diese können zusammen 4 Zustände annehmen. Zusammen mit dem Zustand beim letzten Aufruf – ebenfalls 4 Möglichkeiten – ergeben sich insgesamt 16 mögliche Zustandswechsel. Der zum betreffenden Zustandswechsel gehörende Strittwert (0, +1, -1 oder "ungültig") wird aus einer Tabelle gelesen und zurückgeliefert.

Folgende Aktionen müssen noch ausgefüllt werden:

MAKE_IN
Schaltet einen Port als Eingang
SET
Setzt einen Port auf HIGH bzw. aktiviert seinen Pullup-Woderstand
IS_SET
Ungleich 0, wenn der Port (als Eingang) HIGH ist, 0 sonst

drehgeber.h


#ifndef _DREHGEBER_H_
#define _DREHGEBER_H_

#define DREH_INVALID -128

static inline char drehgeber_step();
static inline char drehgeber_job();

#if defined (__GNUC__) && defined (__AVR__)
#    include <avr/pgmspace.h>
#else
#    define PROGMEM
#    define pgm_read_byte(x) (*(x))
#endif // avr-gcc

// Aufrufen, wenn eine Flanke an einem der Ports festgestellt wurde,
// zB aus einer ISR heraus. Evtl funktioniert auch Polling.
char drehgeber_step ()
{
    static const char drehgeber_trainsitions[] PROGMEM =
    {
         0,                       1,           -1, DREH_INVALID,
        -1,                       0, DREH_INVALID,            1,
         1,            DREH_INVALID,            0,           -1,
         DREH_INVALID,           -1,            1,            0
    };

    // Speichert den vorherigen 4-er Zustand in Bits 0 und 1
    static uint8_t a_alt;
    uint8_t a = a_alt;

    // Den aktuellen 4-er Zustand in Bits 2 und 3 einfügen
    if (IS_SET (PORT_DREH_A))        a |= (1 << 2);
    if (IS_SET (PORT_DREH_B))        a |= (1 << 3);

    // Den aktuellen Zustand für den nächsten Aufruf merken
    a_alt = a >> 2;

    // Zustandswechsel auf Schrittwert abbilden
    return (char) pgm_read_byte (& drehgeber_trainsitions[a]);
}

// Mein Drehgeber hat eine Rasterung und bei jedem Raster erzeugt
// er *zwei* Flanken. Man muss also die Anzahl der Schritte etwas
// mühsam durch 2 teilen. Falls der Drehgeber nur einen Schritt
// pro Raster liefert oder keine Rasterung hat, brauch man diese
// Funktion nicht und kann stattdessen direkt `drehgeber_step' nehmen.
char drehgeber_job()
{
    char step = drehgeber_step();
    char s = 0;

    if (DREH_INVALID != step)
    {
        static char tick;
        char t = step + tick;

        // Wenn |t| >= 2 ist gibt es einen Schritt
        if (t >=  2)    s =  1;
        if (t <= -2)    s = -1;

        // Schritt aus t entfernen: t -= 2*s
        t -= s;
        t -= s;

        tick = t;
    }

    return s;
}

#endif // _DREHGEBER_H_

main.c


Anwendungsbeispiel für einen AVR ATmega88. PORT_DREH_A und PORT_DREH_B müssen noch definiert werden und MAKE_IN, SET und IS_SET durch den konkreten C-Code ersetzt werden. Falls die Drehrichtung nicht stimmt, einfach die Portdefinitionen austauschen.

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

// Für alte avr-gcc Versionen
#ifndef SIGNAL
#include <avr/signal.h>
#endif // SIGNAL

#include "drehgeber.h"

// Kontroller-Frequenz dem Programm mitteilen
// Besser per Kommando-Zeile via -DF_CPU=1000000
#define F_CPU 1000000

// IRQs pro Sekunde
// Besser per Kommando-Zeile via -DIRQS_PER_SECOND=5000
#define IRQS_PER_SECOND     5000

static void ioinit(void);
static void timer1_init (void);

// Zählt die Schritte: von -128...127
char volatile steps;

// Die Interrupt Service Routine (ISR) wird IRQS_PER_SECOND
// mal pro Sekunde aufgerufen.
// Bei jedem Aufruf wird auf die Drehgeber-Ports geschaut.
// So ergibt sich eine Entprellung und es werden keine extra
// IRQs belegt nur für den Drehgeber.
SIGNAL (SIG_OUTPUT_COMPARE1A)
{
    steps += drehgeber_job();

    // In dieser IRQ können auch andere periodisch zu erledingende
    // Aufgaben ausgefürt werden wie der Countdown-Zäler.
}

void ioinit (void)
{
    // Das ist noch anzupassen auf die Ports, die Du verwendest...
    MAKE_IN (PORT_DREH_A); SET (PORT_DREH_A);
    MAKE_IN (PORT_DREH_B); SET (PORT_DREH_B);
}

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

    // 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);

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

int main (void)
{
    // I/O initialisieren
    ioinit ();

    // Drehgeber initialisieren (man weiß ja nicht, wie er steht).
    // Rückgabewert wird nicht gebraucht.
    drehgeber_step();

    // Timer1 initialisieren
    timer1_init();

    // IRQs erlauben
    sei ();

    while (1)
    {
        // `steps' auswerten
    } // main Loop
}