IQ Bal Fix und IQ Bal Optimize

Korrektur von Verstärkungs- und Phasenfehlern zwischen I- und Q-Zweig eines Empfängers — statisch von Hand oder automatisch nachführend, aus gr-iqbalance (bekannt aus gqrx).

1. Beschreibung der Blöcke

Der IQ Bal Fix-Block (iqbalance.fix_cc) wendet eine Verstärkungskorrektur (Magnitude) und eine Phasenkorrektur (Phase) auf ein komplexes IQ-Signal an. Beide Werte lassen sich fest eintragen oder per Message-Port (iqbal_corr) laufend von außen setzen — genau diesen zweiten Weg nutzt der Optimize-Block.

IQ Bal Fix Block

Der IQ Bal Optimize-Block (iqbalance.optimize_c) beobachtet fortlaufend ein IQ-Signal, schätzt daraus die Verstärkungs- und Phasenabweichung und sendet das Ergebnis als komplexe Message (iqbal_corr) — den Realteil als Magnitude-, den Imaginärteil als Phase-Korrektur. Period legt fest, alle wie viele Samples neu geschätzt wird.

IQ Bal Optimize Block

Beide Blöcke stammen aus gr-iqbalance — demselben Modul, das auch hinter der automatischen IQ-Korrektur in gqrx steckt.

2. Woher IQ-Imbalance kommt

Ein SDR-Empfänger gewinnt sein komplexes Basisbandsignal, indem er das HF-Signal mit zwei um 90° phasenverschobenen Mischsignalen (I und Q) multipliziert. In realer Hardware sind Verstärkung und Phasenlage dieser beiden Zweige nie exakt identisch — der Q-Zweig hat z. B. eine minimal andere Verstärkung oder die 90°-Verschiebung ist nicht exakt 90°. Das Ergebnis ist kein Rauschen, sondern ein Spiegelbild jedes empfangenen Signals bei der gegenüberliegenden Frequenz: Ein Signal bei $+f$ erzeugt ein schwächeres, aber echtes Abbild bei $-f$.

Genau dieses Prinzip nutzt der [DC Blocker](/blocks/dc-blocker/) nicht ab — der behebt nur einen Gleichanteil bei 0 Hz, keine Verstärkungs-/Phasenunsymmetrie über das gesamte Band. Beide Fehlerarten treten in realer Hardware gemeinsam auf und werden von unterschiedlichen Blöcken behoben.

3. Parameter

Block Parameter Bedeutung
IQ Bal Fix mag (Magnitude) Verstärkungskorrektur, linear um 0 herum (0 = keine Korrektur)
IQ Bal Fix phase (Phase) Phasenkorrektur in Radiant (0 = keine Korrektur)
IQ Bal Optimize period (Period) Anzahl Samples zwischen zwei Neuschätzungen — kleiner reagiert schneller auf sich ändernde Fehler, größer glättet stärker

4. Verifiziertes Beispiel

Verifiziert (GNU Radio 3.10.12.0, tatsächlich ausgeführt): Ein sauberer Testton bei +5 kHz wird künstlich mit `channels.iqbal_gen` (Empfänger-Modell, 1,5 dB Verstärkungsfehler, 5° Phasenfehler) verzerrt, anschließend läuft `IQ Bal Optimize` + `IQ Bal Fix` im geschlossenen Regelkreis darüber:
from gnuradio import gr, blocks, analog, channels, iqbalance
import numpy as np

samp_rate, tone_freq, n = 48000, 5000, 400000

def image_rejection(sig, samp_rate, tone_freq):
    seg = sig[-65536:] * np.hanning(65536)
    spec = np.abs(np.fft.fftshift(np.fft.fft(seg)))
    freqs = np.fft.fftshift(np.fft.fftfreq(65536, d=1/samp_rate))
    def peak_near(f):
        idx = np.argmin(np.abs(freqs - f))
        return 20*np.log10(np.max(spec[max(0,idx-3):idx+4]) + 1e-12)
    return peak_near(tone_freq), peak_near(-tone_freq)

tb = gr.top_block()
src = analog.sig_source_c(samp_rate, analog.GR_COS_WAVE, tone_freq, 1.0, 0)
imb = channels.iqbal_gen(1.5, 5.0, 1)          # 1.5 dB / 5 Grad Fehler, Empfaenger-Modell
opt = iqbalance.optimize_c(100)
fix = iqbalance.fix_cc(0, 0)                    # startet unkorrigiert
hb = blocks.head(gr.sizeof_gr_complex, n)
snk = blocks.vector_sink_c()

tb.connect(src, imb)
tb.connect(imb, fix)
tb.connect(imb, opt)
tb.msg_connect((opt, 'iqbal_corr'), (fix, 'iqbal_corr'))
tb.connect(fix, hb, snk)
tb.run()

s_db, i_db = image_rejection(np.array(snk.data()), samp_rate, tone_freq)
print(f"Nach Korrektur: Signal {s_db:.1f} dB, Spiegel {i_db:.1f} dB, Unterdrueckung {s_db - i_db:.1f} dB")
print(f"Geschaetzt: mag={fix.mag():.4f}, phase={fix.phase():.4f}")
from gnuradio import gr, blocks, analog, channels, iqbalance
import numpy as np

samp_rate, tone_freq, n = 48000, 5000, 400000

def image_rejection(sig, samp_rate, tone_freq):
    seg = sig[-65536:] * np.hanning(65536)
    spec = np.abs(np.fft.fftshift(np.fft.fft(seg)))
    freqs = np.fft.fftshift(np.fft.fftfreq(65536, d=1/samp_rate))
    def peak_near(f):
        idx = np.argmin(np.abs(freqs - f))
        return 20*np.log10(np.max(spec[max(0,idx-3):idx+4]) + 1e-12)
    return peak_near(tone_freq), peak_near(-tone_freq)

tb = gr.top_block()
src = analog.sig_source_c(samp_rate, analog.GR_COS_WAVE, tone_freq, 1.0, 0)
imb = channels.iqbal_gen(1.5, 5.0, 1)          # 1.5 dB / 5 Grad Fehler, Empfaenger-Modell
opt = iqbalance.optimize_c(100)
fix = iqbalance.fix_cc(0, 0)                    # startet unkorrigiert
hb = blocks.head(gr.sizeof_gr_complex, n)
snk = blocks.vector_sink_c()

tb.connect(src, imb)
tb.connect(imb, fix)
tb.connect(imb, opt)
tb.msg_connect((opt, 'iqbal_corr'), (fix, 'iqbal_corr'))
tb.connect(fix, hb, snk)
tb.run()

s_db, i_db = image_rejection(np.array(snk.data()), samp_rate, tone_freq)
print(f"Nach Korrektur: Signal {s_db:.1f} dB, Spiegel {i_db:.1f} dB, Unterdrueckung {s_db - i_db:.1f} dB")
print(f"Geschaetzt: mag={fix.mag():.4f}, phase={fix.phase():.4f}")

Ausgabe:

Nach Korrektur: Signal 89.6 dB, Spiegel -13.5 dB, Unterdrueckung 103.1 dB
Geschaetzt: mag=-0.1618, phase=-0.0734
Nach Korrektur: Signal 89.6 dB, Spiegel -13.5 dB, Unterdrueckung 103.1 dB
Geschaetzt: mag=-0.1618, phase=-0.0734

Zum Vergleich, ohne Korrektur (nur channels.iqbal_gen, kein Fix-Block): Signal 90,5 dB, Spiegel 70,2 dB, Unterdrückung nur 20,3 dB. Der geschlossene Regelkreis aus Optimize und Fix hebt die Spiegelunterdrückung damit von 20,3 dB auf 103,1 dB an — eine Verbesserung um rund 83 dB, ohne dass die konkreten Fehlerwerte (1,5 dB/5°) irgendwo im Korrekturpfad bekannt gegeben wurden. IQ Bal Optimize hat sie blind aus dem Signal selbst geschätzt.

5. Statisch vs. automatisch

Variante Verdrahtung Wann sinnvoll
Statisch nur IQ Bal Fix, mag/phase fest eingetragen Fehler ist bekannt und stabil (z. B. einmalig mit einem Referenzsignal gemessen) — spart Rechenlast
Automatisch IQ Bal Optimize → Message iqbal_corrIQ Bal Fix Fehler ist unbekannt, ändert sich mit Temperatur/Zeit, oder es steht kein Referenzsignal zur Kalibrierung zur Verfügung

Die im Beispiel automatisch geschätzten Werte (mag=-0.1618, phase=-0.0734) ließen sich genauso gut direkt als feste Magnitude/Phase-Parameter in einen alleinstehenden IQ Bal Fix-Block eintragen, sobald sie einmal bekannt sind — der Regelkreis mit IQ Bal Optimize ist nur dann nötig, wenn sich der Fehler laufend ändern kann.

6. Ein- und Ausgänge

Block Stream-Eingang Stream-Ausgang Message
IQ Bal Fix Complex Complex Eingang iqbal_corr (optional)
IQ Bal Optimize Complex Ausgang iqbal_corr

IQ Bal Optimize hat keinen Stream-Ausgang — er beobachtet nur und meldet das Ergebnis als Message. Er sitzt deshalb typischerweise parallel zum IQ Bal Fix-Block (beide am selben Eingangssignal), nicht in Reihe dahinter.

7. Download

Download: Das Verifikationsskript dieses Artikels als ZIP — iq-bal-bloecke.zip herunterladen, entpacken, dann:
python3 test_iqbalance.py
python3 test_iqbalance.py