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.

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.

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$.
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
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_corr → IQ 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
python3 test_iqbalance.py
python3 test_iqbalance.py
DC Blocker
Behebt den Gleichanteil bei 0 Hz — ein verwandtes, aber eigenständiges Empfänger-Frontend-Problem.
USB-Sprachempfänger für 144,300 MHz
Der Empfänger, in dem sich IQ Bal Fix/Optimize direkt hinter dem DC Blocker ergänzen ließen.
RTL-SDR kalibrieren
Frequenzfehler des Quarzes — ein weiterer, unabhängiger Kalibrierschritt für dieselbe Hardware.