PSK mit Pilotsignal

Wie auf der vorherigen Seite beschrieben, ist es bei der Demodulation von PSK erforderlich, den Beginn des jeweiligen Symbols korrekt zu ermitteln. Dazu kann man eine Pilotsequenz in as Signal einfügen.
Das folgende Beispiel zeigt den Einsatz eines Pilotsignals bei der Modulation und Demodulation.

Modulation des PSK Signals

Das folgende Programm generiert die Sampling Werte für ein solches Pilotsignal. Es wird vor dem eigentlichen Signal ausgesendet. Wie beim Signal selbst, wird jedes ASCII Zeichen in Form von 2 Halbbytes dargestellt, damit die Übertragung mit PSK16 möglich ist.

def generate_pilot_signal(pilot_text, carrier_frequency, sampling_rate, symbol_duration):
    """Generiert und speichert das modulierte Signal des Pilotsymbols."""
    # ASCII-Zeichen in Symbole (4-Bit pro Symbol) kodieren
    symbols = []
    for char in pilot_text:
        ascii_value = ord(char)
        high_nibble = (ascii_value >> 4) & 0xF  # Höhere 4 Bits
        low_nibble = ascii_value & 0xF  # Niedrigere 4 Bits
        symbols.extend([high_nibble, low_nibble])

    # Träger-Signalparameter
    samples_per_symbol = int(sampling_rate * symbol_duration)
    t = np.linspace(0, symbol_duration, samples_per_symbol, endpoint=False)

    # Modulation
    pilot_signal = []
    for symbol in symbols:
        phase = (symbol / 15) * 2 * np.pi
        carrier = np.cos(2 * np.pi * carrier_frequency * t + phase)
        pilot_signal.extend(carrier)

    # Rückgabe des Pilotsignals als numpy-Array
    return np.array(pilot_signal)

# Additives Weißes Gaußsches Rauschen (AWGN)
def add_awgn_noise(signal, snr_db):
    """Fügt Additives Weißes Gaußsches Rauschen (AWGN) zu einem Signal hinzu."""
    signal_power = np.mean(np.abs(signal)**2)
    snr_linear = 10**(snr_db / 10)
    noise_power = signal_power / snr_linear
    noise = np.sqrt(noise_power) * np.random.randn(len(signal))
    return signal + noise

# Kanalverzerrung (FIR-Filter):
def simulate_channel(signal, channel_response):
    """Simuliert eine Kanalverzerrung (z. B. FIR-Filter)."""
    # Erhalte Signal gleicher Länge wie das Original
    return np.convolve(signal, channel_response, mode='full')[:len(signal)]

Der gewünschte Text wird in folgender Funktion moduliert. Um ein möglichst realistisches Signal zu erhalten wird zudem Rauschen hinzugefügt. Weiterhin soll simuliert werden, dass vor dem Start der Übertragung nur Rauschen vorhanden ist.

def modulate_psk_with_pilot(
        text,
        pilot_text,
        carrier_frequency,
        sampling_rate,
        symbol_duration,
        snr_db
):
    
    # Generiere das Pilotsignal
    pilot_signal = generate_pilot_signal(
        pilot_text, carrier_frequency, sampling_rate, symbol_duration
    )

    # ASCII-Zeichen in Symbole (4-Bit pro Symbol) kodieren
    symbols = []
    for char in text:
        ascii_value = ord(char)
        high_nibble = (ascii_value >> 4) & 0xF  # Höhere 4 Bits
        low_nibble = ascii_value & 0xF  # Niedrigere 4 Bits
        symbols.extend([high_nibble, low_nibble])

    # Träger-Signalparameter
    samples_per_symbol = int(sampling_rate * symbol_duration)
    t = np.linspace(0, symbol_duration, samples_per_symbol, endpoint=False)

    # Modulation des Hauptsignals
    main_signal = []
    for symbol in symbols:
        phase = (symbol / 16) * 2 * np.pi
        carrier = np.cos(2 * np.pi * carrier_frequency * t + phase)
        main_signal.extend(carrier)


    # Zufälliges Rauschen vor dem Sihnal 
    dummy = np.random.uniform(0, 0, 500)

    # Gesamtsignal ohne Rauschen
    full_signal = np.concatenate([dummy, pilot_signal, main_signal])

    # Simuliere Kanalverzerrungen, falls angegeben
    if channel_response is not None:
        signal = simulate_channel(full_signal, channel_response)

    # Füge Rauschen hinzu
    signal_with_noise = add_awgn_noise(signal, snr_db)

    return signal_with_noise, pilot_signal


Das Ergebnis sieht dann wie folgt aus:

PSK Signal mir Rauschen

Demodulation des Signals

Im ersten Schritt. wird die Position des Pilotsignals ermittelt. Dies geschieht mittels Korrelation. Das verwendete Prinzip ist hier genauer beschrieben

Diese Funktion bestimmt den Start des Pilotsignals im empfangenen Signal. Durch die Verwendung der Korrelation kann das Signal auch im Rauschen erkannt werden.

def normalized_cross_correlation(sample, muster):
    muster_norm = np.linalg.norm(muster)
    correlation = []
    for i in range(len(sample) - len(muster) + 1):
        ausschnitt = sample[i:i + len(muster)]
        ausschnitt_norm = np.linalg.norm(ausschnitt)
        if ausschnitt_norm == 0 or muster_norm == 0: # Division durch Null vermeiden
            correlation.append(0)
        else:
            korr = np.dot(ausschnitt, muster) / (ausschnitt_norm * muster_norm)
            correlation.append(korr)
    return np.array(correlation)

Das empfangene Signal wird nun nach dem Pilotsignal wieder in seine Ursprungszeichen zurück konvertiert

def demodulate_psk_with_pilot(signal, carrier_frequency, sampling_rate, symbol_duration, start_index):
    # Parameter berechnen
    samples_per_symbol = int(sampling_rate * symbol_duration)
    t = np.arange(samples_per_symbol) / sampling_rate
    reference_carrier = np.exp(-1j * 2 * np.pi * carrier_frequency * t)

    # Symbole demodulieren
    demodulated_symbols = []
    for i in range(start_index, len(signal), samples_per_symbol):
        symbol_segment = signal[i:i + samples_per_symbol]
        if len(symbol_segment) < samples_per_symbol:
            break

        # Multiplikation mit Referenzträger
        mixed_signal = symbol_segment * reference_carrier

        # Summieren und Phase extrahieren
        phase = np.angle(np.sum(mixed_signal))

        # Bestimmen des Symbols aus der Phase
        symbol_index = int(np.round((phase / (2 * np.pi)) * 16)) % 16
        demodulated_symbols.append(symbol_index)

    # Symbole in Text umwandeln
    text = ""
    for i in range(0, len(demodulated_symbols), 2):
        if i + 1 >= len(demodulated_symbols):
            raise ValueError("Unvollständiges Symbolpaar erkannt.")
        high_nibble = demodulated_symbols[i]
        low_nibble = demodulated_symbols[i + 1]
        ascii_value = (high_nibble << 4) | low_nibble
        text += chr(ascii_value)

    return text


Aufruf des Programms und Ergebnis:

import numpy as np
import matplotlib.pyplot as plt

... Funktionen

# Parameter
text = "ABCDEFGHIJKLMNOPQRTSUVW".   # zu übertragender Text
pilot_text = chr(0xFF) + chr(0xFF)  # Pilotsymbol: 3 Zeichen

carrier_frequency = 100  # Hz
sampling_rate = 1000     # Hz
symbol_duration = 0.1    # Sekunden pro Symbol
snr_db = 10              # SNR in Dezibel
channel_response = [1, -0.3, 0.1]  # Ein einfaches Kanalmodell (FIR-Filter)

# Modulation
received_signal, pilot_signal = modulate_psk_with_pilot(
    text, pilot_text, carrier_frequency, sampling_rate, symbol_duration, snr_db
)

# Position des Pilotsignals bestimmen
cor = normalized_cross_correlation(received_signal, pilot_signal)
maxpos = np.argmax(cor)
print("Start Pilotsignal", maxpos)

# Demodulation nach dem Pilotsignal
demod = demodulate_psk_with_pilot(received_signal, carrier_frequency, sampling_rate, symbol_duration,maxpos+len(pilot_signal))
Ergebnis:

Start Pilotsignal 500
Länge des Pilotsignals: 400
Länge des gesamten Signals: 5500
ABCDEFGHIJKLMNOPQRTSUVW

Durch das hinzufügen von zusätzlichem Rauschen tritt irgendwann der Effekt ein, dass die Zeichen nicht mehr korrekt erkannt werden können.

Das Signal ist dann stark verzerrt:

Bei SNR = 1 wird das Pilotsignal noch erkannt, die einzelnen Zeichen enthalten jedoch Fehler: ABCEEFHHIJ[L]OOPRReSeVW

Eine weitere Optimierung der Übertragung kann nun noch durch den Einsatz fehlerkorrigierende Codes erreicht werden.