DCF77-Auswertung

für Funkuhren


Dieses Modul dient zum Decodieren des DCF77-Signals. Es wird alle 10 ms mit dem Zustand des DCF-Empfängers aufgerufen und füllt eine Datenstruktur, solbald die Zeit empfangen wird. Bei guter Empfangslage ist das zu jedem Minutenbeginn der Fall. Bei abgesenktem Träger wird eine 0 an die Routine übergeben — dies ist während der ersten 100ms bzw. 200ms einer Sekunde der Fall. Ansonsten wird eine 1 übergeben.

Wie bei vielen meiner anderen Module auch, braucht die DCF77-Auswertung einen Basistakt von 10 Milli­sekunden. Alle 10ms wird der Port, an dem das DCF-Signal empfangen wird, abgefragt. Ein extra Interrupt wird nicht benötigt. In meinen Anwendungen wird der Port-Zustand in einer Variablen gemerkt, und zwar in einer Interrupt-Routine, die alle 10ms ausgeführt wird. Dieser 10-Milli­sekunden-Interrupt erledigt noch andere Dinge wie zum Beispiel die Taster-Abfrage oder den Countdown.

Die eigentliche Auswertung des gemerkten Port-Zustands geschieht nicht auf Interrupt-Ebene, sondern auf normaler Anwendungs­ebene. Dadurch erzeugt die DCF77-Auswertung praktisch keine zusätzliche IRQ-Last. Allerdings muss ausserhalb des DCF-Moduls eine Zeitbasis geschaffen werden. Wie bereits erklärt, kann ein und dieselbe 10ms-Basis für viele verschiedene Module benutzt werden, so auch für das DCF77-Modul.


Benutzerschnittstelle


Typedefs

time_t
Diese Struktur stellt die Zeit dar und hat folgende Komponenten:
uint8_t second
Sekunde: 0...59
uint8_t minute
Minute: 0...59
uint8_t hour
Stunde: 0...23
uint8_t day_of_week
Wochentag: 1=Montag, ..., 7=Sonntag
uint8_t day
Tag: 1...31
uint8_t month
Monat: 1...12
uint8_t year
Jahr: 0...99
uint8_t mesz
Mitteleuropäische Sommerzeit? 1=Sommerzeit, 0=Winterzeit
dcf77_t
Diese Struktur fasst DCF77-Daten zusammen. Hier nur die wichtigsten Komponenten, die nach aussen hin interessant sind:
time_t *ptime
Bei korrekt empfangener Zeit wird diese nach *ptime kopiert. Wichtig: *ptime muss ausserhalb des dcf77-Moduls initialisiert werden. Ansonsten wird die Zeit nicht hineinkopiert!
uint8_t time_changed
Wenn die neue Zeit nach *ptime kopiert wird, wird time_changed auf 1 gesetzt. Die Anwendung kann time_changed wieder auf 0 zurücksetzten. So kann erkannt werden, daß ein neues Zeit/Datums-Paket angekommen ist.

Objekte

dcf77_t dcf77
Ein globales dcf77-Objekt

Funktionen

void dcf77_tick (dcf77_t *dcf, uint8_t poll)
Diese Funktion wird alle 10 Millisekunden aufgerufen und übergibt den Zustand des Ports, an dem die DCF77-Hardware angeschlossen ist. Dadurch bleibt der DCF-Decoder frei von Hardware-Abhängig­keiten. Zudem kann der Wert für poll in einer Interrupt-Routine erhelten werden, während die Ausführung dieser vergleichsweise aufwändigen Funktion auf normaler Anwen­dungs­ebene erfolgen kann. Im ersten Teil einer Sekunde, der Absenkung des DCF-Trägers, wird poll=0 übergeben; ansonsten ein Wert ungleich Null.

Makros

Beim Übersetzen des Quellcodes kann man verschiedene Defines setzen, um die Codeerzeugung zu beeinflussen. Dazu gibt man beim Aufruf von gcc die Option -D unmittelbar gefolgt vom Makroname an; hier ein Beispiel: avr-gcc ... -DMAKRONAME ...

TIME_MESZ
Falls dieses Makro definiert ist, wird die MESZ-Information des DCF77-Protokolls für mittel­europäische Sommer-/Winterzeit ausgewertet.


Anwendungsbeispiel

für avr-gcc 4.7


Für andere Maschinen sieht das ähnlich aus, nur daß Definition und Implemen­tierung einer ISR natürlich maschinen­abhängig sind.

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

#include "dcf77.h"

// Merkt alle 10 Millisekunden den Portzustand des DCF-Ports
uint8_t volatile dcf77_bit = 0xff;
static void job_dcf77_10ms (void);

// Alle 10 Millisekunden aus einer ISR heraus aufrufen
// Macht alle 10ms einen Schnappschuss vom DCF-Port (hier an Port B1).
void job_dcf77_10ms (void)
{
    uint8_t bit = 0;

    if (PINB & (1 << PB1))
        bit = 1;

    dcf77_bit = bit;
}

// Definition von ISRs etc
ISR (...)
{
    ...
    // Alle 10ms
    job_dcf77_10ms();
    ...
}


time_t time;

int main (void)
{
    // Initialisierung: IRQs aktivieren, Hardware initialisieren, etc.
    // ...

    dcf77.ptime = &time;

    // Hauptschleife
    while (1)
    {
        uint8_t bit = dcf77_bit;

        // Andere Aufgaben in der Hauptschleife
        // ...

        // Sind 10ms vergangen, also dcf_bit in { 0, 1 }?
        if (bit < 2)
        {
            // Ja:
            // Schnappschuss zurücksetzen
            dcf77_bit = 0xff;

            // Die Zeit tickt 10ms weiter
            dcf77_tick (&dcf77, bit);

            // DCF-Abgleich erfolgreich?
            if (dcf77.time_changed)
            {
                // Neue Zeit ist in time angekommen!
                // Merker zurücksetzen
                dcf77.time_changed = 0;

                // Zeit in dcf77.ptime auswerten
                // ...
            }
        } // DCF

        // Andere Aufgaben in der Hauptschleife
        // ...

    } // Hauptschleife
} // main


dcf77.h


#ifndef DCF77_H
#define DCF77_H

#include <stdint.h>

typedef struct
{
    uint8_t second;
    uint8_t minute;
    uint8_t hour;
    uint8_t day;
    uint8_t day_of_week;
    uint8_t month;
    uint8_t year;
#ifdef TIME_MESZ
    uint8_t mesz;
#endif // TIME_MESZ
} time_t;

typedef struct
{
    // In newtime wird die neue Zeit-Information aufgebaut
    time_t newtime;

    // Bei korrekt empfangener Zeit wird die Zeit nach *ptime kopiert.
    //
    // !!! *ptime MUSS AUSSERHALB DES DCF77-MODULS GESETZT WERDEN  !!!
    // !!! ANSONSTEN WIRD DIE ZEIT NICHT HINEINKOPIERT             !!!
    time_t *ptime;

    // Wenn die neue Zeit nach *ptime kopiert wird, wird time_changed
    // auf 1 gesetzt. Die Anwendung kann time_changed wieder auf
    // 0 zurücksetzten. So kann die Anwendung erkennen, daß ein neues
    // Zeit/Datums-Paket angekommen ist.
    uint8_t time_changed;

    // Für interne Verwendung in dcf77.c

    // Parity wird on the fly mitberechnet
    uint8_t parity;

    // Es gab einen Fehler: error != 0
    uint8_t error;

    // Wertigkeit des aktuellen BCD-Bits: 1, 2, 4, 8, 10, 20, 40, 80.
    uint8_t bitval;

    // Dauer der aktuellen Sekunde in Einheiten von 10ms (1 Tick)
    uint8_t ticks;

    // Letzter poll-Wert vom Aufruf von dcf77_tick() zur
    // Flankenerkennung.
    uint8_t poll;

    uint8_t byteno;
} dcf77_t;

// dcf77 wird definiert in dcf77.c
extern dcf77_t dcf77;

/* Obwohl es nur eine dcf77-Struktur gibt, wird nicht direkt
   mittels dcf77.xxx darauf zugegriffen, sondern durch dcf->xxx.
   Grund ist, daß auf AVR durch indirekten Zugriff ein kürzerer
   Code möglich ist als durch direkten Zugriff. Dazu darf gcc die
   Adresse von dcf77 nicht zu sehen bekommen; ansonsten macht er
   auch bei dcf->xxx einen direkten Zugriff. */

extern void dcf77_tick (dcf77_t *dcf, uint8_t poll);

#endif /* DCF77_H */


dcf77.c


Wie gesagt ist dies ein Beispiel für avr-gcc 4.7, denn es wird der neue Address-Space Qualifier __flash verwendet, der erst ab Version 4.7.0 zur Verfügung steht.

Möchte man eine ältere Compiler­version verwenden und dabei nicht darauf verzichten, daß die verwendete Tabelle im Flash abgelegt wird, dann muss man sich mit Konstrukten wie PROGMEM und pgm_read_byte behelfen wie sie z.B. die AVR Libc zur Verfügung stellt.

#include "dcf77.h"

#if defined (__GNUC__) && defined (__AVR__) && !defined (__FLASH)
#warning You need avr-gcc 4.7 or higher to use __flash!
#warning ...locating dcf_byteno[] in RAM
// Notlösung: Standard-C wird dcf_byteno[] im RAM speichern
#define __flash
#endif // avr-gcc

dcf77_t dcf77;

#define PARITY 0xff
#define IGNORE 0xfe
#define NEWVAL 0x10

// Adressdifferenz von .N zu .minute in time_t

#define X(N) ((uint8_t)(&dcf77.newtime.N - &dcf77.newtime.minute))

#define X0 X(minute)
#define X1 X(hour)
#define X2 X(day)
#define X3 X(day_of_week)
#define X4 X(month)
#define X5 X(year)
#define X6 X(mesz)

/* Codiert die DCF-Bits 17..58 bzw. 21...58 (je nachdem, ob MESZ
   empfangen werden soll) und deren Position in time_t.
   Low-Nibble     : Offset relativ zu .minute
   Bit 4 (NEWVAL) : Neuer Wert fängt an, d.h. die Speicherposition
                    muss 1 Byte voranschreiten. */
static const __flash uint8_t dcf_byteno[] =
{
#ifdef TIME_MESZ
    // Zeitzone: MEZ/MESZ (Winter-/Sommerzeit): 2 Bits an Offset 6
    NEWVAL | X6, X6,
    // Ankündigung Schaltsekunde
    IGNORE,
    // Parity Zeitzone (immer 1)
    PARITY,
#endif // TIME_MESZ

    // Minute: 7 Bits an Offset 0
    NEWVAL | X0, X0, X0, X0, X0, X0, X0,
    // Parity Minute
    PARITY,
    // Stunde: 6 Bits an Offset 1
    NEWVAL | X1, X1, X1, X1, X1, X1,
    // Parity Stunde
    PARITY,
    // Tag: 6 Bits an Offset 2
    NEWVAL | X2, X2, X2, X2, X2, X2,
    // Wochentag: 3 Bits an Offset 3
    NEWVAL | X3, X3, X3,
    // Monat: 5 Bits an Offset 4
    NEWVAL | X4, X4, X4, X4, X4,
    // Jahr: 8 Bits an Offset 5
    NEWVAL | X5, X5, X5, X5, X5, X5, X5, X5,
    // Parity Datum
    PARITY
};

static void dcf77_start_bit (dcf77_t*, uint8_t);
static void dcf77_store_bit (dcf77_t*, uint8_t);
static void dcf77_end_bit (dcf77_t*, uint8_t);

/*
   Dies ist der Einstiegspunkt für das DCF-Modul. Die Funktion wird alle
   10ms (ein Tick) aufgerufen.

   poll = 0: Träger ist abgesenkt (erste 100ms oder 200ms einer Sekunde)
   poll = 1: Voller Träger */

void dcf77_tick (dcf77_t *dcf, uint8_t poll)
{
    // Diese Funktion wird zu jedem Tick -- also alle 10ms --
    // aufgerufen. Ticks um 1 weiterzählen.

    uint8_t ticks = 1 + dcf->ticks;

    // Überlauf der Ticks, z.B. bei schlechtem Signal oder nicht-
    // angeschlossenem Empfänger. Ticks bleiben dann bei 255 stehen.

    if (ticks == 0)
        ticks = UINT8_MAX;

    dcf->ticks = ticks;

    // poll normieren auf 0 oder !0
    poll = !!poll;

    // Flanke?
    if (poll != dcf->poll)
    {
        // Ja. poll-Wert zur Erkennung der nächsten Flanke merken.
        dcf->poll = poll;

        if (poll)
        {
            // Flanke 0->1: Bit-Ende 100ms oder 200ms nach Sekundenstart
            dcf77_end_bit (dcf, ticks);
        }
        else
        {
            // Flanke 1->0: Bit-Anfang, Sekundenstart
            dcf77_start_bit (dcf, ticks);
            dcf->ticks = 0;
        }
    }
}


/* Wird aufgerufen bei Flanke 1->0 des DCF-Signals.
   Start eines neuen Bits bzw. Start/Ende einer Sekunde.
   TICKS sind die Anzahl Ticks seit der vorherigen 1->0 Flanke
   und ist ca. 100 (also etwa 1s) oder nach dem ausgelassenen
   59. Bit ca. 200 (also etwa 2s). */

static void dcf77_start_bit (dcf77_t *dcf, uint8_t ticks)
{
    // Aktuelle Sekunde 0..59 um 1 weiterzählen
    uint8_t second = dcf->newtime.second;

    if (second < 60)
        second++;

    dcf->newtime.second = second;

    if (ticks > 200-10 && ticks < 200+10)
    {
        // 59. Sekunde: Der Träger wurde für ca. 200 Ticks (2s)
        // nicht abgesenkt. Nun beginnt eine neue Absenkung, d.h.
        // eine neue Minute beginnt.

        dcf->newtime.second = 0;

        if (dcf->error == 0 && second == 59)
        {
            // Während einer ganzen Minute wurden korrekte
            // Bits empfangen --> aktuelle Zeit/Datum speichern

            dcf->time_changed = 1;

            // !!! FÜR .ptime SIEHE DEN KOMMENTAR IN dcf77.h     !!!

            if (dcf->ptime)
                *(dcf->ptime) = dcf->newtime;
        }

        // Eine Minuten-Marke bewirkt ein Neubeginn der
        // DCF-Auswertung; daher das Fehler-Flag zurücksetzen.

        dcf->error = 0;
    }
    else
    {
        // Eine "normale" Sekunde ist vergangen. Deren Länge
        // ist ca. 100 Ticks (1s). Andernfalls ist's ein Fehler.

        if (ticks < 100-10 || ticks > 100+10)
            dcf->error = 1;
    }
}


/* Wird aufgerufen bei Flanke 0->1 des DCF-Signals.
   Ende der Trägerabsenkung, d.h. Bitende. TICKS ist
   die Dauer der Trägerabsenkung, d.h. die Anzahl der
   10ms-Ticks seit der letzten 1->0 Flanke des DCF-Signals
   und ist für gültige 0-Bits ca. 10 (100ms) und für
   gültige 1-Bits ca. 20 (200ms). */

static void dcf77_end_bit (dcf77_t *dcf, uint8_t ticks)
{
    uint8_t bit = 0;

    if (ticks >= 10-3 && ticks < 10+3)
        // Ein 0-Bit dauert ca. 10 Ticks (100ms)
        bit = 0;
    else if (ticks >= 20-3 && ticks < 20+3)
        // Ein 1-Bit dauert ca. 20 Ticks (200ms)
        bit = 1;
    else
        // Alle anderen Bitlängen sind ungültig --> Fehler
        dcf->error = 1;

    if (!dcf->error)
        dcf77_store_bit (dcf, bit);
}


/* Speichert BIT in der DCF-Struktur */

static void dcf77_store_bit (dcf77_t *dcf, uint8_t bit)
{
    // Nur Bits 17..58 bzw. 21..58 decodieren. Die aktuelle Sekunde,
    // vermindert um 17/21, dient als Index ins Feld dcf_byteno[],
    // wo Informationen dazu stehen, wie BIT zu
    // behandeln und wo es ggf. zu speichern ist.

    const uint8_t nbits = sizeof (dcf_byteno) / sizeof (dcf_byteno[0]);

    uint8_t i = dcf->newtime.second - (59-nbits);

    if (i >= nbits)
        return;

    i = dcf_byteno[i];

#ifdef TIME_MESZ
    if (i == IGNORE)
        return;
#endif // TIME_MESZ

    dcf->parity ^= bit;

    if (i == PARITY)
    {
        // Partity-Test. Diese Funktion wird nur aufgerufen mit
        // .error = 0. Falls parity != 0 ist, liegt also ein
        // Parity-Fehler vor. Ansonsten bleibt .error = 0 unverändert.
        dcf->error = dcf->parity;

        // Parity für nächstes Bit zurücksetzen.
        dcf->parity = 0;

        return;
    }

    // Low-Nibble -> Offset des Bytes (relativ zu .minute),
    // zu dem das Bit gehört
    uint8_t offset = i & 0xf;
    uint8_t *d = &dcf->newtime.minute + offset;

    uint8_t val = *d;

    // Wertigkeit des Bits in BCD: 1, 2, 4, 8, 10, 20, 40, 80
    uint8_t bitval = dcf->bitval << 1;
    if (bitval == 0x10)
        bitval = 10;

    // Ein neuer Wert fängt an
    if (i & NEWVAL)
        val = 0, bitval = 1;

    dcf->bitval = bitval;

    // Falls DCF-Bit gesetzt ist: seine Wertigkeit zum
    // Zielbyte hinzuaddieren

    if (bit)
        val += bitval;

    *d = val;

    // Kleiner Konsistenztest: Testet Einer auf gültiges BCD-Format
    if (bitval == 8  &&  val >= 10)
        dcf->error = val;
}