Auswahl
Projekte
Impressum
lang:en   lang:de

 
 
 
time... Das praktische DCF77-Signal hat es mir angetan. Diesmal soll aber keine Uhr direkt mit dem Signal versorgt werden, sondern ein NTP-Server in meinem kleinen Netz. Mangels anderer brauchbarer Schnittstellen liefert dieser DCF77 Empfänger das aufbereitete Signal über den USB an den Rechner weiter.

Was am Ende rauskommen soll

 
Dargestellt ist links oben die Auswerteelektronik und unten recht der DCF77-Signalempfänger

Planung ersetzt Zufall durch Irrtum

Am Anfang war geplant, im Microcontroller die komplette Uhrenfunktion unterzubringen. Da ich prinzipiell solch eine DCF77-Uhr bereits gebaut und programmiert hatte, erschien mir das als nicht sonderlich schwierig.
Allerdings zeigen die anderen Uhren die empfangene Zeit mittels LEDs oder andern Anzeigen direkt für das menschliche Auge an. Dieses Projekt aber soll die Zeit an einen Rechnerverbund weiterleiten. Und dabei ergeben sich ganz andere Anforderungen an die Qualität der Zeit.
Zu allem Übel kommt noch hinzu, dass man immer damit rechnen muß, daß die Empfangsqualität nicht dauerhaft gesichert und auch nicht immer von hoher Qualität ist.
Gemäß dem Spruch "Bei schönem Wetter kann jeder Segeln", habe ich dann verschiedene Verfahren ausprobiert, um auch aus einem schlechten, nur hin und wieder verfügbaren DCF77-Signal eine brauchbare Zeitreferenz zu erzeugen. Dabei kam es zu den aberwitzigsten Situationen, in denen der Programm-Code Amok lief, weil ich wieder mal irgendeine blöde Nebenbedingung vergessen hatte. So wuchs der Code immer mehr an, um die vielen Ausnahmebehandlungen abzufangen und unter allen Umständen ein zeitkontinuierliches, jitterfreies Zeitsignal zu liefern.
Als der Code dann etwa 90% des verfügbaren Flashes beanspruchte und immer noch die Routinen zur Behandlung der Schaltjahre, Schaltsekunden und Sommerzeitumstellung nicht enthielt (die den Fall abdecken sollten, daß ausgerechnet in der Stunde oder Minute vor dem besonderen Ereignis kein brauchbarer Empfang herrscht) habe ich aufgegeben.
Übrig geblieben ist nun die reine Empfangsfunktion. Die eigentliche DCF77-Dekodierung muss nun doch der angeschlossene Rechner erledigen. Dessen Resourcen sind auch nicht so begrenzt wie in meinem Mikrocontroller.
Hauptfunktion des Controllers ist es nun, aus dem DCF77-Signal die richtigen Schlüsse zu ziehen. D.h. auch aus einem gestörten oder teilweise nicht verfügbaren Signal etwas brauchbares zu erzeugen, was einem hilft, über diese Phasen ohne neue DCF77-Daten hinwegzukommen. Das ist insbesondere die Bereitstellung:
  • eines hochgenauen Sekundenintervals
  • eines hochgenauen Sekundensignals
Klingt beides fast gleich. Das eine soll aber helfen, daß die Zeitreferenz über einen längeren Zeitraum nicht wegdriftet. Das andere ist die Phasenlage des Startzeitpunkts einer jeden Sekunde. Hört sich alles trivial an, weil beides das DCF77-Signal liefert. Sicher, das DCF77-Signal schon, aber nicht jeder DCF77-Empfänger! Der Spaß beginnt, wenn das DCF77-Signal mit einem Jitter von ±20ms vom Empfänger wiedergegeben wird. Da kommen am Ende die herrlichsten Effekte raus, nur keine brauchbare Uhrzeit...
Das folgende Bild zeigt die mit dem internen Timer gemessenen DCF77-Intervalle des Empfängers. Die CPU und damit der Timer werden mit einer 12,0 MHz Referenz betrieben und somit sollte eine Sekunde einen Wert von 46875 Zählern liefern. Tätsächlich sieht das aber ganz anders aus. Die gelben Kreuze zeigen jeweils einen gemessenen Wert an (Messzeit war 3000 Sekunden):
 
Die rote Linie zeigt das, was die Routine aus den Daten als Interval errechnet. Es ist keine Linie, weil ich den Abstand zu der errechneten Sollzeit mit darstelle. Mein 12,0 MHz Quarz liegt nicht ganz bei 12,0, sondern leicht darüber. Somit sind die gemessenen Intervalle alle etwas größer als die errechneten 46875 Zähler (meist so um die 46880 Zähler).
Nach langem Probieren stellte sich heraus, dass man ein brauchbares Sekundeninterval ermitteln kann, wenn man etwa 300 gemessene DCF77-Signal-Intervalle mittelt. Dann stellt sich ein sehr konstanter Wert ein. Das aber auch nur dann, wenn man vorher die vermeintlich unbrauchbaren gemessenen Intervalle aussortiert. Manchmal kommt es zu einem Sturm an Flanken auf der Signalleitung des Empfängers. Das muß man sauber aussortieren, sonst ist alles wieder zum Teufel. Zum Glück hilft hier die Hardware des Microcontrollers. Die Zeitpunkte der Flanken müssen nicht mühsam in Software ermittelt werden oder über Interrupte, sondern die Hardware selber speichert im Moment der Flanke den Wert eines frei laufenden Zählers und signalisiert durch ein Bit dieses Ereignis. Somit ist es möglich, in aller Ruhe diese Ereignisse auszuwerten, ohne Streß und hohe Anforderung an irgendwelches Echtzeitverhalten. Damit erhält man eine sehr präzise Beschreibung des Signalverlaufs und kann daraus alle erforderlichen Schlüsse ziehen. Beispielsweise ob ein Intervall zwischen zwei gleichen Flanken in ein 1-Sekunden-Raster passt oder ob es sich eher um einen Störimpuls handelt, den man verwerfen muss.
Nach dem Ermitteln der "perfekten" Sekunde, muss in einem nachgeschalteten Schritt die Phasenlage der internen Zeitreferenz an das des DCF77-Signals angeglichen werden. Wohlgemerkt an ein Signal, welches mit ±20ms fröhlich jittert. Auch hier ist mir nichts anderes eingefallen, als den gemessenen Jitter über einige Intervalle hinweg zu mitteln und dann die interne Zeitreferenz an den errechneten Ort zu verlegen. Danach folgt der Algorithmus dann in sehr kleinen Sprüngen dem Jitter, folgt somit durch einen Tiefpass hindurch der DCF77-Signalvorgabe. Fehlt das DCF77-Signal vollständig bleibt die Phase einfach stehen und dank der abgeglichenen Intervall-Länge sollte das Sekundensignal auch über einen längeren Zeitraum hinweg stabil genug bleiben.
Soweit die Theorie. Es stellte sich aber heraus, dass wenn die Phasenabweichung zwischen dem DCF77-Signal und dem internen Referenz-Takt nahe 0 ist (also ausgeregelt), die Software wiederum Amok läuft und ein ganz merkwürdiges Verhalten an den Tag legte. Nach langer Suche fand ich heraus, das in diesem Moment die Software nicht mehr zweifelsfrei feststellen konnte, welches der beiden Signale nun vor- bzw. nacheilt und somit ständig die falschen Entscheidungen traf, wohin denn nun die Phase verschoben werden müsste. Schon wieder den Zufall durch Irrtum ersetzt! Geholfen hat am Ende nur, die Phasenlage deutlich zu verschieben und dauerhaft auf konstanten Abstand zu regeln. Damit kommen die Ereignisse in einer definierten Reihenfolge, die Phasenlage kann damit zweifelsfrei ermittelt und somit immer die richtigen Schlüsse gezogen werden. Die konstante Phasenabweichung kann beim Report an den Host nachträglich wieder rausgerechnet werden.

Anbindung an den HOST Rechner

In einem anderen kleinen Projekt, die empfangene DCF77-Zeit in einen Rechner einzuspeisen, kam seinerzeit noch eine RS232-Schnittstelle zum Einsatz. Solche Schnittstellen sterben heute immer mehr aus, waren also in diesem Projekt hier keine Option. Der USB ist stattdessen heute das Mittel der Wahl. Da ich aber nun nicht gleich auch einen USB-tauglichen Microcontroller einsetzen wollte, habe ich wieder auf V-USB zurückgegriffen. Mit V-USB wird ein einfacher ATmega-Microcontroller mit ein paar extern Bauteilen zu einem USB-Lowspeed-Device. Da in diesem Projekt nur sehr wenig Daten übertragen werden müssen, reicht diese Funktionalität vollkommen aus.

Hardware

Die Bauteile sind in einer sehr überschaubaren Anzahl vorhanden und das Löten dauert rund eine halbe Stunde. Okay, okay, es sind SMD-Bauteile beteiligt und die sind nicht jedermanns Sache. Aber hey! DIL ist out. Mit etwas Ruhe, Geduld und viel "Solderwick" sollte sich auch SMD löten lassen. Immerhin sind die Widerstände und Kapazitäten 0805er Bauweise. Also eigentlich riesig, oder? Nur die CPU ist etwas kritisch. Aber mir hilft in so einem Fall immer viel Licht, ein sehr feiner Lötkolben und eine dicke Lupe.
 
Oberseite, CAD Unterseite, CAD
CAD Bild
CAD BILD

So sieht es aufgebaut aus

 

Schaltplan

Die Schaltpläne liegen im PostScript-Format vor:

Layout

Hier die zugehörige EAGLE-Layout-Datei

Stückliste

Diese Bauteile gilt es zu besorgen
Typ Wert Bauform Menge Name(n)
CPU Atmega8 TQFP32 1 IC1
CRYSTAL 12MHz TC26 1 XT1
IC SFH610A DIL4 1 IC2
DIODE BAV99 SOT23 2 D1 D4
DIODE Z3V6 RM2.54 2 D2 D3
LED rot RM2.54 1 D6
LED grün RM2.54 1 D5
LED gelb RM2.54 1 D7
CAP 100n C0805 4 C1 C2 C3 C7
CAP 27p C0805 2 C4 C5
CAP 10u RM2.54 1 C6
RESISTOR 10k R0805 4 R4 R6 R7 R9
RESISTOR 2k2 R0805 1 R11
RESISTOR 360R R0805 3 R10 R5 R8
RESISTOR 5k6 R0805 1 R3
RESISTOR 68R R0805 2 R1 R2
CONNECTOR HEADER10 (2x5) 2x5x2.54 1 X3
Ferrit-Drossel     1 L1
USB Kabel Typ A auf Drahtenden   1 z.B. A auf A Kabel durchtrennen
Gehäuse T1007 ("Soap")   1 Hersteller TEKO

Was braucht es sonst noch?

  • DCF77-Empfänger
  • Gehäuse für diesen Empfänger (ich habe das "PP 23sw" von Segor electronics benutzt)
  • Kabel zur Signalübertragung
Hinweis: Im Prinzip ist der DCF77-Empfänger egal. Er muss derzeit nur ein positives Signal liefern, d.h. der Beginn der Sekunde wird durch die steigende Flanke angezeigt. Andernfalls müsste die Firmware angepasst werden.

Firmware

Was gibt es zur Firmware zu sagen? Nicht allzu viel, aber:
  • Man sollte in selber geschriebenen Interruptroutinen in Assembler nicht vergessen, das Statusregister zu sichern
  • Man sollte ebenfalls nicht vergessen, konkurrierende Zugriffe auf interne 16 Bit Register zu verhindern

Beides hat mich rund 6 Wochen beschäftigt, weil es nur sporadisch zu einem Fehlverhalten des Programms führte. Da sucht man sich echt 'nen Wolf...

Da die V-USB-Routinen recht harte Echtzeitanforderungen haben, benutze ich fast keine Interrupte. Nur das atomare Auslesen der Referenz-Sekunde verwendet noch einen. Damit der wiederum die V-USB-Routinen nicht stört, habe ich ihn in Assembler realisiert und extrem kurz gemacht.
Alles andere läuft in einer Endlosschleife und einer Zustandsmaschine und fragt ständig die diversen Statusbits ab. Hier kommt es einem zugute, dass der Atmega über brauchbare Hardware verfügt die einem viel Arbeit abnimmt.

Mitten rein in diese Endlosschleife schlägt immer wieder der USB, also Anfragen vom Host. Da die Uhrenfunktion aber Dank Hardwareunterstützung zeitunkritisch ist, stört das nicht weiter.

Pro Anfrage des Hosts meldet die Uhr zwei bis dahin aufgelaufene Ereignisse: Start der Referenz-Sekunde und das letzte empfangene Bit aus dem DCF77-Datenstrom. Da diese beiden Ereignisse zeitlich nicht zusammenfallen (das Bit kennt man immer erst, wenn das gerade aktive DCF77-Signal endet) sind pro Sekunde mindestens zwei Anfragen vom Host zu stellen. Die Firmware puffert keines dieser Ereignisse, sondern überschreibt das bisherige Ereignis gnadenlos mit den nächsten Daten. Immerhin meldet die Firmware in so einem Fall, dass mindestens ein DCF77-Bit verpasst wurde.

Jede Antwort an den Host besteht aus 8 Bytes und hat den folgenden Inhalt:

NameBitsBedeutung
flags8 Bit 4: 1 = eine neue Sekunde seit der letzten Abfrage hat begonnen
Bit 1: 1 = mindestens ein Bit des DCF77-Datenstroms ist verloren
Bit 0: 1 = der Wert in bit (DCF77 bit) ist gültig
bit8 das zuletzt empfangene Bit aus dem DCF77 Datenstrom, wenn das flags Bit 0 = 1 ist
0: empfangen wurde ein 0 Bit
1: empfangen wurde ein 1 Bit
8: empfangen wurde ein SYNC
15: empfangen wurde ein ungültiges Bit
stamp_offset16 Zählerdifferenz zum Beginn der laufenden Sekunde
cur_phase16 Aktuelle Phase zwischen dem Startzeitpunkt der Referenz-Sekunde und dem DCF77-Signal
cur_interval16 Timer-Zähler für eine Sekunde

Das Bit 4 in flags ist notwendig, weil es zu dem Fall kommen kann, dass der Beginn einer neuen Sekunde bereits stattgefunden hat, jedoch wegen der laufenden USB-Abfrage das Ereignis noch nicht in den erzeugten Report eingeflossen ist. In diesem Fall kann der Host aus stamp_offset und cur_interval erkennen, dass eine neue Sekunde bereits begonnen hat.
Wie das alles gemeint ist:

  ---------------------------------------------- time flow -------------------------------------------->

            |<----------- cur_interval (equal to one second) ----------------->|
  ref ------|------------------------------------------------------------------|-------------------------
  dcf ------------------------------------|--------------------------------------------------------------
                                          |---------- cur_phase -------------->|--- stamp_offset ---->|
                                                                                                      ^
                                                  this is the point of time we got in the report _____|

ref = Ereignisse der internen Referenz-Sekunde
dcf = Ereignisse des DCF-Signals

Die tatsächliche Zeit ist also cur_phase + stamp_offset, denn die interne Referenzzeit eilt um cur_phase dem DCF-Signal nach.

Wie wird der Zeitpunkt ermittelt, der in stamp_offset geliefert wird:

 USB  _____________________XXXXXXXXXXXXXXX_XX_________________________
 read _______________________XX_______________________________________
                           |<-------------->| ~1.08ms
                         ->|-|<-150us...250us
                           ->|-|<- ~150us to generate the answer

D.h. die gesamte Übertragung (=Aktivität auf dem USB) dauert etwa 1ms. Etwa 150µs...250µs nach dem Beginn der Aktivität auf dem USB wird intern begonnen die Anwort zu generieren. Das bedeutet stamp_offset referenziert diesen Zeitpunkt. Danach braucht die Firmware noch etwa 150µs um das Datenpaket zusammen zustellen. Der Rest der Zeit wird verbraucht, um die Daten dann an den Host zu übertragen.

Und Host-seitig?

Hier hat es sich bewährt, vor und nach der Abfrage via USB einen Zeitstempel zu ziehen und mit der Annahme weiterzurechnen, dass stamp_offset von der USB-Uhr genau in der Mitte der beiden Zeitstempel liegt. Das ist natürlich fehlerbehaftet und dieser Fehler ist schwer zu berechnen, da er vermutlich auch noch von den sonstigen Aktivitäten auf dem USB abhängt, wann die tatsächliche Übertragung relativ zur Systemzeit stattgefunden hat. In meinem System hat sich der NTPD aber nur über einen Jitter von einigen hundert Microsekunden beklagt. Für meine Zwecke ist das genau genug. Evtl. könnte ein Kernel-Treiber genauere Zeitstempel ermitteln, es müsste sich nur jemand hinsetzen, diesen Treiber zu schreiben (Freiwillige vor...).

Erstellen der Firmware

Um die Firmware zu bauen braucht man einen GNU AVR Crosscompiler. Ich habe die GCC-Version 4.3.2 zusammen mit der AVR-C-Bibliothek 1.6.2 und den Binutils 2.19 verwendet. Weiterhin braucht man die V-USB-Quellen, die ich in der Version 20081126 verwendet habe. Die Quellen der Firmware und die V-USB-Konfiguration können von hier und hier geladen werden. Ein paar Anpassungen im Makefile sind sicherlich notwendig, weil ich mit ptxdist das ganze Projekt bauen lasse. Wer das ganze ptxdist-Projekt haben möchte, möge sich via Email bei mir rühren. Wer die Firmware nicht bauen möchte, kann auch die fertige Binärdatei von hier laden.

Anbindung an den NTP Daemon

Das Quellpaket des NTP-Daemons bringt viele Uhrentreiber mit. Mein erster Gedanke war daher, dass es nicht so schwer sein kann, dort ein geeignetes Beispiel zum Abschreiben zu finden. Prinzipiell stimmt das auch. Leider habe ich aber ein paar Nebenbedingungen zu erfüllen, die wiederum der NTP-Daemon nicht erfüllt. Beispielsweise muss ich meine Uhr zweimal pro Sekunde abfragen, damit ich weder das DCF77-Sekunden-Signal, noch das Sekunden-Referenz-Signal verpasse. Der NTP-Daemon ruft aber die Treiber nicht mit dieser Rate auf. Aber es gibt ja noch Threads. Davon riet man mir aber ab, weil ich dann der erste wäre, der den NTP-Daemon mit Threads unter POSIX betreiben würde. Und dieses Neuland wollte ich dann nicht auch noch betreten.
Was blieb, war der SHM-Treiber. Ein separates Programm trägt die Uhrzeit zusammen und liefert diese an den NTP-Daemon über einen shared memory Bereich. Somit kann ich in meinem Host-seitigen Programm tun und lassen, was ich will um die Uhrzeit über USB zu ermitteln und trotzdem benutzt der NTP-Daemon die gelieferte Zeit, ohne diesen irgendwie ändern zu müssen.

Quellen und Konfiguration

Das Quellarchiv für den SHM-Treiber (clockread) kann von hier geladen werden. Alles was jetzt noch zu tun ist, ist den NTPD zu starten und vorher in seiner Konfigurationsdatei /etc/ntpd.conf den externen Treiber hinzuzufügen:
[...]
server 127.127.28.0 mode 0 prefer
fudge 127.127.28.0 stratum 0
[...]
Danach genügt es, die USB-Uhr anzustecken und zu schauen, ob der Kernel gewillt ist, damit zu arbeiten. Es sind übrigens keine Kernel-Treiber notwendig. Die gesamte Kommunikation wird über die libusb abgewickelt. Mein Kernel meldet folgendes:
[...]
usb 5-1: new low speed USB device using uhci_hcd and address 7
usb 5-1: configuration #1 chosen from 1 choice
hiddev96: USB HID v1.01 Device [kreuzholzen.de DCF77-Clock] on usb-0000:00:1d.3-1
Um die Daten der DCF77-Uhr auszuwerten und dem NTPD als Quelle zuzuführen genügt es nun das Programm clockread zu starten. Dieses Programm gibt via Syslog einige Hinweise über seinen Zustand aus und der NTPD kann mittels ntpq-Kommando dabei beobachtet werden, ob und wie er die Zeit aus dem shared memory Treiber verwendet.
[me@host]~$ ntpq -p localhost
     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
 LOCAL(0)        .LOCL.          10 l   57   64  377    0.000    0.000   0.004
*SHM(0)          .SHM.            0 l   31   64  377    0.000    0.486   0.287