Raspberry-Pi-Projekte: DCF77-Empfang und Decodierung

Prof. Jürgen Plate, Jonas Goll, Tobias Müller, Xiao Wang, Dirk Faulenbach

Raspberry Pi: DCF77-Empfang und Decodierung

Allgemeines

Der Langwellen-Zeitzeichensender DCF77 in Mainflingen versorgt die meisten funkgesteuerten Uhren in Westeuropa mit der genauen Uhrzeit. Die Bezeichnung "DCF77" rührt von dem zur internationalen Identifikation zugewiesenen Rufzeichen her. Seine bitweise im Sekundenrhythmus gesendeten Zeitzeichen übertragen die mitteleuropäische Zeit bzw. mitteleuropäische Sommerzeit. Das Trägersignal von 77,5 kHz ist in Frequenz und Phasenlage mit der steuernden primären Atomuhr synchronisiert und besitzt deshalb nur geringe Abweichungen von der Sollfrequenz (über den Tag weniger als ± 2*10-12). Es kann daher auch ohne Auswertung der Zeitinformation als Eichfrequenz verwendet werden.

Die Zeitinformationen werden als digitales Signal zusätzlich zur Normalfrequenz übertragen. Das geschieht durch Amplitudenmodulation des Signals (Absenken des Trägers auf etwa 15%) im Sekundentakt. Der Beginn der Absenkung liegt jeweils auf dem Beginn der Sekunden 0 bis 58 innerhalb einer Minute. In der 59. Sekunde erfolgt keine Absenkung, wodurch die nachfolgende Sekundenmarke den Beginn einer Minute kennzeichnet. Darauf kann der Empfänger synchronisiert werden. Die Dauer der Amplitudenabsenkungen steht jeweils für den entsprechenden Binärwert der Sekunde: 100 ms für logisch "0", 200 ms für logisch "1". Damit stehen innerhalb einer Minute 59 Bit Information zur Verfügung. Die Bits (Zählung begint bei 0) haben folgende Bedeutung:

BitBedeutung der Werte
0 Start einer neuen Minute (ist immer „0“)
1 – 14 seit Ende 2006: Wetterinformationen der Firma MeteoTime
sowie Informationen des Katastrophenschutzes

Die Sekundenmarken 15 bis 19 enthalten zudem Informationen über Unregelmäßigkeiten des Senderbetriebs (Rufbit zum Alarmieren der Mitarbeiter der Physikalisch-Technischen Bundesanstalt), über die Zeitzone und zur Ankündigung von Beginn und Ende der Sommerzeit und Schaltsekunden. Die Bits 17 und 18 zeigen an, ob sich die Angaben ab Bit 20 auf MEZ (Winterzeit) oder MESZ (Sommerzeit) beziehen. Es gibt nur zwei Kombinantionen: "01" oder "10":

BitBedeutung der Werte
15Rufbit
16"1": Am Ende dieser Stunde wird MEZ/MESZ umgestellt.
17"0": MEZ (Winterzeit), "1": MESZ (Sommerzeit)
18"0": MESZ (Sommerzeit), "1": MEZ (Winterzeit)
19"1": Am Ende dieser Stunde wird eine Schaltsekunde eingefügt.

Ab der 20. bis zur 58. Sekunde wird die Zeitinformation für die jeweils nachfolgende Minute seriell in Form von BCD-Zahlen übertragen, wobei jeweils mit dem niederwertigen Bit begonnen wird. Zur Absicherung der Daten werden Paritätsbits (gerade Parität) verwendet. Die Kodierung des Wochentages erfolgt gemäß ISO 8601 bzw DIN EN 28601, wonach der Montag der erste Tag einer Woche ist und der Sonntag der siebte Tag.

BitBedeutung
20Beginn der Zeitinformation (ist immer „1“)
21Minute
(Einer)
Bit für 1
22Bit für 2
23Bit für 4
24Bit für 8
25Minute
(Zehner)
Bit für 10
26Bit für 20
27Bit für 40
28Parität Minute
29Stunde
(Einer)
Bit für 1
30Bit für 2
31Bit für 4
32Bit für 8
33Stunde
(Zehner)
Bit für 10
34Bit für 20
35Parität Stunde
36Kalendertag
(Einer)
Bit für 1
37Bit für 2
38Bit für 4
39Bit für 8
40Kalendertag
(Zehner)
Bit für 10
41Bit für 20
42WochentagBit für 1
43Bit für 2
44Bit für 4
45Monatsnummer
(Einer)
Bit für 1
46Bit für 2
47Bit für 4
48Bit für 8
49Monatsnummer
(Zehner)
Bit für 10
50Jahr
(Einer)
Bit für 1
51Bit für 2
52Bit für 4
53Bit für 8
54Jahr
(Zehner)
Bit für 10
55Bit für 20
56Bit für 40
57Bit für 80
58Parität Datum
59keine Sekundenmarke

Um die korrekte Uhrzeit zu erhalten, muss der Empfang mindestens im Schnitt eine halbe Minute laufen, damit sich der Empfänger auf den Anfang der neuen Minute synchronisieren kann. Danach folgen mindestens 36 Sekunden zum Empfang des Zeittelegramms inklusive der Paritätsbits. Spätestens nach 120 Sekunden störungsfreien Empfangs stehen alle nötigen Informationen zur Verfügung. Die mit übertragenen Paritätsbits erlauben nur eine Fehlererkennung der empfangenen Information, keine Fehlerkorrektur, und können bei gestörtem Empfang keine fehlerfreie Erkennung gewährleisten. Um eine zuverlässige Zeitinformation zu erhalten, ergreift man zusätzliche Maßnahmen, zum Beispiel indem die Zeitinformation über mehrere aufeinanderfolgenden Minuten ausgewertet wird.

DCF77 - Empfängermodule

Für den DCF77-Empfang wwerden meist fertige DCF-Empfangsmodule verwendet. Nicht bei jedem Modul kann man "out of the box" ein auf dem RasPi verarbeitbares Signal empfangen. Einige Module benötigen eine zusätzliche Beschaltung. In der Beschreibung von Reichelt steht beispielsweise, dass man den Ausgan des Moduls gleich (ohne Kollektor-Widerstand) an den entsprechende PIN des Controllers anschließen kann. Das klappt natürlich nur, wenn der Controller intern einen Pullup-Widerstand schalten kann. Auch liefern die Module nur einen sehr geringen Ausgangsstrom. Bei den ersten Versuchen traten deshalb häufig Decodierungs-Fehler auf. Alle Module verwenden den Baustein U2775B. Das folge Bild zeigt eine Prinzipschaltung:

Getestet wurden Module von ELV, Pollin, Reichelt und Conrad. Die Module von Reichelt und Pollin sind am Ausgang nur mit maximal 5 µA belastbar. Einzig und alleine das Conrad-Modul konnte direkt betrieben werden. Die folgende Schaltung löst bei allen anderen Modulen das Problem. Hinter das DCF77-Modul wird ein Komparator geschaltet, der zum einen die genügend Ausgangsstrom liefern kann und der zum anderen die Einstellung der Schaltschwelle erlaubt, so dass auch hier keine Probleme mit den Logikpegel auftreten.

Die Diode am Ausgang des Komparators kompensiert ggf. auftretende Offset-Spannungen. Zusätzlich wurde auf der Adapterplatine noch die Spannungsversorgung des Moduls und eine Anzeige-LED untergebracht. Die Leitungen zum Raspberry Pi werden an Pin 2(+5 V), Pin 6 (GND) und Pin 11 (GPIO 17) der Steckerleiste des Raspi angeschlossen. Statt Pin 11 kann auch jeder andere freie GPIO-Pin verwendet werden - das Programm unten ist ggf. anzupassen.

Das folgende Foto zeigt einen Musteraufbau auf Lochrasterplatte mit dem Pollin-Modul. Die Schaltung und ein Platinenlayout stehen im Eagle-Format zur Verfügung (siehe Links weiter unten).

Da die Module unterschidliches Pin-Layout haben, ist der Stecker für das DCF77-Modul ggf. unterschiedlich zu verdrahten. Das aktuelle Layout ist für das Pollin-Modul konzipiert. Da das Layout für die Tests mehrfach verändert wurde, sind Forward- und Back-Annotiation mit dem Schaltplan nicht mehr gegeben. Die Pinbelegung der einzelnen Module ist in der folgenden Tabelle gelistet (PON = Modul einschalten):

AnbieterPONDATAVCCGND
Pollin1243
Conrad-321
Reichelt2143
ELV-213
Beim Conrad-Modul ist auf Pin4 das invertierte
DATA-Signal herausgeführt.

Software für den DCF77-Empfang

Der Datenausgang des DCF77-Moduls ist beim vorgestellten
Prototyp an GPIO 17 (Pin 11 der Stiftleiste) angeschlossen.

Zur Überprüfung der Funktion des DCF77-Moduls wurde ein kleines Shellscript zur Darstellung des empfangenen Signals verwendet, das die Interpretation des empfangenen Signals erlaubt. Für einen realen Empfang ist es nicht unbedingt geeignet. Es sollte sich nach dem Start eine ähnliche Ausgabe wie im folgenden Listing ergeben (da immer ziemlich viele Nullen kommen wurden die Zeilen etwas gekürzt):

  ...
0000 ... 0000000000000000000001111111111111111111110 *1*
0000 ... 00000000000000000000111111111110 *0*
0000 ... 00000000000000000000000000000000000001111110 *0*
0000 ... 000000000000000000000000000000001111111110 *0*
0000 ... 0000000000000000000000000000000001111111111111111111110 *1*
0000 ... 000000000000000000000000011111110 *0*
0000 ... 0000000000000000000000000000000000111111110 *0*
0000 ... 000000000000000000000000000000001111111110 *0*
0000 ... 000000000000000000000000000000000011111111110 *0*
0000 ... 000000000000000000000000000000001111111111111111111110 *1*
0000 ... 00000000000000000000000111111110 *0*
0000 ... 0000000000000000000000000000000000111111110 *0*
0000 ... 0000000000000000000000000000000000111111111111111111110 *1*
0000 ... 000000000000000000000000111111110 *0*
0000 ... 000000000000000000000000000000000011111111111111111110 *1*
0000 ... 00000000000000000000000001111111111111110 *1*
0000 ... 000000000000000000000011111111110 *0*
   ...
Man kann in etwa die Periodendauer von "0"- und "1"-Signalen erkenne und die Unterscheidung ist sogar mit dem recht langsamen Script möglich. Die dekodierten Bits werden im Sekundentakt empfangen. Es sollten eigentlich keine "X"-Bits zu sehen sein (einige, wenige schaden nicht). Sollte der Takt unregelmäßg sein oder häufig "X"-Bits auftreten, befindet sich entweder eine Störquelle in der Nähe des DCF77-Empfängers oder das Modul empfängt das Signal nur schwach. In solchen Fällen muss das Modul bzw. die Antenne neu ausgerichtet oder in einem störungsfreien Bereich aufgestellt werden. Das Shellscript kann über den Link unten heruntergeladen werden. Gegebenfalls muss die Nummer des GPIO-Pins geändert werden. Auch ist die Unterscheidung von "0" und "1" sehr grob und sie hängt vom CPU-Takt ab.

Für das Auslesen der DCF77-Information wurde ein Python-Programm geschrieben, das an dieser Stelle nur ausschnittweise besprochen wird. Im Programm ist einen Variable DEBUG definiert, die auf "True" gesetzt werden kann, um das Programm bei seiner Arbeit zu beobachten. Ist DEBUG eingeschaltet wird jede Sekundede die Systemzeit, die DCF77-Bitnummer und der Wert des Bits ausgegeben. Ist DEBUG auf "false" gesetzt erfolgt nur am Schluss die Asgabe von Datum und Uhrzeit.

Vor der Verwendung des GPIO-Pins muss dieser konfiguriert werden. Zunächst wird der Mode "GPIO.BOARD" eingestellt (Ansprechen der Ports über die Pinnummern) und dann Pin 11 als Input festgelegt:

# RPi.GPIO Layout verwenden (wie Pin-Nummern)
GPIO.setmode(GPIO.BOARD)

# Pin 11 (GPIO 17) auf Input setzen
GPIO.setup(11, GPIO.IN)
Danach wird zunächst auf einen längeren Low-Wert (länger 1 Sekunde) gewartet. Dieser Wert gibt an, dass es sich um den Anfang einer neuen Bit-Sequenz handelt, weil ja das 59. Bit immer auf Ruhepegel bleibt. Es kann nun mit der Zählung ab dem 0. Bit begonnen werden.
   ...
dt = datetime.now() - start_time
start_time = datetime.now()
   ...
if dt.seconds == 1:
    begin_bit_found = True
elif old_state == GPIO.HIGH and begin_bit_found and dt.microseconds < 110000:
    begin_bit_found = False
    begin_found = True
    counter = 0
    bit_seq[counter] = 0
   ...
Nachdem der Sratpunkt für die Zählung gefunden wurde, werden alle folgenden Bits in im Array "bit_seq" abgespeichert. Die Variable "counter" zählt dabei die ankomenden Bits. Die Unterscheidung, ob es sich um ein "High"- oder "Low"-Bit handelt, wird anhand der Dauer des "High"-Impulses getroffen (Achtung: Die Schwelle hängt vom Prozessortakt ab.). Dauert der Impuls weniger als 110 ms, wird von einem "Low"-Wert des Bits ausgegangen. Bei einer Dauer größer 110 ms wird "High" angenommen.
   ...
        elif begin_found and old_state == GPIO.HIGH:
           counter = counter + 1
           bit_seq[counter] = 0 if int(dt.microseconds) < 110000 else 1
           if DEBUG:
              print str(time.time()) + "   " + str(counter) + " " + str(bit_seq[counter])
   ...
Sobald das 58. Bit eingelesen worden ist, kann die Sequenz dekodiert werden. Die Dekodierung findet in der Funktion decodeDCF77() statt. Die Funktion liefert ein Objekt vom Type "date" zurück. Dieses Objekt wird nun in eine Zeichenkette umgewandelt. Die Formatvorgabe entspricht dem Format wie es von dem Systemkommando date zum Setzen der Zeit akzeptiert wird. Zum Glück ist bei Linux dieses Kommando sehr flexibel und nicht an die starre Urform der Datums- und Uhrzeitangabe gebunden. Die Funktion stützt sich Ihrerseits auf zwei Funktionen, eine zur Umrechnung von vier Bit BCD-Darstellung in eine Dezimalziffer und die zweite zur Berechnung der geraden Parität.
# Unterprogramm zum Umwandeln BCD nach Dezimal
# b0 bis b3 haben als Wert entwder 0 oder 1
def bcd2dez(b3, b2, b1, b0):
    result = b0 + b1 * 2 + b2 * 4 + b3 * 8
    return result


# Paritaetspruefung (gerade P.)
# Es werden der Start- und Ende-Index im Array angegeben
def parity(start, end, bit_arr):
    i = start
    ipar = 0
    while i <= end:
        ipar = ipar + bit_arr[i]
        i = i + 1
    return (ipar % 2)
Neben dem Decodieren überprüft die folgende Funktion die Korrektheit der Bits durch die Paritätsbits. Im Fehlerfall liefert die Funktion lediglich den Wert "None". Der Rückgabewert wird im späteren Programmfluss geprüft. Achtung: Bei der Jahreszahl muss 2000 addiert werden, da der Wert 0 dem Jahr 2000 entspricht. Die Zählung des DCF77-Systems beginnt somit erst ab dem Jahr 2000.
# Unterprogramm zum Decodieren des DCF77-Signals
def decodeDCF77(bit_seq):
    if bit_seq[0] != 0 or bit_seq[20] != 1:
        return None
    # Minute (BCD) berechnen
    minute = bcd2dez(bit_seq[24], bit_seq[23], bit_seq[22], bit_seq[21])
    minute = minute + 10 * bcd2dez(0, bit_seq[27], bit_seq[26], bit_seq[25])
    if parity(21, 27, bit_seq) != bit_seq[28]:
        return None

    # Stunde (BCD) berechnen
    hour = bcd2dez(bit_seq[32], bit_seq[31], bit_seq[30], bit_seq[29])
    hour = hour + 10 * bcd2dez(0, 0, bit_seq[34], bit_seq[33])
    if parity(29, 34, bit_seq) != bit_seq[35]:
        return None

    # Jahr (BCD) berechnen
    year = bcd2dez(bit_seq[53], bit_seq[52], bit_seq[51], bit_seq[50])
    year = year + 10 * bcd2dez(bit_seq[57], bit_seq[56], bit_seq[55], bit_seq[54]) + 2000
    # Monat (BCD) berechen
    month = bcd2dez(bit_seq[48], bit_seq[47], bit_seq[46], bit_seq[45]) + bit_seq[49] * 10
    # Tag (BCD) berechnen
    day = bcd2dez(bit_seq[39], bit_seq[38], bit_seq[37], bit_seq[36])
    day = day + 10 * bcd2dez(0, 0, bit_seq[41], bit_seq[40])
    if parity(36, 57, bit_seq) != bit_seq[58]:
        return None

    # Umrechnen ins datetime-Format
    second = 0
    microsecond = 0
    dcf77_time = datetime(year, month, day, hour, minute, second, microsecond)
    return dcf77_time
Im Anschluss an das Setzen der aktuellen Zeit mittels date wird die Zeitangabe noch in die externe Realtime-Clock übertragen (siehe Projekt RTC). Die Realtime-Clock übernimmt mit dem Befehl hwclock -w die aktuelle Systemzeit in ihr internes RAM und zählt dort die Zeit batteriegepuffert weiter.
   ...
        # 59. Sekunde abwarten
        time.sleep(1)
        # Date/Time-String formatieren
        time_date_str = "\"{:4d}-{:02d}-{:02d} {:02d}:{:02d}:00\"".format(
        dcf77_time.year, dcf77_time.month, dcf77_time.day, dcf77_time.hour, dcf77_time.minute)
        # Systemzeit setzen
        os.system("date -s " + time_date_str + " > /dev/null")
        os.system("hwclock -w")
        # Ausgabe der Zeit
        print str(dcf77_time)

Nicht vergessen, das Ausführungsrecht zu setzen (chmod +x dcf77-reader.py)! Da die Übernahme der DCF77-Informationen doch bis zu zwei Minuten dauern kann und da die Systemuhr bzw. die Realtime-Clock relativ genau sind, reicht es die Info der Atomuhr nur gelegentlich aufzurufen. Wenn der Raspberry Pi in der Regel längere Zeit läuft oder sogar im 24-Stunden-Betrieb, kann der Aufruf von dcf77-reader.py per crontab erfolgen. Wegen der Übergabe der Zeitinformation ans Betriebssystem benötigt dcf77-reader.py Root-Rechte, was beim Eintrag, etwa in die Datei /etc/crontab berücksichtigt werden muss, zum Beispiel:

30 6  * * 7   root   /home/pi/dcf77/dcf77-reader.py
Bitte beachten: In der "normalen" Crontab-Datei fehlt die "root"-Spalte.

Programmaufruf "hängt"

Wenn das DCF-77-Modul keinen Empfang hat, bleibt das Programm "hängen". Das wäre nicht weiter schlimm, wenn der Prozess nicht ständig die Hardware abfragen und so Rechenzeit aufnehmen würde. Es kommt hinzu, dass ja täglich ein neuer, ebenfalls hängender Prozess hinzukommt. Langsam aber sicher würde der RasPi immer träger arbeiten und irgendwann geht dann gar nichts mehr.

Also muss dafür gesorgt werden, dass das Empfängerprogramm nach einer gewissen Zeit abgbrochen wird. Dies kann relativ einfach nach folgendem Schema erreicht werden. Im Python-Programm wird ein Timer gestartet und nach fünf Minuten das Programm zwangsweise abgebrochen. Dazu wird einerseits die entsprechende Bibliothek mittels import signal eingebunden und andererseits am Ende der Funktionsliste ein Signalhandler hinzugefügt, der das Programm beendet:

# Signalhandler fuer den Timeout
def to_handler(signum, frame):
  print "Timeout!"
  exit(1)
Am Anfang des Hauptprogramms wird der Signalhandler mit dem Timer verknüpft und der Timeout auf fünf Minuten (300 s) eingestellt:
# Register the signal function handler
signal.signal(signal.SIGALRM, to_handler)
# Define a timeout for your function
signal.alarm(300)
Das war es dann auch schon. Bei brauchbarem Empfang ist das Programm nach sätestens drei Minuten fertig und endet ganz normal. Klappt es nicht mit dem Empfang, wird es nach fünf Minuten zwangsweise beendet.

Alternativ-Version mit Verbesserungen

Im März 2016 kamen einige E-Mails von Dirk Faulenbach mit einer Fehlerkorrektur (bei der Minute gab es einen Tippfehler: statt der Feldkomponenten 24, 23, 22, 21 stand da 24,23,22,24). Und dann hat er weitere Verbesserungen eingebracht. Ich gebe die wichtigsten Passagen seiner Mails hier wieder:

Mit großem Interesse habe ich ihren Beitrag "Projekt-DCF77" bei mir zuhause umgesetzt, dabei aber die "wiringPi"-Bibliothek eingesetzt. Sie bietet unter anderem die Möglichkeit auf einen Pegelwechsel zu warten, ohne dabei die CPU zu beanspruchen. Lange Rede kurzer Sinn: mit "gpio wfi 0 both" kann ich genau den in Ihrem Beispiel benutzten Port für den DCF abfragen und das Skript bis zum Pegelwechsel "warten" lassen. Das Script funktioniert scheinbar sauber und die CPU-Auslastung ist praktisch bei 0.

Leider stellte sich heraus, dass trotz Paritätsprüfung die Zeit selten aber trotzdem zu oft falsch eingelesen wird. Ich habe dazu das Script weiterentwickelt und einen mehrfachen Lauf eingebaut, der das Ergebnis des vorherigen Laufes überprüft und nur bei Übereinstimmung die Zeit freigibt. Ansonsten wird die zuletzt gelesene Zeit als Referenz genommen und ein neuer Lauf gestartet. Verglichen werden Jahr, Monat, Tag und Stunde. Demnach sollte das Script nicht vor einem Stundenwechsel gestartet werden. Die Wahrscheinlichkeit, dass zweimal hintereinander der gleiche Empfangsfehler vorliegt, ist nahezu Null.

Des weiteren habe ich einen Python-Befehl gefunden, der auf einen Pegelwechsel wartet: GPIO.wait_for_edge(38, GPIO.BOTH). Zur Kontrolle habe ich den PIN 40 als Ausgang geschaltet und mit dem DCF Signal synchronisiert. Nun signalisiert eine über einen Vorwiderstand angeschlossene LED die Sync-Aktivität sowie den sauberen Empfang. Zuletzt wird die gemessene Zeit per echo-Befehl in eine Datei nach /home/pi/dcf77decode.txt geschrieben, sofern Debug gesetzt ist. Das Programm ist unter dcf77-reader-DF-V2.1.py abgelegt.
Dirk Faulenbach, mail1 [at] 5x20.de

Links


Copyright © Hochschule München, FK 04, Prof. Jürgen Plate und die Autoren
Letzte Aktualisierung: