Projekte

DDK9 RTTY-Demodulation mit GNU Radio — Version V2

Robuste FSK-Demodulation des DWD-Kurzwellensenders DDK9 mit Zero-Crossing-Sync, AGC2 und einem Baudot-Decoder.

DDK9 ist ein Kurzwellensender des Deutschen Wetterdienstes und überträgt meteorologische Daten per FSK (F1B) mit 225 Hz Frequenzhub, 50 Baud Symbolrate und ITA2/Baudot-Kodierung. In Teil 1 haben wir einen ersten Ansatz mit verdoppelter Baudrate und Müller&Müller-Synchronisation beschrieben. Diese Version (DDK9_V3) ersetzt diesen Ansatz durch eine robustere Lösung: 50 Baud mit Zero-Crossing-Synchronisation, AGC2, Squelch und einem verbesserten Baudot-Decoder.

WAV-Quelle → Throttle → Freq-Xlating FIR → Squelch → AGC2
  → Quad-Demod → FIR-Tiefpass → Polarität → Symbol-Sync
  → Binary Slicer → Baudot-Decoder
WAV-Quelle → Throttle → Freq-Xlating FIR → Squelch → AGC2
  → Quad-Demod → FIR-Tiefpass → Polarität → Symbol-Sync
  → Binary Slicer → Baudot-Decoder

Signalparameter DDK9

Ein RTTY-Zeichen besteht aus einem Startbit (Space = 0), fünf Datenbits (LSB zuerst) und mindestens einem Stoppbit (Mark = 1). DDK9 verwendet 1,5 Stoppbits, also 30 ms Markton am Ende jedes Zeichens.

Parameter Wert Bemerkung
Modulationsart FSK (F1B) Frequenzumtastung, zwei Töne
Baudrate 50 Baud 50 Symbole pro Sekunde
Bitdauer 20 ms 1 / 50 Baud
FSK-Shift 225 Hz Frequenzabstand Mark–Space
Mark (logisch 1) höhere Frequenz Träger + 112,5 Hz
Space (logisch 0) niedrigere Frequenz Träger − 112,5 Hz
Kodierung ITA2 / Baudot 5-Bit-Code, LSB zuerst
Rahmen 1 + 5 + 1,5 Bits Start + Daten + Stop
Aufzeichnung mono WAV, 12000 Hz USB-Demodulation, Mittenfrequenz ~1000 Hz
Startbit:    1 × 20 ms =  20 ms   (Space = 0)
Datenbits:   5 × 20 ms = 100 ms   (LSB zuerst)
Stoppbit:  1,5 × 20 ms =  30 ms   (Mark = 1)
Gesamt:    min. 150 ms pro Zeichen
Startbit:    1 × 20 ms =  20 ms   (Space = 0)
Datenbits:   5 × 20 ms = 100 ms   (LSB zuerst)
Stoppbit:  1,5 × 20 ms =  30 ms   (Mark = 1)
Gesamt:    min. 150 ms pro Zeichen

WAV-Quelle und Throttle

Die Aufzeichnung liegt als mono WAV-Datei mit 12000 Hz Samplerate vor. Ohne Geschwindigkeitsbegrenzung würde GNU Radio die Datei so schnell wie möglich verarbeiten. Der Throttle-Block begrenzt auf die reale Abtastrate.


samp_rate = 12000   # Hz
baud      =    50   # Symbole/s
shift     =   225   # Hz – FSK-Hub
decim     =    10   # Dezimationsfaktor

blocks.wavfile_source('ddk9_2.wav', True)           # True = Loop
blocks.throttle(gr.sizeof_float*1, samp_rate, True)


samp_rate = 12000   # Hz
baud      =    50   # Symbole/s
shift     =   225   # Hz – FSK-Hub
decim     =    10   # Dezimationsfaktor

blocks.wavfile_source('ddk9_2.wav', True)           # True = Loop
blocks.throttle(gr.sizeof_float*1, samp_rate, True)

Frequency Xlating FIR Filter

Dieser Block übernimmt drei Aufgaben gleichzeitig: Er mischt das Signal so, dass die gewählte Mittenfrequenz auf 0 Hz liegt, er filtert mit einem Tiefpass auf die relevante Bandbreite, und er wandelt das reale Signal in ein komplexes IQ-Signal um. DDK9 liegt in der USB-Demodulation typischerweise bei ~1000 Hz (über einen Schieberegler einstellbar). Nach der Mischung liegt das FSK-Signal symmetrisch um 0 Hz: Mark bei +112,5 Hz, Space bei −112,5 Hz.

Frequency Xlating FIR Filter Block


center_freq = 1000   # Hz, einstellbar 800–1200 Hz

filter.freq_xlating_fir_filter_fcc(
    1,                                             # Decimation (hier keine)
    firdes.low_pass(1.0, samp_rate, 300, 100),     # 300 Hz Grenzfreq, 100 Hz Übergang
    center_freq,                                   # Mischfrequenz
    samp_rate                                      # Eingabe-Samplerate
)


center_freq = 1000   # Hz, einstellbar 800–1200 Hz

filter.freq_xlating_fir_filter_fcc(
    1,                                             # Decimation (hier keine)
    firdes.low_pass(1.0, samp_rate, 300, 100),     # 300 Hz Grenzfreq, 100 Hz Übergang
    center_freq,                                   # Mischfrequenz
    samp_rate                                      # Eingabe-Samplerate
)

Der Tiefpass mit 300 Hz Grenzfrequenz lässt das FSK-Signal (±112,5 Hz) vollständig durch und unterdrückt Nachbarsignale. Ein zu breites Filter (z. B. 600 Hz) würde Störsignale unnötig durchlassen.

Squelch

Ohne Squelch erzeugt Rauschen bei ausgefallenem Signal einen ununterbrochenen Bitstrom, der durch den Baudot-Decoder läuft. Dabei wird der interne shift_state (LETTERS/FIGURES) korrumpiert – sobald das eigentliche Signal einsetzt, werden Zeichen falsch interpretiert.

Squelch Block


analog.simple_squelch_cc(
    threshold_db = -30,    # Öffnet ab −30 dBFS (einstellbar −60..0 dB)
    alpha        = 0.001   # Glättungszeitkonstante der Leistungsmessung
)


analog.simple_squelch_cc(
    threshold_db = -30,    # Öffnet ab −30 dBFS (einstellbar −60..0 dB)
    alpha        = 0.001   # Glättungszeitkonstante der Leistungsmessung
)

AGC2 – Automatic Gain Control

Der Quadraturdemodulator erwartet ein Signal mit konstanter Amplitude. Kurze atmosphärische Störimpulse können die Amplitude kurzzeitig stark überschreiten. Ein einfacher AGC-Block mit einer Zeitkonstante überschwingt bei solchen Impulsen und vernichtet nachfolgende Symbole. agc2_cc trennt Attack und Decay in zwei unabhängige Zeitkonstanten.

AGC2 Block


analog.agc2_cc(
    attack_rate = 1e-1,   # schnelle Reaktion auf starke Signale
    decay_rate  = 1e-2,   # langsames Abklingen nach einem Störer
    reference   = 1.0,    # Zielamplitude
    gain        = 1.0     # Anfangsverstärkung
)


analog.agc2_cc(
    attack_rate = 1e-1,   # schnelle Reaktion auf starke Signale
    decay_rate  = 1e-2,   # langsames Abklingen nach einem Störer
    reference   = 1.0,    # Zielamplitude
    gain        = 1.0     # Anfangsverstärkung
)

Parameter Wert Wirkung
attack_rate 10⁻¹ Dämpft Störimpulse innerhalb weniger Samples
decay_rate 10⁻² Pegel erholt sich langsam → nachfolgende Symbole bleiben stabil

Quadratur-Demodulator

Der Quadraturdemodulator berechnet die momentane Frequenzabweichung des komplexen Eingangssignals. Positive Abweichung (Mark) ergibt positive Ausgangsamplitude, negative Abweichung (Space) ergibt negative Amplitude.

Quadratur-Demodulator Block

Der Gain-Parameter skaliert so, dass ±1,0 am Ausgang einer Frequenzabweichung von ±shift/2 entspricht:


gain = f_s / (2π · shift) = 12000 / (2π · 225) = 12000 / 1413,7 ≈ +8,49


gain = f_s / (2π · shift) = 12000 / (2π · 225) = 12000 / 1413,7 ≈ +8,49


analog.quadrature_demod_cf(samp_rate / (2 * math.pi * shift))
# = 12000 / (2 * 3.14159 * 225) ≈ +8.49


analog.quadrature_demod_cf(samp_rate / (2 * math.pi * shift))
# = 12000 / (2 * 3.14159 * 225) ≈ +8.49

Das Vorzeichen ist entscheidend: Ein negativer Gain vertauscht Mark und Space. Da ITA2 im Idle-Zustand alle Bits = 1 (Mark) erwartet, führt ein falsches Vorzeichen zu systematischen Dekodierfehlern.

Gain-Vorzeichen Mark (höhere Freq.) Space (niedrigere Freq.) Ergebnis
negativ (falsch) negative Amplitude → Slicer: 0 positive Amplitude → Slicer: 1 Mark/Space vertauscht ✗
positiv (korrekt) positive Amplitude → Slicer: 1 negative Amplitude → Slicer: 0 korrekte Polarität ✓

FIR-Tiefpass mit Dezimation

Nach dem Quadraturdemodulator enthält das Signal hochfrequente Artefakte. Ein Tiefpassfilter entfernt diese und reduziert gleichzeitig durch Dezimation die Samplerate auf das Minimum, das für die Symbolsynchronisation ausreicht.

FIR-Tiefpass Block

Nyquist-Kriterium für 50-Baud-RTTY

Das schnellste Signal bei 50 Baud ist eine alternierende Folge 0101… mit 25 Hz. Ein Grenzwert von 50 Hz mit 25 Hz Übergangsband ist optimal.

f_out = f_s / decim = 12000 / 10 = 1200 Hz

sps = f_out / baud = 1200 / 50 = 24
f_out = f_s / decim = 12000 / 10 = 1200 Hz

sps = f_out / baud = 1200 / 50 = 24

filter.fir_filter_fff(
    10,                                                           # Dezimation 12000→1200 Hz
    firdes.low_pass(1.0, samp_rate, 50, 25, window.WIN_HAMMING)  # 50 Hz, 25 Hz Übergang
)


filter.fir_filter_fff(
    10,                                                           # Dezimation 12000→1200 Hz
    firdes.low_pass(1.0, samp_rate, 50, 25, window.WIN_HAMMING)  # 50 Hz, 25 Hz Übergang
)

Parameter Wert Bedeutung
Dezimationsfaktor 10 12000 Hz → 1200 Hz
Grenzfrequenz 50 Hz Nyquist für 50 Baud
Übergangsbreite 25 Hz Steilheit des Filters
Fensterfunktion Hamming ~53 dB Sperrdämpfung
Samples pro Symbol (sps) 24 1200 Hz / 50 Baud

Polaritäts-Block

Der Multiply-Const-Block multipliziert das Signal mit +1 oder −1. Dies ist nötig, wenn die Aufzeichnung in LSB statt USB gemacht wurde – in diesem Fall sind Mark und Space frequenzmäßig gespiegelt. Der Wert ist live über einen Schieberegler umschaltbar.

Das Signal nach diesem Block wird auch an das Eye-Diagramm weitergeleitet.

Polaritäts-Block


blocks.multiply_const_ff(polarity)   # polarity = +1 oder −1


blocks.multiply_const_ff(polarity)   # polarity = +1 oder −1

Symbol-Synchronisation (Zero-Crossing TED)

RTTY ist ein asynchrones Protokoll ohne gemeinsamen Takt zwischen Sender und Empfänger. Zwischen Zeichen verharrt DDK9 im Mark-Idle-Zustand – der Sender schickt nur logische 1er. Der Symbol-Sync-Block muss den lokalen Abtastzeitpunkt laufend nachführen.

Symbol-Synchronisation Block

Problem mit Müller & Müller TED: Dieser Algorithmus leitet Taktkorrektur aus Übergängen (0→1, 1→0) ab. Bei langen Mark-Phasen (Idle) gibt es keine Übergänge – der Takt erhält kein Korrektursignal und driftet. Im Eye-Diagramm sieht man ein „Wandern" des Abtastzeitpunkts.

Der Zero-Crossing TED erkennt Nulldurchgänge im kontinuierlichen Signal zwischen zwei Symbolen. Auch bei Mark-Idle erzeugen die Flanken zwischen Symbolen verwertbare Nulldurchgangsinformation:


# sps = samp_rate / decim / baud = 12000 / 10 / 50 = 24

digital.symbol_sync_ff(
    digital.TED_ZERO_CROSSING,           # stabil auch bei Idle-Phasen
    samp_rate / decim / baud,            # sps = 24
    0.05,                                # loop_bw – schnelles Einrasten
    1.0,                                 # damping
    1.0,                                 # gain_mu
    0.1,                                 # max_dev = ±10 %
    1,                                   # osps = 1 Sample pro Symbol
    digital.constellation_bpsk().base(),
    digital.IR_MMSE_8TAP,
    128, []
)


# sps = samp_rate / decim / baud = 12000 / 10 / 50 = 24

digital.symbol_sync_ff(
    digital.TED_ZERO_CROSSING,           # stabil auch bei Idle-Phasen
    samp_rate / decim / baud,            # sps = 24
    0.05,                                # loop_bw – schnelles Einrasten
    1.0,                                 # damping
    1.0,                                 # gain_mu
    0.1,                                 # max_dev = ±10 %
    1,                                   # osps = 1 Sample pro Symbol
    digital.constellation_bpsk().base(),
    digital.IR_MMSE_8TAP,
    128, []
)

Parameter Wert Wirkung
TED-Typ ZERO_CROSSING Funktioniert ohne Übergänge im Signal
sps 24 Muss exakt samp_rate / decim / baud sein
loop_bw 0,05 Schnelles Einrasten auf neues Signal
max_dev 0,1 Bis zu 10 % Symboltaktabweichung toleriert

Häufiger Fehler: In älteren Versionen wurde fälschlicherweise

sps = f_s / decim / (2 · shift) = 12000 / 10 / 450 ≈ 2,67
sps = f_s / decim / (2 · shift) = 12000 / 10 / 450 ≈ 2,67

verwendet. Die korrekte Formel ist immer

sps = f_s / decim / baud
sps = f_s / decim / baud

Binary Slicer

Der Binary Slicer wandelt das kontinuierliche Float-Signal in einzelne Bits um. Schwellwert ist fest bei 0: Werte > 0 werden zu Bit 1 (Mark), Werte ≤ 0 werden zu Bit 0 (Space).

Binary Slicer Block


digital.binary_slicer_fb()
# Eingang: float (−1 … +1)
# Ausgang: unsigned byte (0 oder 1)


digital.binary_slicer_fb()
# Eingang: float (−1 … +1)
# Ausgang: unsigned byte (0 oder 1)

Baudot-Decoder (Embedded Python Block)

ITA2 / Baudot-Kodierung

ITA2 ist ein 5-Bit-Code mit zwei Zeichensätzen. Umgeschaltet wird durch zwei Steuerworte: LTRS (0b11111) für Buchstaben und FIGS (0b11011) für Ziffern und Sonderzeichen. Die Bits werden LSB zuerst übertragen – Bit 0 hat Gewicht (2^0 = 1).

Die State Machine erkennt den RTTY-Rahmen ohne externe Synchronisation:

  • IDLE: wartet auf fallende Flanke (Startbit = 0)
  • DATA: sammelt 5 Datenbits (LSB zuerst), errechnet 5-Bit-Code, schlägt im LTRS- oder FIGS-Table nach
  • WAIT_STOP: erwartet Stoppbit = 1. Fehlt es, liegt ein Framing Error vor. Nach 3 aufeinanderfolgenden Fehlern vollständiger Reset.

Der DDK9 Baudot Decoder ist kein Standard Block. Er ist ein embedded Python Block, der die eingehenden Bits in einen lesbaren Textoutput umwandelt.

Baudot-Decoder Embedded Python Block

Vollständiger Decoder-Code


import numpy as np
from gnuradio import gr

class ddk9_baudot_decoder(gr.sync_block):
    def __init__(self, text_queue=None):
        gr.sync_block.__init__(
            self, name='DDK9 Baudot Decoder',
            in_sig=[np.int8], out_sig=None
        )
        self.text_queue  = text_queue
        self.state       = 'IDLE'
        self.bit_buffer  = []
        self.shift_state = 'LETTERS'
        self.framing_errors = 0

        # ITA2-Tabelle Buchstaben (P=0b10110, L=0b10010 – nicht vertauschen!)
        self.LETS = {
            0b00100: ' ',  0b10111: 'Q',  0b10011: 'W',  0b00001: 'E',
            0b01010: 'R',  0b10000: 'T',  0b10101: 'Y',  0b00111: 'U',
            0b00110: 'I',  0b11000: 'O',  0b10110: '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',  0b10110: '0',  0b00011: '-',
            0b00101: "'",  0b01001: '$',  0b01101: '!',  0b11010: '&',
            0b10100: '#',  0b01011: '\a', 0b01111: '(',  0b10010: ')',
            0b10001: '+',  0b11101: '/',  0b01110: ':',  0b11110: ';',
            0b11001: '?',  0b01100: ',',  0b11100: '.',
            0b01000: '\r', 0b00010: '\n'
        }

    def _emit(self, char):
        if self.text_queue is not None:
            self.text_queue.put(char)
        else:
            print(char, end='', flush=True)

    def _reset(self):
        self.state = 'IDLE'
        self.bit_buffer = []
        self.shift_state = 'LETTERS'
        self.framing_errors = 0

    def work(self, input_items, output_items):
        in0 = input_items[0]
        for bit in in0:

            if self.state == 'IDLE':
                if bit == 0:                    # Startbit (Space = 0)
                    self.state = 'DATA'
                    self.bit_buffer = []

            elif self.state == 'DATA':
                self.bit_buffer.append(bit)
                if len(self.bit_buffer) == 5:
                    # LSB zuerst: Bit 0 hat Gewicht 2^0
                    code = sum(b << i for i, b in enumerate(self.bit_buffer))

                    if code == 0b11111:         # LTRS – Buchstabenmodus
                        self.shift_state = 'LETTERS'
                        self.framing_errors = 0
                    elif code == 0b11011:       # FIGS – Ziffermode
                        self.shift_state = 'FIGURES'
                        self.framing_errors = 0
                    elif code == 0b00000:
                        pass                    # Null / Idle
                    else:
                        table = self.LETS if self.shift_state == 'LETTERS' else self.FIGS
                        char = table.get(code)
                        if char is not None:
                            self.framing_errors = max(0, self.framing_errors - 1)
                            self._emit('<BEL>' if char == '\a' else char)

                    self.state = 'WAIT_STOP'

            elif self.state == 'WAIT_STOP':
                if bit == 1:                    # Stoppbit korrekt (Mark = 1)
                    self.state = 'IDLE'
                else:                           # fehlendes Stoppbit
                    self.framing_errors += 1
                    if self.framing_errors >= 3:
                        self._reset()           # Reset nach 3 Fehlern
                    else:
                        self.state = 'IDLE'

        return len(in0)


import numpy as np
from gnuradio import gr

class ddk9_baudot_decoder(gr.sync_block):
    def __init__(self, text_queue=None):
        gr.sync_block.__init__(
            self, name='DDK9 Baudot Decoder',
            in_sig=[np.int8], out_sig=None
        )
        self.text_queue  = text_queue
        self.state       = 'IDLE'
        self.bit_buffer  = []
        self.shift_state = 'LETTERS'
        self.framing_errors = 0

        # ITA2-Tabelle Buchstaben (P=0b10110, L=0b10010 – nicht vertauschen!)
        self.LETS = {
            0b00100: ' ',  0b10111: 'Q',  0b10011: 'W',  0b00001: 'E',
            0b01010: 'R',  0b10000: 'T',  0b10101: 'Y',  0b00111: 'U',
            0b00110: 'I',  0b11000: 'O',  0b10110: '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',  0b10110: '0',  0b00011: '-',
            0b00101: "'",  0b01001: '$',  0b01101: '!',  0b11010: '&',
            0b10100: '#',  0b01011: '\a', 0b01111: '(',  0b10010: ')',
            0b10001: '+',  0b11101: '/',  0b01110: ':',  0b11110: ';',
            0b11001: '?',  0b01100: ',',  0b11100: '.',
            0b01000: '\r', 0b00010: '\n'
        }

    def _emit(self, char):
        if self.text_queue is not None:
            self.text_queue.put(char)
        else:
            print(char, end='', flush=True)

    def _reset(self):
        self.state = 'IDLE'
        self.bit_buffer = []
        self.shift_state = 'LETTERS'
        self.framing_errors = 0

    def work(self, input_items, output_items):
        in0 = input_items[0]
        for bit in in0:

            if self.state == 'IDLE':
                if bit == 0:                    # Startbit (Space = 0)
                    self.state = 'DATA'
                    self.bit_buffer = []

            elif self.state == 'DATA':
                self.bit_buffer.append(bit)
                if len(self.bit_buffer) == 5:
                    # LSB zuerst: Bit 0 hat Gewicht 2^0
                    code = sum(b << i for i, b in enumerate(self.bit_buffer))

                    if code == 0b11111:         # LTRS – Buchstabenmodus
                        self.shift_state = 'LETTERS'
                        self.framing_errors = 0
                    elif code == 0b11011:       # FIGS – Ziffermode
                        self.shift_state = 'FIGURES'
                        self.framing_errors = 0
                    elif code == 0b00000:
                        pass                    # Null / Idle
                    else:
                        table = self.LETS if self.shift_state == 'LETTERS' else self.FIGS
                        char = table.get(code)
                        if char is not None:
                            self.framing_errors = max(0, self.framing_errors - 1)
                            self._emit('<BEL>' if char == '\a' else char)

                    self.state = 'WAIT_STOP'

            elif self.state == 'WAIT_STOP':
                if bit == 1:                    # Stoppbit korrekt (Mark = 1)
                    self.state = 'IDLE'
                else:                           # fehlendes Stoppbit
                    self.framing_errors += 1
                    if self.framing_errors >= 3:
                        self._reset()           # Reset nach 3 Fehlern
                    else:
                        self.state = 'IDLE'

        return len(in0)

GUI-Elemente

Eye-Diagramm

Das Eye-Diagramm zeigt das Signal nach dem FIR-Tiefpass überlagert über genau eine Symbolperiode. Ein weit geöffnetes Auge zeigt saubere Symbolerkennung. Ein geschlossenes Auge deutet auf falsche Filterparameter, falschen sps-Wert oder starkes Rauschen hin.

Eye-Diagramm


qtgui.eye_sink_f(
    size       = 1024,
    samp_rate  = samp_rate / decim,              # 1200 Hz
    num_inputs = 1
)
eye_sink.set_samp_per_symbol(int(samp_rate / decim / baud))   # 24


qtgui.eye_sink_f(
    size       = 1024,
    samp_rate  = samp_rate / decim,              # 1200 Hz
    num_inputs = 1
)
eye_sink.set_samp_per_symbol(int(samp_rate / decim / baud))   # 24

Thread-sichere Text-Ausgabe

GNU Radio Worker-Threads dürfen Qt-Widgets nicht direkt beschreiben. Der Decoder schreibt dekodierte Zeichen in eine queue.Queue, die ein QTimer alle 100 ms im GUI-Thread leert:


self.text_queue = queue.Queue()
self._text_timer = Qt.QTimer()
self._text_timer.timeout.connect(self._update_text)
self._text_timer.start(100)   # alle 100 ms

def _update_text(self):
    chars = []
    while not self.text_queue.empty():
        try:
            chars.append(self.text_queue.get_nowait())
        except queue.Empty:
            break
    if chars:
        self.text_display.moveCursor(Qt.QTextCursor.End)
        self.text_display.insertPlainText(''.join(chars))


self.text_queue = queue.Queue()
self._text_timer = Qt.QTimer()
self._text_timer.timeout.connect(self._update_text)
self._text_timer.start(100)   # alle 100 ms

def _update_text(self):
    chars = []
    while not self.text_queue.empty():
        try:
            chars.append(self.text_queue.get_nowait())
        except queue.Empty:
            break
    if chars:
        self.text_display.moveCursor(Qt.QTextCursor.End)
        self.text_display.insertPlainText(''.join(chars))

Bedienregler

Regler Bereich Standard Wirkung
Mittenfrequenz 800–1200 Hz 1000 Hz Kompensiert Frequenzdrift; steuert Freq-Xlating-Block live
Squelch −60 bis 0 dB −30 dB Unterdrückt Rausch-Dekodierung bei fehlendem Signal
Polarität −1 / +1 +1 Tauscht Mark/Space; nötig bei LSB-Aufnahmen

Alle Variablen und ihre Beziehungen


samp_rate = 12000   # Hz – Aufzeichnungs-Samplerate
decim     =    10   # Dezimationsfaktor im FIR-Tiefpass
baud      =    50   # Baudrate des DDK9-Signals
shift     =   225   # Hz – FSK-Frequenzhub

f_work    = samp_rate / decim           # 1200 Hz – Arbeitsrate nach Dezimation
sps       = samp_rate / decim / baud   #   24    – Samples pro Symbol
quad_gain = samp_rate / (2*pi * shift) #  ≈8,49  – Quadraturdemodulator-Verstärkung


samp_rate = 12000   # Hz – Aufzeichnungs-Samplerate
decim     =    10   # Dezimationsfaktor im FIR-Tiefpass
baud      =    50   # Baudrate des DDK9-Signals
shift     =   225   # Hz – FSK-Frequenzhub

f_work    = samp_rate / decim           # 1200 Hz – Arbeitsrate nach Dezimation
sps       = samp_rate / decim / baud   #   24    – Samples pro Symbol
quad_gain = samp_rate / (2*pi * shift) #  ≈8,49  – Quadraturdemodulator-Verstärkung

Ausgabe

Die fertige GUI vereint Bedienregler (Mittenfrequenz, Squelch, Polarität), den dekodierten Textoutput und das Eye-Diagramm zur Kontrolle der Symbol-Synchronisation in einer Ansicht.

Ausgabe / Gesamtansicht

Vollständiger GRC Flow Graph

Die gesamte Verarbeitungskette von der WAV-Quelle über Freq-Xlating-FIR, Squelch, AGC2, Quadratur-Demod, FIR-Tiefpass, Polarität und Symbol-Sync bis zum Binary Slicer und Baudot-Decoder im Überblick.

GRC Flowgraph RTTY Decoder