Im Beitrag “Zeitsynchronisation im lokalen Netz” beschrieb ich die Nutzung von DCF77-Empfängern als Quelle der (Atom-) Standardzeit der PTB. Die Wahl von AM-Empfängermodulen wurde ebenso behandelt wie ihr Anschluss an einen µ-Controller wie den Raspberry Pi.
Hier nun geht es um die Dekodierung des AM-Signals mit C-Software auf einem Pi.
Die Natur des AM-Signals
Zu Beginn einer jeden Sekunde einer Minute mit Ausnahme der letzten wird die Amplitude für 100 oder 200 ms auf 15% abgesenkt. Für den Rest der einen bzw. zwei Sekunden wird die volle Amplitude (100%) gesendet.
Der Digitalausgang von AM-Empfängermodulen ist üblicherweise
• HI (1, true)
für die Zeit mit 15% Amplitude und
• LO (0, false) für den Rest der Modulationsperiode von
1 bzw. 2s.
Aber es gibt es auch umgekehrt – was die Software mit einer Option,
à la - - invert handhaben sollte.
Für die Modulationsdauer gilt
• 200ms bedeutet true und
• 100ms bedeutet false.
Es gilt also, folgende Informationen zu erlangen und zu
dekodieren
a) ein 58-Bit-Telegram in jeder Minute und
b) die Startzeiten der Modulationsperioden.
a): Das Telegramm beginnt mit 14 Bit kommerzieller Geheiminformation. Die übrigen relevanten 44 Bits enthalten alle Zeit- und Datumsinformationen. Der Kode ist sehr einfach und gut publiziert.
b): Die Zeit sollte so exakt wie möglich erfasst werden – am besten als ein µs-Zeitstempel mit 20..50µs Genauigkeit. Mit einem solchen Zeitstempel und der zugehörigen Zeit können wir die Systemzeit setzen bzw. korrigieren oder Zeitsignale bzw. -antworten generieren – also DCF77 als redundante oder auch einzige Zeitquelle nutzen.
So kommt es letztendlich auf das genaue Erfassen der Startzeiten an.
Erfassen (sampling) des AM-Signals
Drei Wege sind denkbar
A) Eingabe des Empfängersignals im 1ms-Zyklus,
B) eine Interrupt-Prozedur für beide Signalflanken oder
C) das Ausnutzen der Fähigkeiten von pigpiod.
Mit einem Laufzeitsystem oder Framework mit SPS-artigen Zyklen, wie in Raspberry for remote services (see publications) beschrieben und in allen unseren µC-Steuerungsprojekten eingesetzt, ist der Ansatz A) möglich. Andererseits möchten wir letztlich DCF77 als Ersatz von oder Redundanz zu NTP einsetzen. Hier muss die Genauigkeit und ggf. die Synchronität um mindestens eine Größenordnung besser sein als der jeweils schnellste Zyklus. Diesen zur Zeiterfassung einzusetzen verbietet sich im Allgemeinen. (Für eine DCF77-Uhr für Menschen würde es genügen.)
Eine Sequenz von zeitgestempelten Ereignissen für die spätere Auswertung in einem eigenen thread aufzuzeichnen ist per se ein guter Ansatz. Und ein Interrupt-Handler (B) könnte dies. Die Raspberry-Prozessoren haben ein Interrupt-System, das Flanken-Interrupts für jeden GPIO bietet. Die Handhabung aber ist nicht einfach und die enthaltende (organisierende) Anwendung braucht sudo-Privilegien.
Ein eingängliche, verständliche C-Lösung mit Pi-GPIO-Interrupts ist kaum
zu finden. Einige Bibliotheken oder Frameworks verwenden einen schnellen
Abtast-thread + interthread/interprocess-Kommunikation – und nennen
das Ganze “interrupt”.
Nun, der Ansatz ist OK. Es ist ein bisschen A) “in schneller” und das, was C)
perfekt und nahtlos unterstützt. Es als Interrupt zu verkaufen, ist jedoch
Etikettenschwindel.
Ich bin ein großer Fan der pigpiod library von Joan N.N. Siehe die betreffenden Kapitel und die Literaturliste in der oben erwähnten Publikation. Wenig überraschend nutze ich auch hier die Fähigkeiten einer Bibliothek, die ich eh auf jedem Pi einsetze.
Erfassen der Modulationsflanken mit pigpiod
pigpiod ist wie gesagt unser bevorzugter Ansatz für IO mit dem Raspberry und auch der einzige den wir hier für echte Steuerungsanwendungen nutzen. pigpiod definiert einen Server oder daemon, der alles initialisiert und (nur) die verwendeten GPIOs mit all ihren möglichen Funktionen steuert. Dieser Server muss mit sudo gestartet werden und kann dann unbegrenzt laufen. Auf allen Raspberry Pis, wo wir dies installierten, erledigen wir den Start ab Einschalten mit einem (sudo) crontab-Eintrag:
@reboot /usr/local/bin/pigpiod -s 10
Die Option -s 10
lässt pigpiod in einem 10µs-Zyklus, statt
default 5µs, laufen. Für alle unsere bisherigen Anwendungen waren
20µs Verzögerung bzw. Genauigkeit von Signalen und Zeitstempeln ausreichend.
(Die schnellste Einstellung wäre 1µs.)
Programme, die (process) IO machen, kommunizieren einfach mit dem
daemon über
• socket (wie im Projekt
GPIO mit Java,
• pipe (hier nie genutzt) oder
• mit einem Satz von C-Funktionen (die das socket
interface nutzen).
Bei pigpiod kann man eine callback-Funktion für die Flanke(n) eines Eingangs-GPIO setzen:
set_mode(thePi, dcfGpio, PI_INPUT); // make dcfGpio input
if (dcfPUD <=PI_PUD_UP) // Raspberry Pi's pull up is sufficient for open
set_pull_up_down(thePi, dcfGpio, dcfPUD); // collector output stages
dcf77callbackID = callback(thePi, // register dcf77receiveRec
dcfGpio, EITHER_EDGE, &dcf77receiveRec); // as callback function
Das ist semantisch ganz nahe an Interrupts. Aber hierbei vermeiden wir
sämtliche Komplikationen (und Gefahren) von Interrupts und bekommen
einen 32 Bit Zeistempel für jede Flanke mit 1µs Auflösung und
etwa 15µs Genauigkeit (mit -s 10
, s.o.) als Extra:
/** The actual respectively last modulation period data received. */
dcf77recPerData_t dfc77actRecPer;
/** DCF77 receive recorder.
*
* This is a pigpiod callback function for .... (in other file)
*/
void dcf77receiveRec(int pi, unsigned gpio, unsigned level, uint32_t tick){
uint32_t const dif = tick - dfc77actRecPer.tic;
dcf77lastLevel = dcfInvert ? !level : level; // handle AM receiver polarity
if (dcf77lastLevel) { // in 15% modulation, i.e end of last period
dfc77actRecPer.per = dif; // calculate duration
dfc77ringBrecPer[dfc77ringBrecWInd] = dfc77actRecPer; // put in FiFo
++dfc77ringBrecWInd; // signal FiFo write respectively period end
dfc77actRecPer.tic = tick; // start of new period
} else { // full (100%) signal
memcpy(dfc77actRecPer.sysClk, lastSysClk, 12); // log time for current
memcpy(lastSysClk, stmp23 + 11, 12); // log time text for next period
dfc77actRecPer.cbTic = get_current_tick(pi); // call back tick
dfc77actRecPer.tim = dif;
dfc77actRecPer.sysClk[12] = dfc77actRecPer.sysClk[13] = '\0';
}
} // dcf77receiveRec(int, 2*unsigned, uint32_t)
Die gezeigte call back-Funktion erzeugt für jede Modulationsperiode eine
Aufzeichnung (record) dfc77actRecPer vom Typ dcf77recPerData_t und tut ihn
in einen
FiFo (ring buffer).
Die Struktur (struct) sieht so aus (Auszug mit gekürzten Kommentaren):
/** Data for one received DCF77 AM period. */
typedef struct {
/** The piogpiod time tick in µs. */
uint32_t tic;
/** The system time as text hh:mm:ss.mmm at period start for logging convenience.
*
* It is the time when the call back function for start of 15% AM is called.
*/
char sysClk[14]; // text provided by rasProject_01 framework
/** The system tick at second's start in µs.
*
* It is the time of entering the call back function for 15% AM.
* By the pigpiod event to callback entry delay cbTic should be later than tic. By
* their difference an apparent system-DCF77 time difference has to be shortened.
*/
uint32_t cbTic;
/** The 15% modulation's duration in µs. */
uint32_t tim; // 100ms: FALSE, 200ms: TRUE
/** The period's duration in µs. */
uint32_t per; // 1s: second's end, 2s: minute's end
} dcf77recPerData_t;
Mit
• der Startzeit .tic, idealerweise zu jeder “Atom-“ Sekunde,
• der Periode .per, idealerweise entweder 1s oder 2s, und
• der Modulationszeit .tim, idealerweise entweder 100ms oder 200ms,
aufgezeichnet über eine Minute haben wir alles um die DCF77-Zeit zu
dekodieren und zu nutzen.
Anmerkung zum Diskriminieren und Dekodieren
Mit der Kette von records – einer pro Modulationsperiode, gestört oder
korrekt – ist der erste Schritt die Werte .tim und .per. zu diskriminieren.
Wir definieren hierzu eine Struktur für (uint32_t) Wertbereiche:
/** Values for discrimination of duration. */
typedef struct {
/** Index.
* The lowest value in a chain (array or enum) shall get the index 0. */
unsigned i;
/** Value.
* This is the upper bound value of the range. <br />
* The highest value in a chain (array or enum) is implied as the maximum
* possible / measurable value. */
uint32_t v;
/** Name.
* This is a short (5 character max.) name of the discrimination range. */
char n[6];
/** Charakter.
* This is a recognisable character for the discrimination range, unique
* for the complete chain. It may be used for (narrow) logs instead of .n */
char c;
} durDiscrPointData_t;
Für die Modulationsperiode (.per) und die Modulationszeit (.tim) definieren wir damit jeweils fünf Bereiche, davon zwei gute und drei schlechte (darunter, undefiniert dazwischen und darüber):
/* Discrimination values for the 15% modulation time. */
durDiscrPointData_t timDiscH[5] = {
{0, 60000, "spike", 's'}, // 0 .. 59.9 ms modulation spike
{1, 128999, "false", 'F'},
{2, 130000, "undef", 'u'},
{3, 228000, "true", 'T'},
{4, MXUI32, "error", 'e'} };
/* Discrimination values for the modulation period. */
durDiscrPointData_t perDiscH[5] = {
{0, 960000, "below", 'b'},
{1, 1040000, "secTk", 'S'}, // seconds tick
{2, 1960000, "undef", 'u'},
{3, 2040000, "minTk", 'M'}, // end minute tick (last two seconds)
{4, MXUI32, "error", 'e'} };
Abhängig von den eingesetzten AM-Empfängern, ihrer Qualität, ggf. zusätzlichen Filtern (nicht empfohlen) usw. können weitere Sätze mit anderen Grenzen bereitgestellt werden. Die Array-Länge ist aber offensichtlich immer 5. Die ermöglicht eine optimale Driskriminator-Funktion, die den zum Wert passenden Eintrag liefert:
/* Discriminating a value.
* @param table discrimination table of length 5. With other lengths the
* function will fail. Must not be null.
* @param value the number to be discriminated
* @return the lowest table entry with value < table[i].v
*/
durDiscrPointData_t * disc5(durDiscrPointData_t table[], uint32_t const value){
if (value < table[1].v) {
if (value < table[0].v) return & table[0];
return & table[1];
}
if (value < table[3].v) {
if (value < table[2].v) return & table[2];
return & table[3];
}
return & table[4]; // Note: table[4].v (MXUI32 above) is never used
} // disc5(durDiscrPointData_t const *, uint32_t const)
Nach der Diskriminierung von Modulationszeit (.tim) und -periode (.per)
gibt es vier mögliche gute Ergebnisse:
• F.S false . Sekunden-Tick
• T.S true . Sekunden-Tick
• F.M false . Minuten-End-Tick (2 s)
• T.M false . Minuten-End-Tick (2 s)
Alle anderen Kombinationen sind schlecht bzw. gestört. Ohne Störungen
haben wir eine Kette logischer Werte (true oder false), denen wir Indizes
bzw. Sekunden-Nummern von 0 bis 58 zuordnen und dekodieren können.
Und idealerweise würde jeder jeder Zeitstempel (.tic) einer
Modulationsperiode eine “echte- Atom-“ Sekunde markieren.
Aber leider sehen wir Ausfälle Störungen aller Arten, die auch schon mal mehr als 59 Modulationsperioden pro Minute vortäuschen. Mit guten Empfängern und geeigneten Antennenstandorten sind Störungen recht selten – aber dennoch, sie passieren.
Störungen mit pigpiod filtern
Die gewöhnliche Störung bei AM-Rundfunk sind kurze – oft unter 1 ms – Störungen entweder als Signalausfall oder Störsignale anderer Sender oder elektrischer/elektronischer Geräte. Im Fall von DCF77 täuschen letztere i.a. anstelle der 15% Modulation volles Signal vor. Neben kurzen Spikes kommen auch länger anhaltende Störungen (40 ms und mehr) vor.
Kurze Spikes bei binären Signalen können mit einfachen “de-bouncing” Algorithmen ausgefiltert werden, die alle Signalwechsel unterhalb einer minimalen Dauer ignorieren. Und, dankenswerterweise, bietet pigpiod genau diese Fähigkeit für Eingänge, die man mit call back-Funktionen überwacht.
if (dcfGlitch > 30000) dcfGlitch = 0;
set_glitch_filter(thePi, dcfGpio, dcfGlitch);
Mit dem Einsatz dieser glitch-Filter kommt der Aufruf der call back-Funktion natürlich später, aber pigpiod setzt den Zeitstempel – wie nicht anders zu erwarten – immer noch korrekt.
Der Wert für den Spike- bzw. glitch-Filter kann zwischen 0 (kein Filter)
und 30.000 µs gesetzt werden. Nach vielen Experimenten (über Wochen) halten
wir ihn unter 10ms.
dcf77onPi --glitch 9999 >> logs/dcf77test32cAnG.log &
Es zeigte sich, dass höhere Werte gelegentlich über viele Sekunden hinweg ausfiltern. Also könnte man fälschlicher zur Diagnose “Gar kein Empfang” oder “Empfänger Aus” kommen.
Zusätzliches Filter für Modulationsstörungen
Der zusätzliche Nutzen von 30ms (das Maximum) Filterzeit verglichen mit den bevorzugten 10 ms ist bei guten Empfängern marginal. Und außerdem könnte beispielsweise die Verkürzung einer Modulationsperiode unter z.B. 400ms durch einen Spike mit dem glitch-Filter von pigpiod sowieso nicht ausgefiltert erden. Genau so etwas kommt durchaus vor und führt dann meistens zu zwei Perioden, wo eine sein sollte. Ohne Zusatzmaßnahmen ist dann alles Indizieren und Dekodieren für die betroffene Minute verloren.
Ein recht einfaches zusätzliches Filter (Software) hiergegen ist es, das erste Vorkommen von ?.b zurückzuhalten und diese (zu kurze) Periode in korrekter Weise zur nächsten hinzu zu addieren. Hiermit können die meisten solchen Fälle repariert werden. Der Log-Auszug (Fr, 29.01.2021 14:16) zeigt eine Minute, die man ansonsten in Sekunde 41 verloren hätte:
### .tic .tim sec dis .per system time -cbck decode
DCF77 1.839.060.809 184080 36: T.S 1000492 14:17:35.138 -.185
DCF77 1.840.061.301 84361 37: F.S 998913 14:17:36.238 -. 85
DCF77 1.841.060.214 80060 38: F.S 998333 14:17:37.138 -. 80
DCF77 1.842.058.547 186510 39: T.S 1004452 14:17:38.133 -.187
DCF77 1.843.062.999 79390 40: F.S 1002533 14:17:39.238 -. 80
DCF77 1.844.065.532 28300 41: s#b 53930 14:17:40.135 ignor= // spiky |
DCF77 1.844.119.462 174560 41: T=S 994862 14:17:41.087 -. 29 29.mm. // v
DCF77 1.845.060.394 180051 42: T.S 1004343 14:17:41.232 -.180
DCF77 1.846.064.737 79360 43: F.S 996692 14:17:42.233 -. 79
DCF77 1.847.061.429 181361 44: T.S 1003933 14:17:43.136 -.181 Day:Fr
DCF77 1.848.065.362 175890 45: T.S 994952 14:17:44.235 -.176
DCF77 1.849.060.314 82120 46: F.S 1003043 14:17:45.234 -. 82
DCF77 1.850.063.357 74190 47: F.S 997652 14:17:46.134 -. 75
DCF77 1.851.061.009 82901 48: F.S 1002913 14:17:47.131 -. 83
DCF77 1.852.063.922 78590 49: F.S 997822 14:17:48.136 -. 79 dd.01.
Diese “gemäßigt intelligente” Filter rettet die meisten solcher Fälle (welche mit guten Empfängern eh schon selten sind). Es kommt vor, dass dieser Algorithmus versagt, wo das Addieren der gestörten Periode zur vorangehenden geholfen hätte. Nun wäre es möglich auch das noch zu implementieren. Aber nach unseren Beobachtungen ist das weder der Mühe noch der verminderten Lesbarkeit des Programms wert.
Schlussbemerkung
Das Empfangen und Dekodieren von DCF77 haben wir auf einem Pi mit preiswerten AM-Empfängermodulen implementiert. Mit dem Empfangs-Tick und dem zugehörigen Paar “call back-Tick + call back-Systemzeit” haben wir die Werte zur Korrektur bzw. dem Setzen der Systemzeit beieinander. Somit haben wir eine (redundante) Zeitquelle, die NTP ersetzen oder ergänzen kann. Damit kann man auch verteilte Systeme in einem abgeschlossenen Netz synchronisieren oder dort einen (zusätzlichen) NTP-Server bereitstellen.
Der ungekürzten Kode findet sich im SVN-Repository weinert-automation.de/svn/.
Literatur
Weinert, Albrecht, “Zeitsynchronisation im lokalen Netz”, 2021 post
Weinert, Albrecht, “Raspberry for remote services”, 2018 – 2020 publication
N.N, Joan, “pigpio Daemon” 2020 documentation
PTB “DCF77” 2004 German survey
Kommentare
Möchten Sie einen Kommentar abgeben — auch gerne auf Englisch? Besuchen Sie die issue-Seite auf GitHub.Zum Kommentieren benötigen Sie ein GitHub-Konto.
Dieser Beitrag hat einen gemeinsamen Kommentarbereich mit timeSyncLocNet_de.html.