So I have a small NooElec Software Defined Radio and I've got it working well with my Raspberry Pi using GQRX as a controller and to visualize the SDR output.

My goal though is to use a python script to listen for pulses (10ms long every 1s) from small VHF beacons on animals that we've tagged. These tags give out a ping around 151MHz and I've been able to find them fine in my GQRX output but I would like to use the pyrtlsdr tool and matplotlib's mlab functions to pull this into python so I can trigger some other equipment once that signal pulse is detected above a certain strength. Basically I'm using signal strength as a proxy for distance to zero in on the tag. My experience with RF is pretty limited but I think this should be a relatively basic operation.

So I ingest the samples from the SDR with pytrlsdr use mlab.magnitude_spectrum() to convert that to signal strength and frequency in order to find out whether or not the signal meets my threshold and this sort of works but I feel like I'm pushing through the dark and wanted to double check my methods and see what else I need to check for when running through this procedure.

Specific Questions:

  • What is the ideal sampling rate? How can I calculate this?
  • Is anything about this method going to hurt my detection radius, as usual it is critical that is a large as possible. Any way to improve the detection radius? I assume I need to filter some noise but I'm not quite sure of the best method.
  • I'm not exactly sure how the find the peak but have been using around with this code from github, is this appropriate for peak finding in this situation?

So here is my current method:

from pylab import *
from rtlsdr import *
import numpy as np
from scipy import signal
import peakdetect 

sdr = RtlSdr()

tag_list = [151.129, 151.029, 151.229]

# configure device and settings
sdr.sample_rate = 2.6e5     # Hz
sdr.center_freq = 151e6 + 0.05e6 # Hz, offset because of artifact on central frequency
sdr.gain = 8
num_samples = 1024*1024

samples = sdr.read_samples(num_samples)
# use matplotlib to estimate and plot the PSD
power, psd_freq = psd(samples, NFFT=1024, Fs=sdr.sample_rate/1e6, 
xlabel('Frequency (MHz)')
ylabel('Relative power (dB)')


power_db = 10*np.log10(power)

maxima, minima = peakdetect.peakdet(v=power_db, delta=1, x=psd_freq)


# > [[ 151.09901953  -38.86662917]
# > [ 151.12898047  -17.83309643]
# > [ 151.179       -17.72161527]
# > [ 151.22394141  -32.65342774]
# > [ 151.22901953  -24.08179541]
# > [ 151.25898047  -39.26995763]]


proximity = .002 # Hz, offset because the tags drift a bit and the SDR isn't exact

for tag_freq in tag_list:
    for mx in maxima:
        if abs(tag_freq - mx[0]) < proximity:
            print('Near tag', tag_freq, mx[0])


# > ('Near tag', 151.129, 151.12898046835249)
# > ('Near tag', 151.229, 151.22901953164751)


I certainly have some reading up to do on RF Signal Analysis but I feel like I'm close to a working system.

  • 2
    $\begingroup$ You will get much better detection performance if you filter out as much of the noise as possible and use a tracking PLL to act as frequency tracking correlator to enhance the received signal pulse. I helped Dirk Gorrissen with something very similar last year: lists.gnu.org/archive/html/discuss-gnuradio/2017-03/… $\endgroup$
    – Andy Walls
    Jun 25, 2018 at 1:03
  • $\begingroup$ Can you please talk a little bit more about the tags? What is their output power? Are they simply beacons or do they emit information too? Are they self synchronising? Do you want to be able to read the information they emmitt or just pick up the ping? What sort of antenna do you have mounted on your SDR? $\endgroup$
    – A_A
    Jun 25, 2018 at 9:14
  • $\begingroup$ Thanks @AndyWalls I will definitely read through that chain. $\endgroup$
    – clifgray
    Jun 25, 2018 at 17:14
  • $\begingroup$ @A_A Here is the spec sheet for the tags: lotek.com/pinpoint-gps.htm we have the Pinpoint 75. I don't have much more info than what is present there. They emit a beacon and information but I'm only trying to listen to the beacon. My SDR will have a small H antenna on it. Let me know if more info would be helpful and I'll try to find it! $\endgroup$
    – clifgray
    Jun 25, 2018 at 17:17
  • 1
    $\begingroup$ @AndyWalls Can you please turn the comments and your suggestion to that mailing list a full answer here? This might be useful for other's too. Clifgray, there are a few things that you can do with tracking these beacons, both using SDR and the off the shelf receivers (in terms of automation). It would be good to see Andy Walls' response too since that is specific to SDR. What do you use to receive these tags currently? $\endgroup$
    – A_A
    Jun 26, 2018 at 11:08

1 Answer 1


As far as the big picture goes, I assume you want to do both optimal detection of pulses and then tracking of the transmitters based on the detections you receive. This answer will only deal with taking steps to optimize detection using a low cost RTL-SDR, and then I'll address some of your specific questions in your post.

In AWGN, the optimal detection filter is a matched filter, also called a correlator or a pulse compressor. Creating a matched filter is conceptually very easy. If $p[n]$ is a sequence of $N$ samples representing the pulse you are trying to detect, the matched filter $h[n]$ is the time-reversed, complex conjugate of the pulse, so $h[n] = p^*[(N-1)-n]$.

Let's take a look at how your SDR is going to present received pulses to you (assuming, for now, you haven't done any downsampling):

$F_s = 2.6 \space\mathrm{Msps} \\ t_p = 10 \space \mathrm{ms}\\ f_p = 50 \space \mathrm{kHz}\\ N = t_p \cdot F_s = 26,000 \space \mathrm{samples}$

$$p[n] = A e^{j\left(2\pi \frac{f_p}{F_s} n + \phi_0\right)} \quad \mathrm{for} \space n \in \{0, 1, \ldots, N-1\}$$

Thus the matched filter would be

$$h[n] = A e^{-j\left(2\pi \frac{f_p}{F_s}(N-1-n) + \phi_0\right)} \quad \mathrm{for} \space n \in \{0, 1, \ldots, N-1\}$$

Ideally, when filtering the ideal incoming pulse $p[n]$, when the pulse and the filter exactly line up, the filter output peaks at

$$\max_n y[n] = \max_n \left(p[n]*h[n]\right) = A^2\sum_{k=0}^{N-1} |e^0|^2 = A^2N$$

That peak should be well above the original received pulse and the filtered AWGN.

Now for the first bits of bad news. The RTL-SDR crystal is not very precise or stable, so you won't precisely know $f_p$ ahead of time, nor will $F_s$ be exactly what you specify in real life. The transmitter on the wildlife tag is also not very precise, which also contributes to $f_p$ being mismatched. The transmitter on the wildlife tag is independent of your receiver, operating at some unknown distance and power-level, so you won't have $A$ as the received amplitude and the initial phase you receive will not be $\phi_0$.

The good news is that you can use a PLL to estimate and track $f_p$ and $\phi_0$, and also effectively correct for $F_s$ not being exact.

Normally one would use the estimates from the PLL to correct the incoming signal and then pass it to the matched filter to create peaks for detection. However, since in your special case, your pulse is an unmodulated tone, you can take a shortcut and use the output of the PLL to implement an adaptive matched filter, instead of the normal steps of performing correction and then passing through a fixed matched filter. This is what is going on in this section of a GNURadio flowgraph pictured below:

enter image description here

The "PLL Ref Out" block locks on to the strongest tone in the incoming signal and outputs a phase locked tone. The "Multiply Conjugate" and "Moving Average" blocks implement the matched filtering operation, since the "Moving Average" block with a scale factor of 1 is just rolling summation. The "Complex to Mag" block eliminates the effect of any residual phase offset from the "PLL Ref Out" block.

Below is a plot of the matched filtered output vs. the input data stream, showing the matched filter peaks from two pulses which are about 1.5 seconds apart:

enter image description here

The following picture is a zoom of the second pulse, to show the input pulse:

enter image description here

Once you have these matched filtered/correlation peaks, you can use whatever adaptive thresholding/outlier detection technique you want to detect the peaks from the noise. I'll suggest Consecutive Mean Excision on the time domain data, if you don't have a favorite adaptive thresholding algorithm. You can also use a high level "PLL" to track pulses that you think will be about 1 seconds apart to track a single transmitter. The "PLL Ref Out" block I use also provides a frequency estimate output, so you can distinguish between transmitters at slightly different frequencies.

OK, now to step back an take a look at the whole GNURadio flowgraph I use:

enter image description here

Especially the filtering and downsampling stages:

enter image description here

To get the best noise performance from SDRs, it is best to use the highest sample rate the SDR provides, and then filter and downsample to the narrowest bandwidth possible that still contains your signal of interest plus any allowance for frequency offset errors.

Note that I have three stages of decimating filters and one final channel filter. I broke the filtering and downsampling down into 3 stages of /4, /8, and /8 to keep FIR filter lengths and CPU utilization manageable. Filtering and downsampling by /256 in one stage would have resulted in an extremely long filter and consumed much more CPU. I have a fourth filter stage just to eliminate as much noise as I could, and still be sure to capture the pulse on one particular channel of the channels that the wildlife tags can use. Note that the frequency translating FIR filter in the first stage can be used to shift to a different channel frequency within the band used by the wildlife tags.

So now for final bits of bad news. SDR RF frontends are horrible in that they are wide open with very broad analog filters, if they have any at all. They let in all sorts of RF interferers, some of them strong enough to clip inside the amplifiers and create intermodulation products that effectively raise your noise floor. Cheap SDR units are not shielded from EMI and often bring in noise or interference from nearby electronics, such as the computer they are plugged into and its USB or Ethernet interface. RTL-SDR units have gains in the RF frontend that can affect your noise performance -- RF Gain, Mixer Gain, IF Gain, and Baseband Gain -- and you are never provided enough information to set them properly for best performance. The auto gain setting is usually suboptimal. The LO leaks into the RF or IF front end and mixes in showing up as a mixing product at DC. The direct downconversion can have I/Q imbalance, cause "ghost" signals reflected about DC from real signals in the spectrum.

If you want the best performance, you have to deal with the above problems with the RF frontend. I recommend:

  1. Get a higher quality SDR. RTL-SDR units will always be poor performers.
  2. Use a USB extension cable and get the RTL-SDR away from the computer and any nearby sources of VHF interference.
  3. Use copper foil tape connected to a ground to shield the RTL-SDR unit, which is normally only in a plastic case.
  4. Use a directional antenna.
  5. Add an external LNA and SAW filter to limit the amount of analog RF bandwidth going in to the front end.
  6. Set the mixer gain to the nominal mid level and leave it alone as long as the unit appears to be downconverting signals properly.
  7. Set the RF Gain as high as you can, before clipping your signals of interest in the IF stage.
  8. Set the IF Gain stage to its lowest setting at first and then increase until just before clipping your signals of interest in the baseband stage.
  9. Don't tune the RTL-SDR exactly to your frequency of interest, but offset by at least a few kHz, to avoid the DC spike corrupting your signal of interest and pulling off your PLL.
  10. Most RTL-SDRs that I've used can't support 2.6 Msps reliably. I believe 2.4 Msps is the safe, maximum rate across all RTL-SDR variants.

I'll stop this rambling answer here. I hope this helps.

  • 1
    $\begingroup$ Thanks @Andy Walls for the helpful and comprehensive answer! This is a little over my head but I think with a week or so of noodling on this I'll be able to get it all working. A couple of your suggestions applied to my more brute force solution above, such as moving down to 2.4 Msps, adding a directional antenna, and adding some shielding, has already significantly improved my ability to detect tags. Now to work on implementing the meat of the solution! Do you have any suggestions for getting up to speed on GNURadio? Just the default wiki.gnuradio.org/index.php/Tutorials ? $\endgroup$
    – clifgray
    Jul 9, 2018 at 22:48
  • $\begingroup$ I want to do exactly the same thing - not quite sure this answer although detailed will get it going. $\endgroup$
    – Al Grant
    Nov 16, 2023 at 8:11
  • $\begingroup$ What values are in your firdes low pass filters? $\endgroup$
    – Al Grant
    Nov 17, 2023 at 4:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.