Here some test results to evaluate the hardware performance and the use of a pseudo random binary sequence as reference to cross correlate the streams received from the 3radio, a radio composed by  multiple RTL-SDR hardware.

The 3radio has been tuned to 69 MHz center frequency and a sampling rate of 2048000 Hz has been chosen.  3 streams of 100000 IQ sample have been recorded using librtlsdr . The real time length is half a second.

The reference signal was a pseudo noise digital stream at 2 MHz shift rate.

The same signal was routed via separated switching diodes to all the RTL-SDR receivers.

Sequences of  about 3 ms or 6000 bits have been generated. A delay of some hundred of ms separates the different bursts. The reason was to be able to recognize some sync reference just looking to signals in time.


The previous figure shows the IQ streams in time, a chunk of about 6200 samples at 2048000 sampling is selected.

A script in python was written to compute the cross correlation. Here after the script:

# ik1xpv oscar steila 2016
# complex cross correlation of N files with 8 bit rawIQ data (RTL-SDR)

import math
import cmath
import matplotlib.pyplot as plt
import numpy as np
from scipy import signal

signals = [] #array of processed signals

# data raw IQ files [howmany required]
rawfiles = ["testX1", "testX2", "testX3"]

# The raw, captured IQ data is 8 bit unsigned data.
# Each I and Q value varies from 0 to 255.
# To get from the unsigned (0 to 255) range we need to subtract 127.5
# from each I and Q value, which results in a new range from -127.5 to +127.5.
# The complex data is y = I + jQ and we subtract 127.5 +127.5j

for filename in rawfiles:
y = np.fromfile(filename, dtype=np.dtype("u1"))
y = y.astype(np.float32).view(np.complex64)
y -= (127.5 + 127.5j)

ncorr = len(signals)
chunk_length = len(signals[0])
print(str(ncorr) + " sample vectors of " +str( chunk_length) +" samples length" )
for i in range(ncorr):
sig1 = signals[i]
ix = i+1
if ix == ncorr:
ix = 0
sig2 = signals[ix]
print("cross correlation",i,"<->",ix)
ccorr = signal.fftconvolve(sig1, np.conj(sig2[::-1]))
mod = np.linalg.norm(sig1)*np.linalg.norm(sig2[::-1])
print("mod", mod)
print("ccorr lenth", len(ccorr))
peakat = np.argmax(np.abs(ccorr))
print ("max position:", peakat)
print ("ccorr peak value:", cmath.polar(ccorr[peakat]))
print ("normalized value:", cmath.polar(ccorr[peakat])/mod,"\n" )
zcorr = ccorr[peakat - 500: peakat + 500]/mod
ss = "Correlation "+str(i)+"-"+str(ix)

plt.figure("normalized zoom near maximum")
plt.xlabel('note: signals are aligned with the max in position 500')

The cross correlation was computed over the whole streams length of 1000000 samples.

I got the following results and plots:

========= RESTART: C:\Users\Oscar\Documents\Python_code\ =========
3 sample vectors of 1000000 samples length
filenames: ['testX1', 'testX2', 'testX3']
cross correlation 0 <-> 1
mod 1.43562e+08
ccorr lenth 1999999
max position: 1003874
ccorr peak value: (87426948.37116447, -3.0573868558433284)
normalized value: [ 6.08982555e-01 -2.12965829e-08]

cross correlation 1 <-> 2
mod 1.71879e+08
ccorr lenth 1999999
max position: 1002975
ccorr peak value: (124230240.98693986, 1.4627621167409313)
normalized value: [ 7.22777073e-01 8.51041512e-09]

cross correlation 2 <-> 0
mod 1.92693e+08
ccorr lenth 1999999
max position: 993148
ccorr peak value: (131838258.19245286, 1.598828000018114)
normalized value: [ 6.84189098e-01 8.29729322e-09] .


The results look quite good nevertheless the reference signal was far from perfect as you can notice from the cross correlation side lobes.

The time differences of the max positions define the time skewing between the streams. This time depends on the USB latency, on the different starting time of the software application trigger. I hope that the hardware clock skewing is solved with the synchronization.


3radio project - Part 3

3radio project - Part 2

3radio project - Part 1