Grundlagen · Mustererkennung mittels Korrelation
PSK mit Pilotsignal
Wie ein vorangestelltes Pilotsignal die Synchronisation einer PSK16-Übertragung per Kreuzkorrelation ermöglicht.
Wie auf der Seite Phase Shift Keying beschrieben, ist es bei der Demodulation von PSK erforderlich, den Beginn des jeweiligen Symbols korrekt zu ermitteln. Dazu kann man eine Pilotsequenz in das 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 zwei 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 / 16) * 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)]
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 / 16) * 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,
channel_response=None
):
# 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)
# Stille vor dem eigentlichen Signal simulieren
dummy = np.random.uniform(0, 0, 500)
# Gesamtsignal ohne Rauschen
full_signal = np.concatenate([dummy, pilot_signal, main_signal])
# Simuliere Kanalverzerrungen, falls angegeben
signal = full_signal
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
def modulate_psk_with_pilot(
text,
pilot_text,
carrier_frequency,
sampling_rate,
symbol_duration,
snr_db,
channel_response=None
):
# 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)
# Stille vor dem eigentlichen Signal simulieren
dummy = np.random.uniform(0, 0, 500)
# Gesamtsignal ohne Rauschen
full_signal = np.concatenate([dummy, pilot_signal, main_signal])
# Simuliere Kanalverzerrungen, falls angegeben
signal = full_signal
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:


Demodulation des Signals
Im ersten Schritt wird die Position des Pilotsignals ermittelt. Dies geschieht mittels Korrelation. Das verwendete Prinzip ist unter Mustererkennung mittels Korrelation 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)
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
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
symbol_segment ist ein reelles Signal (nur cos(...)), reference_carrier dagegen komplex (exp(-jωt)). Die Multiplikation beider und die anschließende Summation über ein Symbol ist eine klassische I/Q-Abmischung: Sie liefert einen Term bei der Differenzfrequenz $f_c - f_c = 0$ (der die Phaseninformation trägt) und einen Term bei $2f_c$. Der $2f_c$-Anteil summiert sich über die Symboldauer nur dann exakt zu Null, wenn die Symboldauer eine ganzzahlige Anzahl von Trägerperioden enthält — hier: $f_c \cdot T_{sym} = 100\,\text{Hz} \cdot 0{,}1\,\text{s} = 10$ volle Perioden. Bei einer Symboldauer, die keine ganze Trägerperiodenzahl ergibt, bliebe ein Rest des $2f_c$-Terms in der Summe stehen und würde die extrahierte Phase systematisch verfälschen.Aufruf des Programms und Ergebnis:
import numpy as np
import matplotlib.pyplot as plt
# ... Funktionen wie oben definiert
# Parameter
text = "ABCDEFGHIJKLMNOPQRTSUVW" # zu übertragender Text
pilot_text = chr(0xFF) + chr(0xFF) # Pilotsymbol: 2 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, channel_response
)
# 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)
)
import numpy as np
import matplotlib.pyplot as plt
# ... Funktionen wie oben definiert
# Parameter
text = "ABCDEFGHIJKLMNOPQRTSUVW" # zu übertragender Text
pilot_text = chr(0xFF) + chr(0xFF) # Pilotsymbol: 2 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, channel_response
)
# 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
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 dB wird das Pilotsignal noch erkannt, die einzelnen Zeichen enthalten jedoch Fehler:
ABCEEFHHIJ[L]OOPRReSeVW
ABCEEFHHIJ[L]OOPRReSeVW
Zusammenfassung
Die Demodulation erfolgt in zwei Schritten:
- Erkennung des Pilotsignals per normierter Kreuzkorrelation, um die Startposition des Signals zu finden
- Symbol-Demodulation mit Hilfe eines Referenzträgers, um die Phaseninformation zu extrahieren und den ursprünglichen Text zurückzugewinnen
Bei einem SNR von 10 dB rekonstruiert das System den Text „ABCDEFGHIJKLMNOPQRTSUVW" fehlerfrei. Bei einem SNR von 1 dB verschlechtert sich die Erkennung, es treten Zeichenfehler auf. Fehlerkorrigierende Codes könnten die Übertragungssicherheit weiter verbessern.