Countdown-Zähler

Erlaubt den Aufbau eines Programms ohne Warteschleifen.


In countdown.h wird die Struktur count definiert, deren Einträge alle 10 Millisekunden um Eins verringert werden. Ist ein Wert bis auf Null gelaufen, bleibt er bei Null stehen, bis er durch die Anwendung wieder aufgezogen wird. Neben den Einträgen für 10 Millisekunden hat die Struktur auch Abschnitte, in die Werte eingetragen werden können, die jede Sekunde oder jede Minute um Eins vermindert werden sollen.

Herabzählen der Werte übernimmt die Funktion job_countdown(), die alle 10 Millisekunden aufgerufen werden muss. Von dem hier vorgestellten Modul wird also nur die Aktualisierung der Countdown-Zähler übernommen; die Zeitbasis von 10 Millisekunden muss ausserhalb realisiert werden.

Durch Einsatz dieser simplen Technik kann ein Programm komplett von lästigen Warteschleifen befreit werden, die nur Zeit vertrödeln und oft andere Programmteile behindern. Dies ist auf Systemen mit knappen Resourcen angezeigt, die zu klein sind, um etwa ein Echtzeit-Betriebssystem zu fahren, oder wo ein solches System zu viel kostet (Zeit, Speicher, Anschaffung, ...)

Um etwa eine LED im Sekundentakt blinken zu lassen ist dann nur noch folgender Programmschnipsel notwendig:

// Ist der Blink-Zähler der LED abgelaufen?
if (0 == count.ms10.led_blink)
{
    // Ja:
    // Blink-Zähler neu aufziehen (eine Sekunde hat 100 * 10ms)
    count.ms10.led_blink = 100;

    // LED blinken
    ...
}

Ein Programmaufbau komplett ohne die altgewohnten Warteschleifen setzt natürlich ein Umdenken voraus, und wenn Zeitspannen aufeinander folgen sollen und nicht (quasi)gleichzeitig ablaufen müssen, dann muss man die einzelnen Phasen in einer Variablen merken und entsprechend der Phase die dafür notwendigen Aktionen ausführen. Wie das konkret aussieht sei vorest eine Übung für den Leser ;-)

Die Struktur count hat folgende Teilbereiche:

count.ms10
8-Bit Einträge, die alle 10 Millisekunden um Eins vermindert werden und bei Null stehen bleiben.
count.sec
8-Bit Einträge, die jede Sekunde um Eins vermindert werden und bei Null stehen bleiben.
count.min
8-Bit Einträge, die jede Minute um Eins vermindert werden und bei Null stehen bleiben.

Die Auflösung der Minuten-Timer ist eine Minute, die Zeiten werden also nicht 100% exakt eingehalten, weil die internen Zähler i.d.R. nicht zeitgleich mit dem Aufziehen eines Zählers zurückgesetzt werden. In der Praxis hat das aber noch bei keinem meiner Projekt, in denen ich den Countdown-Mechanismus verwende, eine Rolle gespielt. Will man zwei Minuten abwarten, ist es übrigens besser, stattdessen 120 Sekunden zu warten denn dadurch ist die Zähler-Auflösung dann bei einer Sekunde.


countdown.h


Hier werden die Werte eingetragen, die hinunter gezählt werden sollen. Nicht benötigte Werte sollten auskommentiert oder entfernt werden, um nicht unnötig Zeit zu verbrauchen. Dieser Beispielheader ist aus der Eieruhr-Software; neben den Einträgen, die intern bebraucht werden, gibt es nur zwei weitere.

Teilstrukturen können leer bleiben (GNU-C), ohne daß es Fehler beim Compilieren gibt oder diese Platz oder Zeit kosten würden.

#ifndef COUNTDOWN_H
#define COUNTDOWN_H

#include <stdint.h>

typedef struct
{
    struct
    {
        // .ms10.timer_1s wird intern verwendet
        uint8_t timer_1s;

        // 8-Bit Werte, die alle 10ms runtergezählt werden sollen
        uint8_t beep;
    } ms10;

    struct
    {
        // .sec.timer_1m wird intern verwendet
        uint8_t timer_1m;

        // 8-Bit Werte, die jede Sekunde runtergezählt werden sollen
    } sec;

    struct
    {
        // 8-Bit Werte, die jede Minute runtergezählt werden sollen
        uint8_t eiuhr;
    } min;
} countdown_t;

// 'count' global bekannt machen
extern volatile countdown_t count;

// Diese Routine alle 10ms aufrufen
extern void job_countdown (void);

#endif // COUNTDOWN_H

countdown.c


Diese Datei braucht nicht geändert zu werden, es sei denn, man will zB noch einen Stundenzähler hinzuprogrammieren. Oder einen Zähler mit 16 Bit Auflösung.

#include "countdown.h"

countdown_t volatile count;

#ifdef __GNUC__
#    define inline __attribute__((always_inline))
#endif // GCC

static inline void down (uint8_t, char volatile *);

void down (uint8_t i, char volatile * cd)
{
    // Komponenten (i Stück) von hinten nach vorne durchlaufen
    // Zeiger cd steht anfangs hinter der letzten Komponente
    do
    {
        // Zeiger erniegrigen, Wert lesen und um 1 vermindern
        char val = *(--cd) - 1;

        // Falls der Wert nicht abgelaufen ist wird er (vermindert)
        // zurückgeschrieben, ansonsten bleibt er bei 0.
        if (val != -1)
            *cd = val;
    } while (--i);
}

#define ITEM_COUNT(A,B) sizeof (A) / sizeof (B)

#define DOWN(x)             \
    do {                    \
        if (!sizeof (x))    \
            return;         \
        down (ITEM_COUNT(x, char), (char*) & x + ITEM_COUNT(x, char)); \
    } while (0)

// Alle 10 Millisekunden aufrufen
void job_countdown (void)
{
    // Die 10ms-Werte von 'count' runterzählen (8-Bit Werte)
    DOWN   (count.ms10);

    // .ms10.timer_1s abgelaufen?
    if (count.ms10.timer_1s)
        // Nein
        return;

    // Ja. Eine neue Sekunde beginnt: Sekunden-Zähler neu aufziehen
    count.ms10.timer_1s = 100;

    // Die Sekunden-Werte von 'count' runterzählen
    DOWN (count.sec);

    // Falls bei den Minuten nicht zu tun ist sind wir fertig.
    if (!sizeof (count.min))
        return;

    // Minuten-Zähler abgelaufen?
    if (count.sec.timer_1m)
        // Nein
        return;

    // Minuten-Zähler neu aufziehen
    count.sec.timer_1m = 60;

    // Die Minuten-Werte von 'count' runterzählen
    DOWN (count.min);
}


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 job_countdown() aufgerufen.

Die Hauptschleife deutet an, wie man es anstellt, gleichzeitig zwei LEDs blinken zu lassen; und zwar eine im Takt von einer Sekunde und eine im Rhythmus von 1.1 Sekunden. Dabei hängt die Hauptschleife zu keinem Zeitpunkt nutzlos in einer Warteschleife! Man kann also ungestört noch andere Aufgaben erledigen oder sich getrost schlafen legen (idle) um Strom zu sparen und sich durch den Timer-IRQ wieder wecken lassen.

Für dieses konkrete Beispiel müssten in count noch die Einträge .ms10.led1 und .ms10.led2 angelegt werden (An der Stelle, wo jetzt beep steht, welches ja nicht gebraucht wird und nur der Veranschaulichung gedient hat.)

Im Beispiel geschieht die Verarztung des Countdown-Zähler aus der Interrupt-Routine heraus. Ebenso ist vorstellbar, den Aufruf der Zählerroutine 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. Dazu ist dann essenziell, daß im Programm keine Warteschleifen sind, denn diese würden einen Wert abtesten, der in der Warteschleife nie (auch nicht innerhalb einer ISR-Blase) verändert wird!

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

#include "countdown.h"

#define F_CPU 1000000

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

static void timer1_init (void);

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;
    }

    if (irq_num == 0)       job_countdown();
    //if (irq_num == 1)     noch was erledigen alle 10ms, aber bitte nicht alles auf einmal ;-)
    //if (irq_num == 2)     ...

    interrupt_num_10ms = irq_num;
}

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
    TIMSK = (1 << OCIE1A);
}

int main()
{
    timer1_init();

    // IRQs global erlauben
    sei();

    while (1)
    {
        ...
        // Ist Zähler für LED1 abgelaufen?
        if (!count.ms10.led1)
        {
            // Ja:
            // Zähler neu aufziehen auf 1 Sekunde
            count.ms10.led1 = 100;
            // Toggle LED1
            ...
        }

        // Ist Zähler für LED2 abgelaufen?
        if (!count.ms10.led2)
        {
            // Ja:
            // Zähler neu aufziehen auf 1.1 Sekunden
            count.ms10.led2 = 110;
            // Toggle LED2
            ...
        }
        ...
    } // main Loop
}