1
0
Fork 0

Reorganize notes; measure effect of LNA gain, ADC calibration.

Python code to experiment with IF filtering.
This commit is contained in:
Joris van Rantwijk 2014-01-11 23:19:20 +01:00
parent f1b4dc55d6
commit 13219ed6ba
3 changed files with 150 additions and 29 deletions

View File

@ -2,15 +2,23 @@
This file contains random notitions
-----------------------------------
Valid sample rates
------------------
Sample rates between 300001 Hz and 900000 Hz (inclusive) are not supported.
They cause an invalid configuration of the RTL chip.
rsamp_ratio = 28.8 MHz * 2**22 / sample_rate
If bit 27 and bit 28 of rsamp_ratio are different, the RTL chip malfunctions.
The RTL chip has a configurable 32-tap FIR filter.
Behaviour of RTL and Elonics tuner
----------------------------------
The RTL chip has a configurable 32-tap FIR filter running at 28.8 MS/s.
RTL-SDR currently configures it for cutoff at 1.2 MHz (2.4 MS/s).
Casual test of ADC errors:
Casual test of ADC mismatch:
* DC offset in order of 1 code step
* I/Q gain mismatch in order of 4%
* I/Q phase mismatch in order of 1% of sample interval
@ -32,18 +40,54 @@ mode causes a brief level spike, while manually rewriting the same IF gain in
AGC mode does not have any effect).
It seems more likely that AGC is a digital gain in the downsampling filter.
Default settings in librtlsdr:
Elonics LNA gain: when auto tuner gain: autonomous control with slow update
otherwise gain as configured via rtlsdr_set_tuner_gain
Elonics mixer gain: autonomous control disabled,
gain depending on rtlsdr_set_tuner_gain
Elonics IF linearity: optimize sensitivity (default), auto switch disabled
Elonics IF gain: +6, +0, +0, +0, +9, +9 (non-standard mode)
Elonics IF filters: matched to sample rate (note this may not be optimal)
RTL AGC mode off
Default settings in librtlsdr
-----------------------------
Elonics LNA gain: when auto tuner gain: autonomous control with slow update
otherwise gain as configured via rtlsdr_set_tuner_gain
Elonics mixer gain: autonomous control disabled,
gain depending on rtlsdr_set_tuner_gain
Elonics IF linearity: optimize sensitivity (default), auto switch disabled
Elonics IF gain: +6, +0, +0, +0, +9, +9 (non-standard mode)
Elonics IF filters: matched to sample rate (note this may not be optimal)
RTL AGC mode off
Local radio stations:
Effect of settings on baseband SNR
----------------------------------
STATION SRATE LNA IF GAIN AGC SOFT BW IF LEVEL GUARD/PILOT
radio3 1 MS/s 24 dB default off 150 kHz 0.19 -62.6 dB/Hz
radio3 1.5 MS 24 dB default off 150 kHz 0.19 -62.7 dB/Hz
radio3 2 MS/s 24 dB default off 150 kHz 0.18 -62.7 dB/Hz
radio3 2 MS/s 34 dB default off 150 kHz 0.46 -64.0 dB/Hz
radio3 2 MS/s 34 dB default off 80 kHz -64.0 dB/Hz
radio3 2 MS/s 34 dB default off 150 kHz adccal -64.0 dB/Hz
radio4 2 MS/s 24 dB default off 150 kHz 0.04 -41.1 dB/Hz
radio4 1 MS/s 34 dB default off 150 kHz 0.06 -43.3 dB/Hz
radio4 1 MS/s 34 dB default off 80 kHz -51.2 dB/Hz
radio4 2 MS/s 34 dB default off 150 kHz 0.10 -42.4 dB/Hz
radio4 2 MS/s 34 dB default off 80 kHz -48.2 dB/Hz
radio4 2 MS/s 34 dB default off 150 kHz adccal -42.4 dB/Hz
Note: all measurements 10 second duration
Note: all measurements have LO frequency set to station + 250 kHz
Conclusion: Sample rate (1 MS/s to 2 MS/s) has little effect on quality.
Conclusion: LNA gain has little effect on quality.
Conclusion: Narrow IF filter improves quality of weak station.
Conclusion: ADC gain/offset calibration has no effect on quality.
Local radio stations
--------------------
radio2 92600000 (good)
radio3 96800000 (good)
radio4 94300000 (bad)
@ -52,3 +96,4 @@ radio538 102100000 (medium)
radio10 103800000 (bad)
radio west 89300000 (medium)
--

View File

@ -1,19 +1,16 @@
* (experiment) make nice plot of baseband distortion due to IF filtering
* (experiment) consider downsampling IF signal before FM detection
* (experiment) measure effect of IF gain on baseband SNR
* (experiment) measure effect of IF gain linearity on baseband SNR
* (experiment) measure effect of RTL AGC mode on baseband SNR
* (experiment) measure effect of ADC calibration on baseband SNR
* (experiment) measure effect of IF bandwidth on baseband SNR
* (experiment) measure effect of IF sample rate on baseband SNR
* (experiment) try if RTL AGC mode improves FM decoding
* (feature) support 'M' 'k' suffixes for sample rates and tuning frequency
* (feature) implement off-line FM decoder in Python for experimentation
* (feature) implement stereo pilot pulse-per-second
* (quality) consider DC offset calibration
* (speedup) maybe replace high-order FIR downsampling filter with 2nd order butterworth followed by lower order FIR filter
* figure out why we sometimes lose stereo lock
* it looks like IF level sometimes varies so much that it saturates the receiver; perhaps this can be solved by dynamically managing the hardware gain in response to level measurements
* (quality) figure out if I/Q balance can improve weak stations
* (quality) figure out if hardware gain settings can improve weak stations
* (feature) implement RDS decoding
* (quality) consider FM demodulation with PLL instead of phase discriminator

103
pyfm.py
View File

@ -6,6 +6,8 @@ import sys
import types
import numpy
import numpy.fft
import numpy.linalg
import numpy.random
import scipy.signal
@ -78,24 +80,37 @@ def firFilter(d, coeff):
return scipy.signal.lfilter(coeff, 1, d)
def quadratureDetector(d):
"""FM frequency detector based on quadrature demodulation."""
def quadratureDetector(d, fs):
"""FM frequency detector based on quadrature demodulation.
Return an array of real-valued numbers, representing frequencies in Hz."""
k = fs / (2 * numpy.pi)
# lazy version
def g(d):
prev = None
for b in d:
if prev is None:
yield numpy.angle(b[1:] * b[:-1].conj())
else:
x = numpy.concatenate((prev[-1:], b[:-1]))
yield numpy.angle(b * x.conj())
if prev is not None:
x = numpy.concatenate((prev[1:], b[:1]))
yield numpy.angle(x * prev.conj()) * k
prev = b
yield numpy.angle(prev[1:] * prev[:-1].conj()) * k
if isinstance(d, types.GeneratorType):
return g(d)
else:
return numpy.angle(d[1:] * d[:-1].conj())
return numpy.angle(d[1:] * d[:-1].conj()) * k
def modulateFm(sig, fs, fcenter=0):
"""Create an FM modulated IQ signal.
sig :: modulation signal, values in Hz
fs :: sample rate in Hz
fcenter :: center frequency in Hz
"""
return numpy.exp(2j * numpy.pi * (sig + fcenter).cumsum() / fs)
def spectrum(d, fs=1, nfft=None, sortfreq=False):
@ -232,11 +247,12 @@ def pll(d, centerfreq, bandwidth):
return y, phasei, phaseq, phaseerr, freq, phase
def pilotLevel(d, fs, freqshift, bw=150.0e3):
def pilotLevel(d, fs, freqshift, nfft=None, bw=150.0e3):
"""Calculate level of the 19 kHz pilot vs noise floor in the guard band.
d :: block of raw I/Q samples or lazy I/Q sample stream
fs :: sample frequency in Hz
nfft :: FFT length
freqshift :: frequency offset in Hz
bw :: half-bandwidth of IF signal in Hz
@ -255,10 +271,10 @@ def pilotLevel(d, fs, freqshift, bw=150.0e3):
d = firFilter(d, b)
# Demodulate FM.
d = quadratureDetector(d)
d = quadratureDetector(d, fs)
# Power spectral density.
f, q = spectrum(d, fs=fs, sortfreq=False)
f, q = spectrum(d, fs=fs, nfft=nfft, sortfreq=False)
# Locate 19 kHz bin.
k19 = int(19.0e3 * len(q) / fs)
@ -266,7 +282,7 @@ def pilotLevel(d, fs, freqshift, bw=150.0e3):
k19 = k19 - kw + numpy.argmax(q[k19-kw:k19+kw])
# Calculate pilot power.
p19 = numpy.sum(q[k19-1:k19+2]) * fs * 0.75 / len(q)
p19 = numpy.sum(q[k19-1:k19+2]) * fs * 1.5 / len(q)
# Calculate noise floor in guard band.
k17 = int(17.0e3 * len(q) / fs)
@ -278,3 +294,66 @@ def pilotLevel(d, fs, freqshift, bw=150.0e3):
return (p19db, guarddb, guarddb - p19db)
def modulateAndReconstruct(sigfreq, sigampl, nsampl, fs, noisebw=None, ifbw=None, ifnoise=0):
"""Create a pure sine wave, modulate to FM, add noise, filter, demodulate.
sigfreq :: frequency of sine wave in Hz
sigampl :: amplitude of sine wave in Hz (carrier swing)
nsampl :: number of samples
fs :: sample rate in Hz
noisebw :: calculate noise after demodulation over this bandwidth
ifbw :: IF filter bandwidth in Hz, or None for no filtering
ifnoise :: IF noise level
Return (ampl, phase, noise)
where ampl is the amplitude of the reconstructed sine wave (~ sigampl)
phase is the phase shift after reconstruction
noise is the standard deviation of noise in the reconstructed signal
"""
# Make sine wave.
sig0 = sigampl * numpy.sin(2*numpy.pi*sigfreq/fs * numpy.arange(nsampl))
# Modulate to IF.
fm = modulateFm(sig0, fs=fs, fcenter=0)
# Add noise.
if ifnoise:
fm += numpy.sqrt(0.5) * numpy.random.normal(0, ifnoise, nsampl)
fm += 1j * numpy.sqrt(0.5) * numpy.random.normal(0, ifnoise, nsampl)
# Filter IF.
if ifbw is not None:
b = scipy.signal.firwin(61, 2.0 * ifbw / fs, window='nuttall')
fm = scipy.signal.lfilter(b, 1, fm)
fm = fm[61:]
# Demodulate.
sig1 = quadratureDetector(fm, fs=fs)
# Fit original sine wave.
k = len(sig1)
m = numpy.zeros((k, 3))
m[:,0] = numpy.sin(2*numpy.pi*sigfreq/fs * (numpy.arange(k) + nsampl - k))
m[:,1] = numpy.cos(2*numpy.pi*sigfreq/fs * (numpy.arange(k) + nsampl - k))
m[:,2] = 1
fit = numpy.linalg.lstsq(m, sig1)
csin, ccos, coffset = fit[0]
del fit
# Calculate amplitude, phase.
ampl1 = numpy.sqrt(csin**2 + ccos**2)
phase1 = numpy.arctan2(-ccos, csin)
# Calculate residual noise.
res1 = sig1 - m[:,0] * csin - m[:,1] * ccos
if noisebw is not None:
b = scipy.signal.firwin(61, 2.0 * noisebw / fs, window='nuttall')
res1 = scipy.signal.lfilter(b, 1, res1)
noise1 = numpy.sqrt(numpy.mean(res1 ** 2))
return ampl1, phase1, noise1