SDR calibration via GSM FCCH using Kalibrate and LTE-Cell-Scanner on RTL-SDR and HackRF

Radio transceivers, as all physical devices, have some degree of imprecision. Modern crystal oscillators are often built within a tolerance measurable within parts per million (ppm), which can vary due from device to device due to manufacturing imprecision and/or temperature.

16 MHz quartz crystal from Wikipedia

To transmit and receive on the proper frequency, it is important to calibrate. This can be done using expensive test equipment, but fortunately there are signals in the air commonly available throughout the world providing a stable reference frequency to use for calibration instead.

GSM Bands vs ITU Regions

For purposes of radio spectrum allocation, the world is divided in three:

International Telecommunication Union regions 2, 1, and 3 (left to right)

Cellular frequencies differ in each of the ITU regions, GSM frequency bands:

  • Band name (uplink / download frequencies in MHz), #channel numbers
  • GSM-850 (824.2–849.2 / 869.2–893.8), #128–251
  • E-GSM-900 (880.0–915.0 / 925.0–960.0), #975–1023, 0–124
  • DCS-1800 (1710.2–1784.8 / 1805.2–1879.8), #512–885
  • PCS-1900 (1850.2–1909.8 / 1930.2–1989.8), #512–810

What bands you can receive depend on where you are in the world, so make note of it. For this article, I’ll use ITU region 2, which uses the 850 MHz and 1900 MHz bands.

Out of curiosity, I also scanned for the 900 MHz and 1800 MHz bands not used for cellular radio in my area. For some reason, kalibrate detected channel 997 (929.6MHz — 10.641kHz) with power 86276.18, and channel 1002 (930.6MHz — 9.574kHz) power, 79202.53, but attempting to calibrate against them gave wildly disparate results (+15 ppm to -27 ppm) inconsistent with the 850 MHz band where there actually was a GSM signal. shows 930–931 MHz is allocated to narrowband PCS, a 62.5 kHz bandwidth. This is much smaller than the 200 kHz of GSM expected by the tools described below, so they are not expected to work. Additionally 902–928 MHz is the US ISM band, overlapping 902–914 MHz. For proper calibration, stick with the known frequencies available in your region.

You may also see GSM-R, used for railways, but it wasn’t available in my area either, so I’ll only be using 850 MHz here.

Kalibrate, GSM, and FCCH

kalibrate is a tool originally developed by Joshua Lackey to assist in frequency calibration of software-defined radio hardware, by comparing to the GSM’s FCCH channel, mandated to be accurate with 0.05 ppm:

A base station transmits a frequency correction burst on the Frequency Correction CHannel (FCCH) in regular positions. The FCCH repeats every 51 TDMA frames and the frequency correction burst is located in timeslot 0 of frames 0, 10, 20, 30, and 40.

Written first for the Ettus USRP SDR, since ported to RTL-SDR and HackRF:

If you have a USRP you may be able to us the original kalibrate, but if you have another SDR device, read on.

kalibrate-rtl on Raspberry Pi 3 for a NESDR Mini 2+ RTL-SDR

steve-m’s kalibrate-rtl from 2013 is versioned 0.4.1-rtl, and is included in some Linux distributions: at least Kali Linux (added here).

However, it is not available by default in Raspbian Jessie 2016–05–27, the official operating system for the Raspberry Pi I’m using:

pi@raspberrypi:~ $ apt-cache search kalibrate
pi@raspberrypi:~ $

Gareth Hayes released a prebuilt image with kalibrate-rtl and other rtl tools included (rtl_adsb, rtl_fm, rtl_sdr, rtl_test, rtl_eeprom, rtl_power, rtl_tcp, kalibrate, multimon-n), but it was for an older OS version (Raspbian Wheezy, 2014) and I already had Raspbian installed, wouldn’t want to reimage from scratch. Fortunately, it can easily be compiled from source.

There are 40 forks of steve-m’s original 2013 kalibrate-rtl, with new fixes and possibly features, worth investigating but I figured I’d try the original first:

sudo apt-get install automake
sudo apt-get install libtool
sudo apt-get install libfftw3–dev
sudo apt-get install librtlsdr-dev
sudo apt-get install libusb1.0.0-dev
git clone
cd kalibrate-rtl

If it compiles successfully, the binary will be available in ./src/kal.

Let’s try it with the NooElec NESDR Mini 2+, a RTL282U/R820T2 SDR. This device is a couple bucks more than the NESDR Mini 2, but claims to have a more accurate oscillator, accurate within ±0.5ppm. To start, first scan for available GSM channels on a given band:

./src/kal -s GSM850
chan: 236 (890.8MHz — 1.190kHz) power: 28191.40

In my case, channel 236 is being broadcast, so I’ll tune against it using: `./src/kal -c 236`. Result: average absolute error 1.427 ppm. Not bad. Rerunning the command shows the ppm is consistently around this value, from 1.425 ppm to 1.492 ppm or so, at least without powering it on for an extensive period of time. It does increase after time, as we’ll see soon, presumably as the device heats up. Fan cooling could help here.


The newest branch of kalibrate-rtl at the time of this writing is, last modified 2016/03/10. However it doesn’t build with the stock librtlsdr in Raspbian: In member function ‘bool usrp_source::set_dithering(bool)’: error: ‘rtlsdr_set_dithering’ was not declared in this scope
return (bool)(!rtlsdr_set_dithering(dev, (int)enable));

due to this dithering enhancement, requiring a matching librtlsdr. For now, I’ll skip it, although it could be worth investigating at some point.


Try another branch:, developed by the active developer of the project. Hint from these instructions:

You will need a librtlsdr0 package for Raspbian. There is no standard build of this.

Might as well install @mutability’s librtlsdr too. Releases can be manually downloaded from First remove Raspbian’s:

sudo apt-get remove librtlsdr0

then install:

sudo dpkg -i librtlsdr0_0.5.4.git-1_armhf.deb
sudo dpkg -i librtlsdr-dev_0.5.4.git-1_armhf.deb
sudo dpkg -i rtl-sdr_0.5.4.git-1_armhf.deb

clone from git and rebuild. Scanning the GSM850 band, same channel:

chan: 236 (890.8MHz — 1.115kHz) power: 26744.34

Cold Calibration

Powered off the device for an hour. Measured surface temperature (of the plastic enclosure, not the integrated circuit chip itself) using an infrared thermometer at 74.5ºF.

Insert the device, then run in a loop:

while true; do date ; ./src/kal -c 236 ; done

an hour later, temperature raised to 105.5ºF. Overnight, 109.0ºF. No continuous temperature measurements available, but let’s take a look at how the error changes over the course of about 10 minutes:

NooElec NESDR Mini 2+ w/ mutability/kalibrate-rtl

As it “warms up”, the error slightly decreases. Maximum 1.612 ppm, minimum 1.566 ppm, for a range of 0.046 ppm. Compared to the advertised 0.5 ppm, not too far off.

Manual Calibration

It is instructive to view the waveform visually, to see what is going on here. sm313's video “Frequency correction of rtl-sdr using GSM signal and Gqrx”.

chan: 236 (890.8MHz — 1.190kHz) power: 28191.40


rtl_test -p from librtlsdr. This was suggested to get an initial error to specify to kal with -e. However, rtl_test’s technique is very crude: racing the computer’s clock (which may not be accurate) against the RTL’s. Testing with the same NooElec NESDR Mini 2+ on a Raspberry Pi 3 sure enough gives noisy results:

real sample rate: 2047662 current PPM: -165 cumulative PPM: -165
real sample rate: 2047989 current PPM: -5 cumulative PPM: -83
real sample rate: 2048012 current PPM: 6 cumulative PPM: -53
real sample rate: 2048327 current PPM: 160 cumulative PPM: 1
real sample rate: 2047675 current PPM: -158 cumulative PPM: -31

I’ll trust kalibrate-rtl’s measurement of ~1.6 ppm.

What to do with the PPM? gqrx & dump1090

Now that you know the error of the SDR dongle, how can we correct for it?

kalibrate-bladeRF has an option to write the calibration data to flash, for nuand BladeRF devices, but for the low-cost RTL-SDR dongles we’ll have to add the error offset in the client software. GQRX input controls “freq. correction”, allows entering within 1/10 ppm:

gqrx frequency correction

Using dump1090, mutability variant, --ppm flag:

./dump1090 --ppm 2

Only integer values are accepted, would like to enter 1.6 but have to round to 2. This value can also be specified in /etc/default/dump1090-mutability:

# RTLSDR frequency correction in PPM


Next device I’ll calibrate, a HackRF One. There is another branch of kalibrate for the HackRF, developed by scateu aptly named kalibrate-hackrf. Scan for channels on GSM850:

kalibrate-hackrf $ ./src/kal -s GSM850
kal: Scanning for GSM-850 base stations.
chan: 139 (871.4MHz + 17.827kHz) power: 10133959.12

Scan again:

kalibrate-hackrf $ ./src/kal -s GSM850
kal: Scanning for GSM-850 base stations.
chan: 141 (871.8MHz + 19.723kHz) power: 10445127.43

Unclear why I am receiving different channels. Others have reported issues, possibly related to differences between HackRF Jawbreaker and HackRF One (signed/unsigned IQ data), but that may or may not be the issue, would require further investigation.

Until then, I sought alternate means to calibrate the HackRF: via the LTE cellular signals.

Calibrating HackRF with LTE-Cell-Scanner

GSM (as used with Kalibrate) is for the old 2G cellular networks, UTMS for 3G, and LTE (Long Term Evolution) for 4G, currently the latest and greatest.

Hint from /u/superkuh:

Yep. Kal uses 270k of bandwidth for GSM reception and if the error of the dongle is too large the FCCH-peak is outside the range and it’ll give you incorrect results.

If there’s LTE in your rtlsdr tuner’s range I recommend using LTE Cell Scanner to get your frequency error instead of Kal. It works much more reliably.

LTE-Cell-Scanner and tracker. Originally by Evrytania, but significantly enhanced by JiaoXianjun. dholm/homebrew-sdr has a formula for installation on OS X, but it only built LTE-Cell-Scanner with RTL-SDR support, not the optional HackRF and BladeRF support.

Installing on OS X using Homebrew:

brew tap rxseger/homebrew-hackrf
brew install rxseger/hackrf/lte-cell-scanner --HEAD

Run the search, per the readme:

CellSearch --freq-start 715e6 --freq-end 768e6

CellSearch will list the cells it finds, along with the crystal correction factor. The PPM value can be calculated as 1e6*(1-x), using this patch from cgommel (added in

Detected the following cells:
DPX:TDD/FDD; A: #antenna ports C: CP type ; P: PHICH duration ; PR: PHICH resource type
DPX CID A fc freq-offset RXPWR C nRB P PR CrystalCorrection ppm
FDD 117 2 731.6M -5.66k -20 N 25 N one 0.999992268251413 -7.73
FDD 237 2 751.1M -5.74k -19.7 N 50 N one 0.999992357264591 -7.64

The “CrystalCorrection” factor can be passed to LTE-Tracker with the --correction flag:

LTE-Tracker --freq 751000000 --correction 0.999992357264591

or equivalently in PPM, using the positive value:

LTE-Tracker --freq 751000000 --ppm 7.73

The tracker should lock on to the frequency quickly, given the correct error correction value, and show the cells.

Configuring PPM in CubicSDR

Using CubicSDR on OS X, as an alternative to GQRX. Overall I find CubicSDR to have improved usability compared to GQRX, although somewhat less functionality. To configure PPM, hold down the Option/Alt key over the Frequency slider, it changes to Device PPM:

CubicSDR, configuring Device PPM (holding down Option key)

To confirm, tuning to a NOAA weather station, 162.450 MHz and 162.550 MHz narrow-band FM shown here.


The NESDR had less error, as expected due to its temperature-compensated oscillator (TXCO). However, the RTL chip has limited frequency range and bandwidth compared to the HackRF. Either way, GSM and LTE cellular signals can be used to successfully calibrate within a few parts per million.