DDK9 RTTY Demodulation

Der Sender DDK9 des Deutschen Wetterdienstes strahlt meteorologische Daten auf mehreren Frequenzen im LW und KW Bereich aus.
In diesem Beitrag wird die Demodulation der Daten dieses Senders beschrieben.

Um das DDK9-Signal erfolgreich zu dekodieren, müssen wir die physikalischen Eigenschaften der Übertragung verstehen. Technisch gesehen handelt es sich um Rundfunktelegrafie (RTTY), die auf einer binären Frequenzumtastung basiert.

Der DWD nutzt für den Sender DDK9 das F1B-Verfahren. Das bedeutet:

  • Modulation: Frequency Shift Keying (FSK). Es gibt zwei Zustände: Mark (logisch 1) und Space (logisch 0).
  • Frequenzhub (Shift): 450Hz. Das bedeutet, die Trägerfrequenz springt zwischen zwei Zuständen hin und her, die exakt 450 Hz auseinanderliegen.
  • Symbolrate: 50Baud. Ein Bit dauert also genau 20ms.
  • Kodierung: ITA2 (International Telegraph Alphabet No. 2), besser bekannt als Baudot-Code. Dies ist ein 5-Bit-Code, der Start- und Stoppbits sowie Umschaltzeichen für Buchstaben und Ziffern verwendet.

Die Demodulation eines Kurzwellensignals wie DDK9 klingt in der Theorie simpel, ist in der Praxis aber mit physikalischen Störfaktoren konfrontiert, die im GNU Radio Flowgraph einige Herausforderungen bereithält.

Im Unterschied zur Amplitudenmodulation des DCF77-Signals, bei der lediglich die Absenkung der Signalstärke detektiert werden muss, liegt die Herausforderung bei DDK9 in der präzisen Auflösung der Frequenzverschiebung, die eine deutlich komplexere Taktrückgewinnung und Kompensation von Frequenzdrift erfordert.

Wir beginnen wieder zunächst bei der Signalbereitstellung für die Demodulation.
Das hier verwendete Signal ist eine Aufzeichnung aus KiwiSDR auf der Frequent 10100.8kHz

Das Eingangssignal ist wieder ein WAV File, also eine Datei mit rein reellen Zahlen.
Wie erwartet ist damit das Spektrum symmetrisch.

Das eigentliche Signal ist in einem der beiden Seiten.
Erste Aufgabe ist es also das Signal in die Mitte zu schieben um so die beiden Frequenzanteile für die Frequenzumtastung symmetrisch zu 0 zu erhalten.

Schaut man genauer auf das Spektrum kann man erkennen, das das Signal um ca. 1kHz verschoben ist.

Um das Signal nun in die Mitte zu verschieben, kommt der Frequency Xlating FIR Filter zum Einsatz.
Neben der Frequenzverschiebung erzeugt dieser Filter auch noch gleich einen Tiefpass, um uns von überflüssigen Frequenzen zu befreien.

Die Frequenz ist nun so eingestellt, dass die beiden Kanäle symmetrisch zu 0 sind. Bei einem Shift von 450Hz ist liegen die benötigten Frequenzen nurn bei +/- 225Hz.

Um Das Signal zu demodulieren, kommt der Quadratur-Demodulator zum Einsatz. Dieser Block wandelt die Frequenzänderungen (FSK) in ein Amplitudensignal um. Ziel ist es also, dass eine Änderung von 225Hz eine +1 Wert und -225Hz eine -1 Wert ergibt. Dazu muss GAIN entsprechend eingestellt werden.

Gain=SampleRate/(2πf)Gain = Sample Rate / (2π * ∆f)

∆f ist der halbe Frequenzhub also 225Hz. Bei einer Samplerate von 12000, wie wir sie wieder im WAV File haben, kkommen wir auf ca. 8.5. Die Formel kann man direkt im Demodulator eintragen

Mit Hilfe eines weiteren Tiefpassfilters kann man sich wieder von unnötigen Störungen befreien.

Der Quadratur-Demodulator wandelt Frequenzsprünge in Amplitudensprünge um. Das Ergebnis am Ausgang ist ein rechteckähnliches Signal.

Der Demodulationsprozess erzeugt dabei wieder hochfrequentes Rauschen. Dieses Rauschen kann bei der folgenden Verarbeitung im Bitslicer (Threshold) “Jitter” oder Fehltriggerungen verursachen.

Um Jitter zu vermeiden, kommt ein weiterer Tiefpass Filter zum Einsatz.
Da wir 50 Bits pro Sekunde (Baud) als Signal empfangen, ist das schnellstmögliche Signal (ein ständiger Wechsel von 0 und 1) eine 25 Hz Schwingung.

Eine CutOff Frequenz von 50Hz ist nun ausreichend, um das Rauschen zu reduzieren. Eine Transition Width 10Hz macht den Filter entsprechend steil. Mit der Filterung reduzieren wir nun auch die Anzahl der Samples um den Faktur 10 indem die Decimation auf 10 eingestellt wird. Das muss bei der Einstellung der Samplerate für die Folgeblöcke bedacht werden.

Wir haben nun ein schönes Signal, bei dem jeder Wechsel der Frequenz die Amplitude umschaltet.

Im nächsten Schritt machen wir aus dem Signal echte Bits. Dazu kommt der Bit-Slicer Block zum Einsatz.

Wir haben nun eine schöne Folge von Signalwechseln. Die kürzeste beträgt 20ms, wie es bei 50Baud zu erwarten ist. Noch besteht das Signal aus 1200 Samples pro Sekunde. Im nächsten Schritt ist es das Ziel nur noch 1 Sample pro Bit zu haben.

Der Symbol Sync Block ist dafür vorgesehen, diese Aufgabe zu übernehmen.
Dieser Block muss noch vor dem Bit Slicer eingesetzt werden, da er keine Bytes verarbeiten kann. Mit einer Symbolrate von 24 (samp_rate / 10 / baud) reduziert er die entsprechenden Samples.

Als Ausgang des Bit Slicers erhalten wir nun eine Folge von 0 und 1.

Bsp: …0110101010101000…

Ein Zeichen im Baudot/ITA2 Code besteht nicht aus einer festen Byte-Größe, sondern aus einer zeitlichen Abfolge von Impulsen:

  • Start-Bit: Es ist immer 1 But lang (20ms) und hat den Zustand 0 (Space)
  • Daten Bits: Immer 5 Stück (insgesamt 100ms), hier steckt die Information
  • Stopp-Bit: Es hat den Zustand 1 (Mark) ist aber 1.5mal so lang wie ein normales Bit (30ms)

Unsere Demodulation ist auf 50 Baud synchroniert, der Symbol Sync Block denkt in 20ms Schritten. Wenn nun ein Stoppbit kommt, das 30 ms lang ist, beginnt der Takt zu wandern. Ein Zeichen mit 1,5 Stoppbits daher als zwei Einsen (11) abgetastet, wobei die zweite Eins eigentlich schon die “Pause” vor dem nächsten Startbit ist.

Was bedeutet das für unsere Bitfolge, die “RYRY”-Gruppe: 011 0101010101000
(wir nehmen dabei an, dass die erste 0 das Startbit ist).

Die Sequenz sieht also wie folgt aus: 0 10101 11.
10101 ist das Zeichen Y im Baudot Code und
01010 ist das Zeichen R

Der Decoder ignoriert alle Bits solange er eine 1 sieht, Erst beim Flankenwechsel von 1 auf 0 ermittelt er die nächsten 5 Bit. und dekodiert diese. Anschließend wartet er wieder auf den 1 -> 0 Übergang.

In der RYRY Bitfolge ist das wie folgt abgebildet:

..0101001010100110101

Die RYRYRY Sequenz führt zu einem kontinuierlichen Wechsel zwischen den beiden Zuständen (Mark und Space), da die Datenbits komplette Spiegelbilder sind.
Das Signal ist damit das ideale Testsignal, um Verzerrungen festzustellen.
Bei den mechanischen Fernschreibern wurde es früher benötigt, um die Walzen von Sender und Empfänger korrekt zu synchronisieren.

Der vollständige Workflow für die Demodulation sieht nun so aus:


Hier ist der zugehörige Code für den DDK9 Baudor Decoder Block.

Python
import numpy as np
from gnuradio import gr

class ddk9_baudot_decoder(gr.sync_block):
    def __init__(self):
        gr.sync_block.__init__(
            self,
            name='DDK9 Baudot Decoder',
            in_sig=[np.int8], # Wir erwarten Bytes (0 oder 1) vom Binary Slicer
            out_sig=None      # Ausgabe erfolgt direkt im Terminal (stdout)
        )
        self.state = "IDLE"
        self.bit_buffer = []
        self.shift_state = "LETTERS" # Baudot hat zwei Ebenen: Buchstaben & Zahlen
        
        # ITA2 Dekodiertabelle
        self.LETS = {
            0b00100: ' ', 0b10111: 'Q', 0b10011: 'W', 0b00001: 'E', 0b01010: 'R',
            0b10000: 'T', 0b10101: 'Y', 0b00111: 'U', 0b00110: 'I', 0b11000: 'O',
            0b10010: 'P', 0b00011: 'A', 0b00101: 'S', 0b01001: 'D', 0b01101: 'F',
            0b11010: 'G', 0b10100: 'H', 0b01011: 'J', 0b01111: 'K', 0b10010: 'L',
            0b10001: 'Z', 0b11101: 'X', 0b01110: 'C', 0b11110: 'V', 0b11001: 'B',
            0b01100: 'N', 0b11100: 'M', 0b01000: '\r', 0b00010: '\n'
        }
        self.FIGS = {
            0b00100: ' ', 0b10111: '1', 0b10011: '2', 0b00001: '3', 0b01010: '4',
            0b10000: '5', 0b10101: '6', 0b00111: '7', 0b00110: '8', 0b11000: '9',
            0b10010: '0', 0b00011: '-', 0b00101: '\'', 0b01001: '$', 0b01101: '!',
            0b11010: '&', 0b10100: '#', 0b01011: 'BELL', 0b01111: '(', 0b10010: ')',
            0b10001: '+', 0b11101: '/', 0b01110: ':', 0b11110: ';', 0b11001: '?',
            0b01100: ',', 0b11100: '.', 0b01000: '\r', 0b00010: '\n'
        }

    def work(self, input_items, output_items):
        in0 = input_items[0]
        for bit in in0:
            if self.state == "IDLE":
                if bit == 0: # Start-Bit erkannt!
                    self.state = "DATA"
                    self.bit_buffer = []
            
            elif self.state == "DATA":
                self.bit_buffer.append(bit)
                if len(self.bit_buffer) == 5:
                    # Baudot ist LSB first (niedrigstes Bit zuerst)
                    code = 0
                    for i in range(5):
                        code |= (self.bit_buffer[i] << i)
                    
                    # Steuerzeichen prüfen
                    if code == 0b11111: # Shift to Letters
                        self.shift_state = "LETTERS"
                    elif code == 0b11011: # Shift to Figures
                        self.shift_state = "FIGURES"
                    else:
                        # Zeichen ausgeben
                        table = self.LETS if self.shift_state == "LETTERS" else self.FIGS
                        char = table.get(code, '')
                        print(char, end='', flush=True)
                    
                    self.state = "WAIT_STOP"
            
            elif self.state == "WAIT_STOP":
                if bit == 1: # Zurück in den Ruhemodus sobald Stopp-Bit da ist
                    self.state = "IDLE"
                    
        return len(in0)

Der Code liefert aus dem Testsignal die enthaltene Zeichenfolge des Wettersenders:

DDH7 DDK9

FREQUENCIES   4583 KHZ   7646 KHZ   11.8 KHZ

RYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRCode-Sprache: CSS (css)

Das ist schon mal ein schöner Erfolg. Es gibt aber noch Defizite, die im folgenden Beitrag eliminiert werden sollen.

Der Start der Datenbits wird ausschließlich vom Übergang MARK zu SPACE getriggert. Das heißt, dass sich der Datenstrom eher zufällig einrastet und leicht aus dem Tritt kommen kann. Das Stop Bit wird derzeit nicht ausgewertet.

Der Vollständigkeit halber ist hier noch der Code für den Terminal Sink Block. Einzige Aufgabe dieses Blockes war es, die Bitsequenz im Terminal auszugeben. Ein kleines Hilfsmittel, um zu prüfen, ob die erhaltene Bitfolge für eine weitere Dekodierung geeignet ist.

Python
import numpy as np
from gnuradio import gr

class blk(gr.sync_block):
    def __init__(self):
        gr.sync_block.__init__(self, name='Terminal Sink', in_sig=[np.int8], out_sig=None)

    def work(self, input_items, output_items):
        for bit in input_items[0]:
            # Schreibt die Bits einfach als 0 und 1 ins Terminal
            print(bit, end='', flush=True)
        return len(input_items[0])