Projekte
DCF77-Demodulation in GNU Radio
Vom dateibasierten Python-Skript zur echtzeitfähigen Blockverschaltung im GNU Radio Companion.
Dieser Artikel beschreibt die Umsetzung der DCF77-Zeitzeichen-Demodulation mit GNU Radio — der Übergang von dateibasierter Python-Verarbeitung zu einer echtzeitfähigen Streaming-Architektur.
Dateiverarbeitung vs. Echtzeitsystem
Dateibasierte Ansätze laden den gesamten Datensatz in den Speicher und ermöglichen wahlfreien Zugriff über Array-Indizes. Echtzeitsysteme verarbeiten dagegen einen kontinuierlichen Datenstrom ohne definierten Anfang oder Ende. In GNU Radio gibt es „keine Variable t für die Zeit" — nur einen Sample-Zähler. Blöcke erhalten das aktuelle Sample, kennen aber keine absolute Zeit.
Verarbeitungskette in GNU Radio
Throttle-Block – wirkt als künstlicher Flaschenhals, begrenzt den Sample-Fluss, um reale Antennenbedingungen zu simulieren und eine CPU-Überlastung bei der Dateiwiedergabe zu verhindern.

Signalaufbereitung:
- Hilbert-Transformation erzeugt IQ-Daten aus dem WAV-Eingang
- der Complex-to-Mag-Block extrahiert die Hüllkurve durch Berechnung der Sample-Beträge
- ein Tiefpassfilter glättet das Signal und ermöglicht die Dezimierung (Faktor 10, von 12.000 auf 1.200 Samples/Sekunde)
- der Threshold-Block wandelt die analogen Werte in ein binäres (0/1) Signal um
Rohsignal vs. Hüllkurve: Die blaue Kurve zeigt den heruntergemischten 77,5-kHz-Träger, die rote Kurve die durch Hilbert-Transformation und Complex-to-Mag extrahierte Hüllkurve — deutlich sichtbar die Amplitudenabsenkung in der Mitte.

Über mehrere Sekunden betrachtet zeigt die noch ungeglättete Hüllkurve die periodischen Absenkungen im Sekundentakt — deutlich verrauschter als nach der finalen Tiefpassfilterung.

Der GRC-Threshold-Block wandelt das geglättete, analoge Hüllkurvensignal mit den hier gezeigten Schwellwerten in ein binäres (0/1) Signal um.

DCF77-Bit-Erkennungsblock
Der individuelle Python-Block implementiert eine dreistufige Verarbeitung:
Stufe 1 – Schmitt-Trigger mit Hysterese: nutzt zwei Schwellwerte (0,4 und 0,6), um Rauschen zu unterdrücken. Ein Zustandswechsel erfolgt erst nach stabiler Bestätigung über mehrere Samples (Debounce).
Stufe 2 – Pulsdauermessung: fallende Flanken markieren den Pulsbeginn, steigende Flanken das Pulsende. Die Dauer berechnet sich als Dauer[ms] = Anzahl_Samples / samp_rate × 1000.
Stufe 3 – Bit-Klassifikation:
- 70–150 ms → logische „0"
- 160–300 ms → logische „1"
- Timeout-Schutz bei 400 ms
Die Ergebnisse werden als Metadaten-Tags direkt an den Stream-Samples der steigenden Flanken angehängt.
Die gemessenen Pulsdauern (204 ms bzw. 98 ms) werden direkt den Bit-Werten 1 bzw. 0 zugeordnet und als dcf77_bit-Tag an das Sample der steigenden Flanke (pulse_start) angehängt.

DCF77-Decoder-Block
Sammelt die getaggten Bits und decodiert die Protokollinformation. Die vollständige Kette ab dem Threshold-Block: Ein eigener „DCF77 Debug Tagger" führt die Schmitt-Trigger-Hysterese (Threshold_Low 0,4 / Threshold_High 0,6) und das Debounce aus Stufe 1 aus, bevor der DCF77-Decoder die Zeitinformation extrahiert und per Message Debug sowie ZMQ-Sink ausgibt.

BCD-Dekodierung: jedes Bit wird mit gewichteten Positionswerten multipliziert (z.B. Minuten-Einer: 1, 2, 4, 8; Zehner: 10, 20, 40).
Protokollelemente (Bitpositionen):
| Feld | Bitpositionen |
|---|---|
| Zeitzone (MESZ/MEZ) | 17–18 |
| Minute | 21–28 |
| Stunde | 29–35 |
| Tag | 36–42 |
| Wochentag | 42–45 |
| Monat | 45–50 |
| Jahr | 50–58 |
Frames synchronisieren sich über die 59-Sekunden-Lücke; ein Message Port gibt formatierte Zeitstrings aus.
Datenausgabe
Nachrichten werden per ZMQ an localhost:5555 übertragen und von einem externen Python-Anzeigeclient empfangen, der die dekodierte Uhrzeit darstellt.
