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_link = 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, dann muss man die einzelnen Phasen in einer Variablen merken und entsprechend der Phase die dafür notwendigen Aktionen ausführen.

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 eine Rolle gespielt. Will man zwei Minuten abwarten, ist es übrigens besser, stattdessen 120 Sekunden zu warten; dadurch ist die Zähler-Auflösung dann bei einer Sekunde.


countdown.h


Hier werden die Werte eingetragen, die heruntergezählt werden sollen. Nicht benötigte Werte können entfernt werden, um nicht unnötig Zeit zu verbrauchen. Analog können neue Zähler hinzugefügt werden.

Für dieses konkrete Beispiel zweier asynchron blinkender LEDs enthält count die Einträge .ms10.led1 und .ms10.led2, die in der Hauptschleife abgefragt werden.

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>

// Alle 10 ms aufrufen.
extern void job_countdown (void);

// In folgende Struktur die benötigten Countdown-Timer eintragen.
// Um ein Minute zu warten, ist es geschickter, einen
// 1s-Zähler anzulegen und ihn auf 60 aufzuziehen anstatt
// einen 1m-Zähler anzulegen und ihn auf 1 aufzuziehen.
// Die Zähler werden auf 0 herabgezählt und bleiben dann  stehen.
typedef struct
{
    // Die Komponente muss mindestens das erste Elemenet enthalten
    struct
    {
        // Erstes Element ist für internen Gebrauch!
        // Es bleibt nicht bei 0 stehen und wird immer wieder von
        // job_countdown aufgezogen, sobald der Zähler abgelaufen ist.
        uint8_t timer_1s;

        // Ab hier die benötigten 8-Bit Countdown-Zähler eintragen
        uint8_t wait_10ms;
        uint8_t led1;
        uint8_t led2;
    } ms10;

    // Die Sekunden-Timer.
    // Die Komponente darf auch komplett leer sein
    struct
    {
        // Erstes Element ist für internen Gebrauch!
        // Es bleibt nicht bei 0 stehen und wird immer wieder
        // von job_countdown aufgezogen, sobald der Zähler
        // abgelaufen ist.
        uint8_t timer_1m;

        // Ab hier die benötigten 8-Bit Countdown-Zähler eintragen
    } sec;

    // Die Minuten-Timer.
    // Die Komponente darf auch komplett leer sein
    struct
    {
        // Ab hier die benötigten 8-Bit Countdown-Zähler eintragen
    } min;
} countdown_t;

extern volatile countdown_t count;

static inline
void wait_10ms (const uint8_t t)
{
    count.ms10.wait_10ms = 1+t;
    while (count.ms10.wait_10ms);
}

#endif /* COUNTDOWN_H */

countdown.c


Diese Datei braucht nicht geändert zu werden! Es sei denn, man will zB noch einen Stundenzähler hinzuprogrammieren.

#include "countdown.h"

// An diesem Modul sind *KEINE* Änderungen nötig.
// Falls neue Zähler gebraucht werden, dann werden diese in
// countdown.h eingetragen, nicht hier!

// Unser Countdown-Objekt
countdown_t volatile count;

// Zählt die Anzahl gleichgroßer Komponenten in einer Struktur
#define ITEM_COUNT(A,B) sizeof (A) / sizeof (B)

static inline void down8 (int8_t *addr, uint8_t cnt)
{
    // Wir stellen uns hinter die letzte Komponente...
    addr += cnt;

    // ...und rollen das Feld von hinten auf
    do
    {
        // Adresse erniedrigen, Zähler-Wert lesen und
        // um 1 vermindern
        int8_t val = *--addr -1;

        if (val != -1)
            // Falls der Wert nicht schon 0 war
            // um 1 vermindert zurückschreiben
            *addr = val;

    } while (--cnt);
}

// Für ne leere Komponente ist nix zu tun. Ansonsten
// bestimmen wir die Anzahl der Elemente darin
// und verminden alle Zählerwerte ungleich 0 um 1.
#define DOWN(x)                                 \
    do                                          \
    {                                           \
        if (!sizeof (x))                        \
            return;                             \
        down8 ((int8_t *) & x, ITEM_COUNT(x, int8_t));          \
    } while (0)

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

    if (0 != count.ms10.timer_1s)
        return;
    // Eine Sekunde ist vergangen: 100ms-Zähler neu aufziehen
    count.ms10.timer_1s = 100;

    ///////////////////////////////////////////////////////////
    // Die Sekunden-Werte von 'count' runterzählen
    // (8-Bit Werte, falls vorhanden)
    DOWN (count.sec);

    if (!sizeof (count.min) || 0 != count.sec.timer_1m)
        return;

    // Eine Minute ist vergangen: Sekunden-Zähler neu aufziehen
    count.sec.timer_1m = 60;

    ///////////////////////////////////////////////////////////
    // Die Minuten-Werte von 'count' runterzählen
    // (8-Bit Werte, falls vorhanden)
    DOWN (count.min);
}

main.c


Ein Beispiel wie es für einen ATmega168 aussehen könnte. Es wird Timer2 so programmiert, daß er 1000 IRQs pro Sekunde auslöst. In der Interrupt-Routine wird die Anzahl der IRQs mitgezählt, und wenn 10 Millisekunden erreicht sind, wird job_countdown() aufgerufen um die Countdown-Zähler zu bedienen.

In main wird zunächst die Hardware so initialisiert, daß 1000 mal pro Sekunde Timer2-IRQ auslöst. Die Hauptschleife blinkt gleichzeitig zwei LEDs; eine im Takt von 1 Sekunde und eine zweite im Takt von 1.1 Sekunden. Man kann also in der Hauptschleife noch ungestört andere Aufgaben erledigen oder sich getrost schlafen legen (idle) um Strom zu sparen, und sich durch den Timer-IRQ wieder wecken lassen.

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/wdt.h>
#include <avr/interrupt.h>

#include "countdown.h"

// LED1
#define PORT_LED1 PORTC
#define DDR_LED1  DDRC
#define PIN_LED1  PINC
#define PAD_LED1  3

// LED2
#define PORT_LED2 PORTB
#define DDR_LED2  DDRB
#define PIN_LED2  PINB
#define PAD_LED2  1

// Taktrate: 1MHz ist Werkseinstellung
#define F_CPU       1000000

// IRQs pro Sekunde
#define IRQS        1000

// Prescaler für Timer2.
// Timer2 ist nur ein 8-Bit Zähler, d.h. OCR2A kann nur Werte
// 0..255 aufnehmen, washalb der Takt-Vorteiler gebraucht wird.
#define PRESCALE    8

/*************************************************************************/

static void timer2_init (void)
{
    // Mode #2 (CTC) und PRESCALE = 8
    TCCR2A = 1 << WGM21;
    TCCR2B = 1 << CS21;

    // OutputCompare für gewünschte Timer2 Frequenz
    OCR2A = (uint32_t) F_CPU / PRESCALE / IRQS -1;

    // OutputCompare-Interrupt A für Timer 2
    TIMSK2 = 1 << OCIE2A;
}

static void ioinit (void)
{
    // LED-Ports als Ausgang
    DDR_LED1 |= 1 << PAD_LED1;
    DDR_LED2 |= 1 << PAD_LED2;

    // LEDs ausschalten: LED1 ist gegen VCC geklemmt, LED2 gegen GND
    PORT_LED1 |= 1 << PAD_LED1;
    PORT_LED2 &= ~(1 << PAD_LED2);
}

/************************************************************************/

// Diese ISR wird 1000x pro Sekunde aufgerufen.

ISR (TIMER2_COMPA_vect)
{
    static uint8_t irqs_10ms;

    wdt_reset();

    ////////////////////////////////////////////////////////////
    // 10 ms-Takt für Jobs: countdown-Zähler, Tasten-Entprellung, DCF, ...
    irqs_10ms = 1 + irqs_10ms;

    // Sind 10 Millisekunden voll?
    // IRQS wird oben definiert, es gibt die Anzahl der IRQs pro Sekunde an.
    // Um 10ms zu erhalten, teilen wir also durch 100
    if (irqs_10ms >= IRQS / 100)
        irqs_10ms = 0;

    // Führe nicht alle Jobs gleichzeitig aus, damit eine ISR nicht
    // zu lange dauert. Stattdessen staffeln wir die Aufrufe: es gibt
    // nur einen je 10ms, aber immer einen anderen (oder garkeinen).

    if (0 == irqs_10ms)   job_countdown();
    // else if (1 == irqs_10ms)   mach_was_alle_10ms();
    // else if (2 == irqs_10ms)   mach_nochwas_alle_10ms();
}

/************************************************************************/

int main (void)
{
    // Hardware initialisieren
    ioinit ();

    // Timer2-ISR wird alle 10ms job_countdown() aufrufen.
    timer2_init();

    // Interrupts (IRQs) global erlauben
    sei();

    // Zeit vertrödeln bis sich die Spannung stabilisiert hat.
    // In die Hauptschleife gehört 'wait' oder 'delay' NICHT rein!
    wait_10ms (1);

    // Blockierfreie Hauptschleife
    // Weil die LEDs zu Anfang aus sind und die die Countdowns
    // nach der Initialisierung auf 0 stehen, beginnen die LEDs
    // sofort mit einem ON-Zyklus.
    while (1)
    {
        if (0 == count.ms10.led1)
        {
            // Countdown für LED1 ist abgelaufen:
            // Zähler aufziehen auf 1 Sekunde (100ms) und toggle LED1
            PIN_LED1 |= 1 << PAD_LED1;
            count.ms10.led1 = 100;
        }

        if (0 == count.ms10.led2)
        {
            // Countdown für LED2 ist abgelaufen:
            // Zähler aufziehen auf 1.1 Sekunde (110ms) und toggle LED2
            PIN_LED2 |= 1 << PAD_LED2;
            count.ms10.led2 = 110;
        }

        // Weiterer Code in der Hauptschleife
        // ...
    } // Hauptschleife

    // Hier kommen wir nie hin
}