Modulation

Phase Shift Keying (PSK)

Digitale Daten kodiert als Phasenlage eines Trägersignals — von BPSK bis 8-PSK, inklusive Demodulation in Python.

Phase Shift Keying (PSK) ist ein digitales Modulationsverfahren, bei dem die Phase eines Trägersignals verwendet wird, um digitale Daten zu übertragen. Bei PSK wird die Phase des Trägersignals je nach den zu übertragenden Daten verändert. Die Idee ist, dass verschiedene Phasenlagen des Signals bestimmten Datenwerten (Bitmustern) entsprechen, sodass die Information durch die Phasenänderungen kodiert wird.

Funktionsweise von PSK:

  1. Trägersignal: Das Trägersignal ist ein sinusförmiges Signal mit einer konstanten Frequenz.
  2. Phasenänderung: Das Trägersignal wird in unterschiedlichen Phasen moduliert, um digitale Informationen zu übertragen. Jede Änderung in der Phase repräsentiert ein Bit oder eine Gruppe von Bits.

Typen von PSK:

Es gibt verschiedene Formen von PSK, abhängig davon, wie viele Phasenunterschiede verwendet werden, um die Daten zu kodieren:

  1. BPSK (Binary Phase Shift Keying):

    • Hier gibt es zwei Phasenzustände: 0° und 180°.
    • Jedes Bit wird durch einen Phasenwechsel dargestellt:
      • 0° repräsentiert z. B. ein Bit „0".
      • 180° repräsentiert z. B. ein Bit „1".
    • Es ist die einfachste Form von PSK, daher auch robust gegen Rauschen, aber die Datenrate ist begrenzt.
  2. QPSK (Quadrature Phase Shift Keying):

    • Bei QPSK werden vier verschiedene Phasenzustände verwendet: 0°, 90°, 180°, und 270°.
    • Da vier Phasen verwendet werden, können zwei Bits pro Phasenänderung übertragen werden (z. B. „00", „01", „10", „11").
    • QPSK ermöglicht eine höhere Datenrate als BPSK bei gleicher Bandbreite.
  3. 8-PSK:

    • Hier gibt es acht verschiedene Phasenzustände (z. B. 0°, 45°, 90°, 135°, 180°, 225°, 270°, 315°).
    • Dadurch können drei Bits pro Symbol übertragen werden, was die Datenrate weiter erhöht.
    • Mit mehr Phasenzuständen wird das System allerdings empfindlicher gegenüber Rauschen und Signalstörungen.

PSK am Beispiel von Numpy

Im folgenden Code wird ein PSK Signal erzeugt, das den ASCII Code übertragen soll.
Ein ASCII-Zeichen wird dabei in 2 Teile zerlegt und jeweils als 4-Bit-Symbol übertragen — bei 16 möglichen Phasenzuständen ($2^4 = 16$) trägt jedes Symbol also 4 Bit.

Bsp.: Das Zeichen "z" hat den HexCode 0x7A. Es wird also zunächst die 7 und anschließend das A übertragen.

Der Code für das PSK Signal ist folgender:

import numpy as np
import matplotlib.pyplot as plt

# Parameter
carrier_freq = 100e3  # 100 kHz Trägerfrequenz
sampling_rate = 1e6  # Abtastrate von 1 MHz
symbol_duration = 1e-3  # Dauer eines Symbols (1 ms)
message = "" # Textnachricht

for i in range(255):
    message = message + chr(i)

# ASCII-Werte der Zeichen im Text
ascii_values = [ord(char) for char in message]
print(f"ASCII-Werte: {ascii_values}")

# Umwandlung von ASCII in 2 Symbole (je 4 Bits)
binary_message = []
for value in ascii_values:
    binary_value = bin(value)[2:].zfill(8)  # ASCII-Wert in 8-Bit Binär umwandeln
    binary_message.extend([binary_value[:4], binary_value[4:]])  # In zwei 4-Bit-Symbole aufteilen

# Anzahl der Phasen für das PSK-Signal (16 Phasen für 4-Bit-Symbole)
num_phases = 16

# Berechnung der Phasenverschiebung für jedes Symbol und Speicherung im Konstellationsdiagramm
constellation_points = []

for symbol in binary_message:
    symbol_value = int(symbol, 2)  # Binärsymbol in Zahl umwandeln
    phase_shift = (2 * np.pi * symbol_value) / num_phases  # Berechne die Phase basierend auf der Anzahl der Phasen

    # Speichere den Punkt im komplexen Plan
    real_part = np.cos(phase_shift)  # Realteil (Amplitude entlang der x-Achse)
    imag_part = np.sin(phase_shift)  # Imaginärteil (Amplitude entlang der y-Achse)
    constellation_points.append(complex(real_part, imag_part))  # Punkt als komplexe Zahl speichern

# Konstellationsdiagramm plotten
plt.figure(figsize=(8, 8))
for point in constellation_points:
    plt.plot(point.real, point.imag, 'bo')  # Plotte die Punkte im Diagramm
plt.title("Konstellationsdiagramm für PSK Signal (ohne Paritätsbit)")
plt.xlabel("Realteil (Amplitude entlang der x-Achse)")
plt.ylabel("Imaginärteil (Amplitude entlang der y-Achse)")
plt.grid(True)
plt.axis('equal')  # Gleiche Skala für x und y Achse
plt.show()
import numpy as np
import matplotlib.pyplot as plt

# Parameter
carrier_freq = 100e3  # 100 kHz Trägerfrequenz
sampling_rate = 1e6  # Abtastrate von 1 MHz
symbol_duration = 1e-3  # Dauer eines Symbols (1 ms)
message = "" # Textnachricht

for i in range(255):
    message = message + chr(i)

# ASCII-Werte der Zeichen im Text
ascii_values = [ord(char) for char in message]
print(f"ASCII-Werte: {ascii_values}")

# Umwandlung von ASCII in 2 Symbole (je 4 Bits)
binary_message = []
for value in ascii_values:
    binary_value = bin(value)[2:].zfill(8)  # ASCII-Wert in 8-Bit Binär umwandeln
    binary_message.extend([binary_value[:4], binary_value[4:]])  # In zwei 4-Bit-Symbole aufteilen

# Anzahl der Phasen für das PSK-Signal (16 Phasen für 4-Bit-Symbole)
num_phases = 16

# Berechnung der Phasenverschiebung für jedes Symbol und Speicherung im Konstellationsdiagramm
constellation_points = []

for symbol in binary_message:
    symbol_value = int(symbol, 2)  # Binärsymbol in Zahl umwandeln
    phase_shift = (2 * np.pi * symbol_value) / num_phases  # Berechne die Phase basierend auf der Anzahl der Phasen

    # Speichere den Punkt im komplexen Plan
    real_part = np.cos(phase_shift)  # Realteil (Amplitude entlang der x-Achse)
    imag_part = np.sin(phase_shift)  # Imaginärteil (Amplitude entlang der y-Achse)
    constellation_points.append(complex(real_part, imag_part))  # Punkt als komplexe Zahl speichern

# Konstellationsdiagramm plotten
plt.figure(figsize=(8, 8))
for point in constellation_points:
    plt.plot(point.real, point.imag, 'bo')  # Plotte die Punkte im Diagramm
plt.title("Konstellationsdiagramm für PSK Signal (ohne Paritätsbit)")
plt.xlabel("Realteil (Amplitude entlang der x-Achse)")
plt.ylabel("Imaginärteil (Amplitude entlang der y-Achse)")
plt.grid(True)
plt.axis('equal')  # Gleiche Skala für x und y Achse
plt.show()

Im Constellation Diagramm ergibt sich dann folgendes Bild, wenn man alle Zeichen aus dem Code überträgt:

Konstellationsdiagramm für das PSK-Signal

Die constellation_points oben sind nur die idealen Phasenwerte zur Visualisierung — noch kein tatsächliches Zeitbereichssignal. Um ein PSK-Signal zu demodulieren, muss zuerst ein echtes, oszillierendes Signal erzeugt werden, das den Träger tatsächlich mit den berechneten Phasen moduliert. Das übernimmt die folgende Funktion:
def modulate_psk(binary_symbols, num_phases, carrier_freq, sampling_rate, symbol_duration):
    """Erzeugt aus einer Folge von Binärsymbolen ein reelles PSK-Zeitbereichssignal."""
    samples_per_symbol = int(sampling_rate * symbol_duration)
    t_symbol = np.arange(samples_per_symbol) / sampling_rate

    psk_signal = []
    for symbol in binary_symbols:
        symbol_value = int(symbol, 2)
        phase_shift = (2 * np.pi * symbol_value) / num_phases
        psk_signal.extend(np.cos(2 * np.pi * carrier_freq * t_symbol + phase_shift))

    return np.array(psk_signal)

psk_signal = modulate_psk(binary_message, num_phases, carrier_freq, sampling_rate, symbol_duration)
def modulate_psk(binary_symbols, num_phases, carrier_freq, sampling_rate, symbol_duration):
    """Erzeugt aus einer Folge von Binärsymbolen ein reelles PSK-Zeitbereichssignal."""
    samples_per_symbol = int(sampling_rate * symbol_duration)
    t_symbol = np.arange(samples_per_symbol) / sampling_rate

    psk_signal = []
    for symbol in binary_symbols:
        symbol_value = int(symbol, 2)
        phase_shift = (2 * np.pi * symbol_value) / num_phases
        psk_signal.extend(np.cos(2 * np.pi * carrier_freq * t_symbol + phase_shift))

    return np.array(psk_signal)

psk_signal = modulate_psk(binary_message, num_phases, carrier_freq, sampling_rate, symbol_duration)

Demodulation

Für die Demodulation des Signals werden die einzelnen Phasen im Signal wieder ermittelt. Wir folgen dabei im Wesentlichen dem Prozess der hier beschrieben ist (Digitale Modulation).

Das modulierte Signal besteht nun aus einer Folge von Symbolen, wobei jedes Symbol für eine feste Zeitdauer (symbol_duration) ein bestimmtes Bitmuster repräsentiert. Um die Phasen jedes Symbols zu analysieren, muss das Signal in Blöcke segmentiert werden, die der Länge eines Symbols entsprechen.

Wichtig für die Demodulation: psk_signal ist reell (nur der Kosinus-Anteil). Ein Referenzträger, der ebenfalls nur aus cos(...) besteht, kann die Phase nicht eindeutig bestimmen — die Summe zweier reeller Signale ist immer reell, und np.angle() einer reellen Zahl liefert nur 0 oder π, egal welche der 16 Phasen tatsächlich gesendet wurde. Der Referenzträger muss deshalb komplex sein (Kosinus- und Sinusanteil, also I und Q), damit np.angle() den vollen Bereich von 0 bis 2π unterscheiden kann.
# Demodulationsfunktion
def demodulate_psk(psk_signal, carrier_freq, sampling_rate, symbol_duration, num_phases):
    """
    Args:
        psk_signal (numpy.ndarray): Das modulierte PSK-Signal (reell).
        carrier_freq (float): Trägerfrequenz in Hz.
        sampling_rate (float): Abtastrate in Hz.
        symbol_duration (float): Dauer eines Symbols in Sekunden.
        num_phases (int): Anzahl der möglichen Phasen (z. B. 16 für 4-Bit-PSK).

    Returns:
        list of str: Liste der Binärsymbole, die demoduliert wurden.
    """
    samples_per_symbol = int(sampling_rate * symbol_duration)
    phase_step = 2 * np.pi / num_phases  # Schrittweite zwischen den Phasen
    symbol_bins = np.arange(0, 2 * np.pi, phase_step)  # Alle möglichen Phasen

    demodulated_symbols = []

    # Iteriere über das Signal in Blöcken mit der Länge eines Symbols
    for i in range(0, len(psk_signal), samples_per_symbol):
        # Extrahiere ein Symbol (entspricht der Dauer eines Symbols)
        symbol_segment = psk_signal[i:i + samples_per_symbol]
        if len(symbol_segment) < samples_per_symbol:
            break  # Letzter Block könnte kürzer sein

        # Rekonstruiere die Phase des Symbols mit einem komplexen Referenzträger (I/Q)
        t = np.arange(len(symbol_segment)) / sampling_rate
        reference_carrier = np.exp(-1j * 2 * np.pi * carrier_freq * t)  # komplexer Referenzträger
        phase = np.angle(np.sum(symbol_segment * reference_carrier))  # Berechne die Phase

        # Mappe die Phase auf das nächste Bin (zirkulärer Abstand wegen des Übergangs von 2π zu 0)
        phase = (phase + 2 * np.pi) % (2 * np.pi)  # Stelle sicher, dass Phase positiv ist
        circular_diff = (symbol_bins - phase + np.pi) % (2 * np.pi) - np.pi
        closest_bin = np.argmin(np.abs(circular_diff))  # Finde die nächste gültige Phase

        # Speichere das Symbol (Binärwert des Phasenindex)
        demodulated_symbols.append(format(closest_bin, f'0{int(np.log2(num_phases))}b'))

    return demodulated_symbols
# Demodulationsfunktion
def demodulate_psk(psk_signal, carrier_freq, sampling_rate, symbol_duration, num_phases):
    """
    Args:
        psk_signal (numpy.ndarray): Das modulierte PSK-Signal (reell).
        carrier_freq (float): Trägerfrequenz in Hz.
        sampling_rate (float): Abtastrate in Hz.
        symbol_duration (float): Dauer eines Symbols in Sekunden.
        num_phases (int): Anzahl der möglichen Phasen (z. B. 16 für 4-Bit-PSK).

    Returns:
        list of str: Liste der Binärsymbole, die demoduliert wurden.
    """
    samples_per_symbol = int(sampling_rate * symbol_duration)
    phase_step = 2 * np.pi / num_phases  # Schrittweite zwischen den Phasen
    symbol_bins = np.arange(0, 2 * np.pi, phase_step)  # Alle möglichen Phasen

    demodulated_symbols = []

    # Iteriere über das Signal in Blöcken mit der Länge eines Symbols
    for i in range(0, len(psk_signal), samples_per_symbol):
        # Extrahiere ein Symbol (entspricht der Dauer eines Symbols)
        symbol_segment = psk_signal[i:i + samples_per_symbol]
        if len(symbol_segment) < samples_per_symbol:
            break  # Letzter Block könnte kürzer sein

        # Rekonstruiere die Phase des Symbols mit einem komplexen Referenzträger (I/Q)
        t = np.arange(len(symbol_segment)) / sampling_rate
        reference_carrier = np.exp(-1j * 2 * np.pi * carrier_freq * t)  # komplexer Referenzträger
        phase = np.angle(np.sum(symbol_segment * reference_carrier))  # Berechne die Phase

        # Mappe die Phase auf das nächste Bin (zirkulärer Abstand wegen des Übergangs von 2π zu 0)
        phase = (phase + 2 * np.pi) % (2 * np.pi)  # Stelle sicher, dass Phase positiv ist
        circular_diff = (symbol_bins - phase + np.pi) % (2 * np.pi) - np.pi
        closest_bin = np.argmin(np.abs(circular_diff))  # Finde die nächste gültige Phase

        # Speichere das Symbol (Binärwert des Phasenindex)
        demodulated_symbols.append(format(closest_bin, f'0{int(np.log2(num_phases))}b'))

    return demodulated_symbols

Die Multiplikation des modulierten Signals (symbol_segment) mit dem komplexen Referenzträger (reference_carrier) hebt die Phase des Symbols hervor. Mathematisch entspricht dies einer I/Q-Abmischung: Der Realteil des Referenzträgers korreliert mit der In-Phase-Komponente des Signals, der Imaginärteil mit der Quadraturkomponente — genau diese Kombination braucht np.angle(), um die volle Phase zu rekonstruieren.

Formel: y(t) = s(t) · e^{-j2πf_c·t}

Nach der Multiplikation bleiben nur die niederfrequenten Anteile übrig, die Informationen über die Phase des Symbols tragen.
Es ergibt sich ein Signal mit schwankender Amplitude und Phase. Um eine stabile Phase für das gesamte Symbol zu bestimmen, wird durch die Summierung das Signalrauschen reduziert und die Phaseninformation verstärkt.

Der zirkuläre Abstand beim Runden auf das nächste Phasenbin ist kein Detail, sondern notwendig: Symbol 0 liegt bei Phase 0. Drückt Rauschen die gemessene Phase knapp unter 0 (also nach der Modulo-Operation knapp unter 2π), würde ein einfacher linearer Abstand |symbol_bins - phase| das fälschlich als "weit von Bin 0 entfernt" werten und stattdessen Bin 15 wählen — obwohl 0 und 2π auf dem Phasenkreis direkt benachbart sind. Erst (symbol_bins - phase + π) % 2π - π berücksichtigt den Kreis korrekt.

Nach der Summierung liegt ein komplexer Wert vor (Summe von sinus- und kosinusmodulierten Anteilen). Die Phase kann mit der Funktion np.angle() aus dem komplexen Signal berechnet werden.

Da die erhaltenen Phasenwerte in einem Kreis von 0 bis 2π liegen, muss die Phase normalisiert werden, um sicherzustellen, dass sie in diesem Bereich bleibt. Zusätzlich muss sie auf den nächstgelegenen Konstellationspunkt im PSK-Konstellationsdiagramm gemappt werden.

Der nächstgelegene Konstellationspunkt repräsentiert ein spezifisches Binärsymbol.
Die so erhaltenen Binärwerte können wieder zu einer zusammenhängenden Binärfolge zusammengefügt werden. Daraus lässt sich die ursprüngliche Nachricht ermitteln.

Bei der Demodulation eines PSK-Signals müssen Sie wissen, wo der Beginn jedes Zeichens liegt, um das Signal korrekt in Symbole zu segmentieren.
Das obige Programm berücksichtigt dies noch nicht. Eine Demodulation ist hier nur möglich, weil wir den Beginn des Zeichens aus dem generierten Signal sicher kennen.
In echten Signalen muss der Beginn eines Symbols zunächst erkannt werden.

Für die Symbolerkennung können verschiedene Verfahren zum Einsatz kommen:

  1. Verwendung eines Präambels (Synchronisationssignal)
    Der Sender fügt zu Beginn der Übertragung eine festgelegte Folge von Symbolen ein, die als Präambel dient. Die Präambel hat eine bekannte Phase oder Amplitudenfolge, die der Empfänger verwendet, um den Beginn der Übertragung zu erkennen.
    Eine solche Präambel wird z.B. bei der Übertragung von Bildern oft verwendet. Jede neue Zeile eines Bildes wird dann z.B. mit einer 0/1-Folge eingeleitet.
    Bei Bildern verliert man dadurch maximal eine Zeile, bis das Signal demoduliert werden kann. Eine laufende Textübertragung kann man so nicht demodulieren, wenn die Präambel am Anfang des gesamten Textes ist.

    Die Präambel kann mathematisch wieder durch Korrelation im Signal erkannt werden.

  2. Frame-Synchronisation durch zusätzliche Marker
    Wenn jedes Zeichen aus mehreren Symbolen besteht, kann ein spezieller Marker zwischen Zeichen hinzugefügt werden. Dieser Marker hat eine eindeutige Phase oder Amplitude, die nicht in den normalen Symbolen vorkommt.
    Der Marker kann dann durch eine Mustererkennung gefunden werden.

  3. Pilot-Symbol-Tracking
    In längeren Übertragungen können regelmäßig eingefügte Pilot-Symbole verwendet werden, die synchron bleiben. Diese Pilot-Symbole haben eine spezifische, bekannte Phase oder Amplitude.
    Der Empfänger synchronisiert sich durch Erkennung der Pilot-Symbole.

  4. Fehlerkorrektur und Selbstsynchronisation
    Einige Übertragungsprotokolle verwenden redundante Daten oder Fehlerkorrekturcodes, um den Beginn eines Zeichens zu erkennen. Bei der Demodulation wird überprüft, ob die empfangenen Daten den erwarteten Mustern entsprechen, und die Synchronisation wird automatisch angepasst.
    Beispiel: Ein 8-Bit-Zeichen wird durch ein Parity-Bit ergänzt, und nur gültige Binärmuster werden als Beginn eines Zeichens akzeptiert.

Vollständiges Beispiel mit visualisierten Zwischenschritten

Das folgende, eigenständig lauffähige Programm fasst Modulation und Demodulation zusammen und stellt vier Zwischenschritte des Verfahrens in einem gemeinsamen Diagramm dar: das Zeitbereichssignal, die ideale Sende-Konstellation, die tatsächlichen (verrauschten) Korrelationswerte vor dem Runden auf das Phasenraster, und den direkten Vergleich zwischen gesendeten und demodulierten Symbolwerten.

import numpy as np
import matplotlib.pyplot as plt

# ---------- Parameter ----------
carrier_freq = 100e3      # 100 kHz Trägerfrequenz
sampling_rate = 1e6       # 1 MHz Abtastrate
symbol_duration = 1e-3    # 1 ms pro Symbol
num_phases = 16           # 16 Phasenzustände -> 4 Bit pro Symbol
samples_per_symbol = int(sampling_rate * symbol_duration)

message = "Hallo SDR!"

# ---------- 1. Text -> Bits -> Symbolwerte ----------
binary_message = []
for value in (ord(char) for char in message):
    bits = bin(value)[2:].zfill(8)
    binary_message.extend([bits[:4], bits[4:]])
symbol_values = [int(s, 2) for s in binary_message]

# ---------- 2. Modulation: Symbolwerte -> PSK-Zeitsignal ----------
def modulate_psk(symbol_values, num_phases, carrier_freq, sampling_rate, samples_per_symbol):
    t_symbol = np.arange(samples_per_symbol) / sampling_rate
    signal, ideal_points = [], []
    for value in symbol_values:
        phase = (2 * np.pi * value) / num_phases
        signal.extend(np.cos(2 * np.pi * carrier_freq * t_symbol + phase))
        ideal_points.append(np.exp(1j * phase))
    return np.array(signal), np.array(ideal_points)

tx_signal, ideal_points = modulate_psk(
    symbol_values, num_phases, carrier_freq, sampling_rate, samples_per_symbol
)

# ---------- 3. Kanal: etwas Rauschen zur Veranschaulichung ----------
noise = np.random.normal(0, 0.05, size=tx_signal.shape)
rx_signal = tx_signal + noise

# ---------- 4. Demodulation: I/Q-Korrelation je Symbol ----------
def demodulate_psk(signal, carrier_freq, sampling_rate, samples_per_symbol, num_phases):
    t_symbol = np.arange(samples_per_symbol) / sampling_rate
    reference = np.exp(-1j * 2 * np.pi * carrier_freq * t_symbol)  # komplexer Referenzträger
    phase_step = 2 * np.pi / num_phases
    symbol_bins = np.arange(0, 2 * np.pi, phase_step)

    raw_points, decoded_symbols = [], []
    for i in range(0, len(signal) - samples_per_symbol + 1, samples_per_symbol):
        segment = signal[i:i + samples_per_symbol]
        # Normierung auf N/2, damit ideale Punkte wieder Betrag 1 haben (siehe Herleitung im Text)
        correlation = np.sum(segment * reference) / (samples_per_symbol / 2)
        raw_points.append(correlation)

        phase = np.angle(correlation) % (2 * np.pi)
        circular_diff = (symbol_bins - phase + np.pi) % (2 * np.pi) - np.pi  # zirkulärer Abstand
        decoded_symbols.append(int(np.argmin(np.abs(circular_diff))))

    return np.array(raw_points), decoded_symbols

raw_points, decoded_symbols = demodulate_psk(
    rx_signal, carrier_freq, sampling_rate, samples_per_symbol, num_phases
)

# ---------- 5. Symbolwerte -> Bits -> Text ----------
decoded_text = ""
for i in range(0, len(decoded_symbols) - 1, 2):
    bits = format(decoded_symbols[i], "04b") + format(decoded_symbols[i + 1], "04b")
    decoded_text += chr(int(bits, 2))

print("Gesendet: ", message)
print("Empfangen:", decoded_text)

# ---------- 6. Visualisierung der vier Zwischenschritte ----------
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
angles = np.linspace(0, 2 * np.pi, num_phases, endpoint=False)

# (a) Zeitbereich: die ersten paar Symbole
n_show = 4 * samples_per_symbol
t_axis_ms = np.arange(n_show) / sampling_rate * 1000
axes[0, 0].plot(t_axis_ms, tx_signal[:n_show])
for k in range(1, 4):
    axes[0, 0].axvline(k * symbol_duration * 1000, color="gray", linestyle="--", linewidth=0.8)
axes[0, 0].set_title("1. Zeitbereich: PSK-Signal (erste 4 Symbole)")
axes[0, 0].set_xlabel("Zeit [ms]")
axes[0, 0].set_ylabel("Amplitude")

# (b) Ideal-Konstellation (gesendete Phasen)
axes[0, 1].plot(np.cos(angles), np.sin(angles), "o", color="lightgray", markersize=10)
axes[0, 1].plot(ideal_points.real, ideal_points.imag, "bo", label="gesendete Symbole")
axes[0, 1].set_title("2. Ideal-Konstellation (Sender)")
axes[0, 1].set_xlabel("Realteil (I)")
axes[0, 1].set_ylabel("Imaginärteil (Q)")
axes[0, 1].axis("equal")
axes[0, 1].grid(True)
axes[0, 1].legend()

# (c) Korrelationswerte vor dem Runden aufs Phasenraster (zeigt den Rauscheinfluss)
axes[1, 0].plot(np.cos(angles), np.sin(angles), "o", color="lightgray", markersize=10)
axes[1, 0].plot(raw_points.real, raw_points.imag, "r.", label="Korrelation (vor dem Runden)")
axes[1, 0].set_title("3. Demodulation: Korrelationsergebnis vs. Phasenraster")
axes[1, 0].set_xlabel("Realteil (I)")
axes[1, 0].set_ylabel("Imaginärteil (Q)")
axes[1, 0].axis("equal")
axes[1, 0].grid(True)
axes[1, 0].legend()

# (d) Vergleich: gesendete vs. demodulierte Symbolwerte
axes[1, 1].step(range(len(symbol_values)), symbol_values, where="mid", label="gesendet")
axes[1, 1].step(range(len(decoded_symbols)), decoded_symbols, where="mid", linestyle="--", label="demoduliert")
axes[1, 1].set_title("4. Symbolwerte: gesendet vs. demoduliert")
axes[1, 1].set_xlabel("Symbolindex")
axes[1, 1].set_ylabel("Symbolwert (0–15)")
axes[1, 1].legend()

plt.tight_layout()
plt.show()
import numpy as np
import matplotlib.pyplot as plt

# ---------- Parameter ----------
carrier_freq = 100e3      # 100 kHz Trägerfrequenz
sampling_rate = 1e6       # 1 MHz Abtastrate
symbol_duration = 1e-3    # 1 ms pro Symbol
num_phases = 16           # 16 Phasenzustände -> 4 Bit pro Symbol
samples_per_symbol = int(sampling_rate * symbol_duration)

message = "Hallo SDR!"

# ---------- 1. Text -> Bits -> Symbolwerte ----------
binary_message = []
for value in (ord(char) for char in message):
    bits = bin(value)[2:].zfill(8)
    binary_message.extend([bits[:4], bits[4:]])
symbol_values = [int(s, 2) for s in binary_message]

# ---------- 2. Modulation: Symbolwerte -> PSK-Zeitsignal ----------
def modulate_psk(symbol_values, num_phases, carrier_freq, sampling_rate, samples_per_symbol):
    t_symbol = np.arange(samples_per_symbol) / sampling_rate
    signal, ideal_points = [], []
    for value in symbol_values:
        phase = (2 * np.pi * value) / num_phases
        signal.extend(np.cos(2 * np.pi * carrier_freq * t_symbol + phase))
        ideal_points.append(np.exp(1j * phase))
    return np.array(signal), np.array(ideal_points)

tx_signal, ideal_points = modulate_psk(
    symbol_values, num_phases, carrier_freq, sampling_rate, samples_per_symbol
)

# ---------- 3. Kanal: etwas Rauschen zur Veranschaulichung ----------
noise = np.random.normal(0, 0.05, size=tx_signal.shape)
rx_signal = tx_signal + noise

# ---------- 4. Demodulation: I/Q-Korrelation je Symbol ----------
def demodulate_psk(signal, carrier_freq, sampling_rate, samples_per_symbol, num_phases):
    t_symbol = np.arange(samples_per_symbol) / sampling_rate
    reference = np.exp(-1j * 2 * np.pi * carrier_freq * t_symbol)  # komplexer Referenzträger
    phase_step = 2 * np.pi / num_phases
    symbol_bins = np.arange(0, 2 * np.pi, phase_step)

    raw_points, decoded_symbols = [], []
    for i in range(0, len(signal) - samples_per_symbol + 1, samples_per_symbol):
        segment = signal[i:i + samples_per_symbol]
        # Normierung auf N/2, damit ideale Punkte wieder Betrag 1 haben (siehe Herleitung im Text)
        correlation = np.sum(segment * reference) / (samples_per_symbol / 2)
        raw_points.append(correlation)

        phase = np.angle(correlation) % (2 * np.pi)
        circular_diff = (symbol_bins - phase + np.pi) % (2 * np.pi) - np.pi  # zirkulärer Abstand
        decoded_symbols.append(int(np.argmin(np.abs(circular_diff))))

    return np.array(raw_points), decoded_symbols

raw_points, decoded_symbols = demodulate_psk(
    rx_signal, carrier_freq, sampling_rate, samples_per_symbol, num_phases
)

# ---------- 5. Symbolwerte -> Bits -> Text ----------
decoded_text = ""
for i in range(0, len(decoded_symbols) - 1, 2):
    bits = format(decoded_symbols[i], "04b") + format(decoded_symbols[i + 1], "04b")
    decoded_text += chr(int(bits, 2))

print("Gesendet: ", message)
print("Empfangen:", decoded_text)

# ---------- 6. Visualisierung der vier Zwischenschritte ----------
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
angles = np.linspace(0, 2 * np.pi, num_phases, endpoint=False)

# (a) Zeitbereich: die ersten paar Symbole
n_show = 4 * samples_per_symbol
t_axis_ms = np.arange(n_show) / sampling_rate * 1000
axes[0, 0].plot(t_axis_ms, tx_signal[:n_show])
for k in range(1, 4):
    axes[0, 0].axvline(k * symbol_duration * 1000, color="gray", linestyle="--", linewidth=0.8)
axes[0, 0].set_title("1. Zeitbereich: PSK-Signal (erste 4 Symbole)")
axes[0, 0].set_xlabel("Zeit [ms]")
axes[0, 0].set_ylabel("Amplitude")

# (b) Ideal-Konstellation (gesendete Phasen)
axes[0, 1].plot(np.cos(angles), np.sin(angles), "o", color="lightgray", markersize=10)
axes[0, 1].plot(ideal_points.real, ideal_points.imag, "bo", label="gesendete Symbole")
axes[0, 1].set_title("2. Ideal-Konstellation (Sender)")
axes[0, 1].set_xlabel("Realteil (I)")
axes[0, 1].set_ylabel("Imaginärteil (Q)")
axes[0, 1].axis("equal")
axes[0, 1].grid(True)
axes[0, 1].legend()

# (c) Korrelationswerte vor dem Runden aufs Phasenraster (zeigt den Rauscheinfluss)
axes[1, 0].plot(np.cos(angles), np.sin(angles), "o", color="lightgray", markersize=10)
axes[1, 0].plot(raw_points.real, raw_points.imag, "r.", label="Korrelation (vor dem Runden)")
axes[1, 0].set_title("3. Demodulation: Korrelationsergebnis vs. Phasenraster")
axes[1, 0].set_xlabel("Realteil (I)")
axes[1, 0].set_ylabel("Imaginärteil (Q)")
axes[1, 0].axis("equal")
axes[1, 0].grid(True)
axes[1, 0].legend()

# (d) Vergleich: gesendete vs. demodulierte Symbolwerte
axes[1, 1].step(range(len(symbol_values)), symbol_values, where="mid", label="gesendet")
axes[1, 1].step(range(len(decoded_symbols)), decoded_symbols, where="mid", linestyle="--", label="demoduliert")
axes[1, 1].set_title("4. Symbolwerte: gesendet vs. demoduliert")
axes[1, 1].set_xlabel("Symbolindex")
axes[1, 1].set_ylabel("Symbolwert (0–15)")
axes[1, 1].legend()

plt.tight_layout()
plt.show()

Ausgabe des Programms:

Ergebnis der Simulation

Bei geringem Rauschen (wie im Beispiel, Standardabweichung 0,05) liegen die roten Punkte in Diagramm 3 dicht an den idealen Rasterpunkten, und die beiden Stufenkurven in Diagramm 4 liegen exakt aufeinander — der Text wird fehlerfrei zurückgewonnen. Erhöht man die Rauschamplitude, wandern die roten Punkte zunehmend von ihrem idealen Rasterpunkt weg; sobald sie näher an einem benachbarten Rasterpunkt liegen als am eigenen, entstehen Symbolfehler — sichtbar als Abweichung zwischen den beiden Kurven in Diagramm 4.

Erhöht man z.B. sigma auf 5, liegen die roten Punkte nicht mehr exakt an den erfünschten Phasenpunkten. Es ergeben sich dadurch Bitfehler, die Nachricht kann nicht mehr fehlerfrei demoduliert werden.

Fehler bei höherem Rauschen