RTL-SDRs are great devices. They are known for being small, inexpensive, light in power demand, operable from low frequencies through L band, and workable with numerous signal processing packages. They are NOT known for having precise and stable clock oscillators. Frequency errors of 30 to 50 parts per million are common. For users tuning VHF narrowband voice or digital modes, such an error is enough to cause signals to fall on the edge of the receiver passband. In some cases, signals may fall out of a narrow passband and be completely missed.
One solution is to only use RTL-SDRs sold with upgraded clock oscillators, such as the NESDR SMArt or RTL-SDR.com V3. Another way to put the RTL-SDR on frequency is to check it against a known signal and manually program the offset into applications controlling the dongle. That is often accurate enough, but still a broad approximation. A better solution, also applicable to upgraded devices, is to automate the process of checking against known signals and get a more precise measurement based on a time series of phase measurements. For that, there is a nifty application named Kalibrate-RTL.
Kalibrate-RTL uses mobile telephone calibration signals, which are derived from GNSS signals as a precise frequency and time reference. Also, it eliminates human factors in measuring offsets. In fact it takes multiple readings and averages them to work out a mean offset. That is good enough (quite good enough) for most users. For critical applications needing top notch, minimal drift, precision tuning, an SDR better than RTL dongles should be considered...
How well do you know your RTL dongle? If its capabilities or how it works seems a bit mysterious, check out our more detailed page about understanding the RTL-SDR.
Given below is a bash script for running Kalibrate-RTL. It was developed on a modest Ubuntu Linux system, and tested on run of the mill RTL-SDR hardware. It can get the PPM offset, then save it along with a gain setting and SoapySDR device string for other applications to use. For example, GQRX, OpenWebRX, or Dump1090 can configured with the help of a wrapper script to read the offset and gain before bringing up the SDR for signal collection.
#!/bin/bash # Copyright (c) 2019 by Philip Collier, Radio AB9IL. # This script is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. There is NO warranty; not even for # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # Measure and save rtl-sdr calibration information using GSM base stations. # Edit the file referenced for gain setting by certain applications. # Save SoapySDR device string and key for other uses. Encoding=UTF-8 savedir="/usr/local/etc" get_offset(){ echo "Measuring ppm offset on channel "$best_channel offset=($(kal -c $best_channel 2>&1 | grep "absolute error" | grep -Po "\d*\." | awk '{printf "%.0f", $0}')) if [[ -z "$offset" ]]; then notifyerror fi echo $offset > $savedir/sdr_offset # save offset for general usage sed -i "s/corr_freq=.*/corr_freq=${offset}000000/g" ~/.config/gqrx/default.conf # save offset for gqrx WINDOW=$(zenity --info --height 100 --width 350 \ --title="Calibration and Gain" \ --text="An offset of $offset ppm has been written to file $savedir/sdr_offset."); } notifyerror(){ WINDOW=$(zenity --info --height 100 --width 350 \ --title="Calibration and Gain" \ --text="Something went wrong."); exit } scan_band(){ echo "Please stand by. Scanning for $band stations..." mapfile -t arr < <(kal -v -s $band -g 40 2>&1 | grep 'chan:' | awk '{printf $2" "$7"\n"}' | sort -nrk2) echo "Chan Strength" printf '%s\n' "${arr[@]}" set -- ${arr[0]} best_channel=$1 get_offset } set_device(){ OUTPUT=$(zenity --forms --title="SoapySDR Device Type" --width 400 --height 100 \ --text="Enter the SDR start-up parameters reported by \"SoapySDRUtil --find\"." \ --separator="," \ --add-entry="SoapySDR Device Driver (e.g. rtlsdr):" \ --add-entry="SoapySDR Device Key (e.g. rtl=0):" \ ); if [[ "$?" -ne "0" ]]; then notifyerror fi driver=$(awk -F, '{print $1}' <<<$OUTPUT) devkey=$(awk -F, '{print $2}' <<<$OUTPUT) # write device driver data to the reference file echo $driver > $savedir/sdr_driver # write device key to the reference file echo $devkey > $savedir/sdr_key WINDOW=$(zenity --info --height 100 --width 350 \ --title="SoapySDR Device Type" \ --text="Device driver $driver has been written to file $savedir/sdr_driver. Device key $devkey has been written to file $savedir/sdr_key."); } set_gain(){ OUTPUT=$(zenity --forms --title="Calibration and Gain" --width 400 --height 100 \ --text="Enter the desired SDR gain." \ --add-entry="Gain:"); if [[ "$?" -ne "0" || -z "$?" ]]; then exit fi gain=$(awk -F, '{print $1}' <<<$OUTPUT) # write gain setting to the reference file echo $gain > $savedir/sdr_gain WINDOW=$(zenity --info --height 100 --width 350 \ --title="Calibration and Gain" \ --text="A gain of $gain has been written to file $savedir/sdr_gain."); } set_offset(){ OUTPUT=$(zenity --forms --title="Calibration and Gain" --width 400 --height 100 \ --text="Enter the desired SDR offset (ppm)." \ --add-entry="Offset:"); if [[ "$?" -ne "0" || -z "$?" ]]; then exit fi offset=$(awk -F, '{print $1}' <<<$OUTPUT) echo $offset > $savedir/sdr_offset # save offset for general usage sed -i "s/corr_freq=.*/corr_freq=${offset}000000/g" ~/.config/gqrx/default.conf # save offset for gqrx WINDOW=$(zenity --info --height 100 --width 350 \ --title="Calibration and Gain" \ --text="An offset of $offset ppm has been written to file $savedir/sdr_offset."); } gui(){ ans=$(zenity --list --title "SDR Operating Parameters" --width=500 --height=290 \ --text "Manage RTL-SDR frequency calibration and gain. 1) Calibration uses measurements of GSM base stations. 2) Device gain is saved for reference by other applications." \ --radiolist --column "Pick" --column "Action" \ TRUE "Scan for GSM 850 MHz base stations." \ FALSE "Scan for GSM 900 MHz base stations." \ FALSE "Scan for E-GSM base stations." \ FALSE "Manually program the SDR offset." \ FALSE "Manually program the SDR gain." \ FALSE "Manually program the SoapySDR device data."); if [ "$ans" = "Scan for GSM 850 MHz base stations." ]; then band='GSM850' scan_band elif [ "$ans" = "Scan for GSM 900 MHz base stations." ]; then band='GSM900' scan_band elif [ "$ans" = "Scan for E-GSM base stations." ]; then band='EGSM' scan_band elif [ "$ans" = "Manually program the SDR offset." ]; then set_offset elif [ "$ans" = "Manually program the SDR gain." ]; then set_gain elif [ "$ans" = "Manually program the SoapySDR device data." ]; then set_device fi } case "$1" in gui) gui ;; gsm850) band='GSM850' scan_band ;; gsm900) band='GSM900' scan_band ;; egsm) band='EGSM' scan_band ;; offset) if [[ "$2" -ne "0" || -z "$2" ]]; then exit fi echo "$2" > $savedir/sdr_offset ;; gain) if [[ "$2" -ne "0" || -z "$2" ]]; then exit fi echo "$2" > $savedir/sdr_gain ;; device) if [[ "$2" -ne "0" || -z "$2" ]]; then exit fi echo "$2" > $savedir/sdr_driver if [[ "$3" -ne "0" || -z "$3" ]]; then exit fi echo "$3" > $savedir/sdr_key ;; *) echo "Usage: kal.shgui Use the graphical interface to select a function. gsm850 Scan for GSM 850 MHz base stations. gsm900 Scan for GSM 900 MHz base stations. egsm Scan for E-GSM base stations. offset Manually program the SDR offset . gain Manually program the SDR gain . device Manually program the SoapySDR device data . " >&2 exit 3 ;; esac
It is a simple matter to calibrate a dedicated receiver at boot time, as the script can be executed from a system init script. Here is a snippet of code which would run from a launcher in /etc/xdg/autostart/ or the script /etc/init.d/rc.local:
# calibrate the rtl-sdr sh -c "kal.sh gsm850" &
The above example could also be run regularly as a cron job, but be aware that some scripting would be necessary to stop anther process which may be using the device, then start it up again after the calibration finishes. Calibration of SDR devices with very large offsets may fail, as the cell tower signals may fall outside of the receiver passband. In that case, use your favorite SDR application to manually find the strongest GSM signals, then invoke kal manually to tune one of them.
kal -v -fFor more scrpts and snippets useful for SDR operating, see the Skywave Linux Github repository.-g