|
|
|
|
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 festellen 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 |
|
|
|
|
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 (ich habe wieder den von Conrad genommen)
- 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:
| Name | Bits | Bedeutung |
| flags | 8 |
Bit 4: 1 =
eine neue Sekunde seit der letzen 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
|
| bit | 8 |
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_offset | 16 |
Zählerdifferenz zum Beginn der laufenden Sekunde
|
| cur_phase | 16 |
Aktuelle Phase zwischen dem Startzeitpunkt der Referenz-Sekunde und dem DCF77-Signal
|
| cur_interval | 16 |
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
|
|
|
|