"""
Note:
I consider usrp_spectrum_sense.py as the best given working example in the gnuradio project. I think also that the understanding of this program is very important to every gnuradio user (because it includes a practical FFT implementation + FSM control in its cpp code)
With competitive price and timely delivery, Highmesh sincerely hope to be your supplier and partner.
Introduction:
-------------
1) This program can be used as a basic code for implementing wideband spectrum analyzer.
2) As we know, the USRP cannot examine more than 8 MHz of RF spectrum due to USB bus limitations.
3) So, to scan across a wide RF spectrum band (bigger than 8 MHz) we have to tune USRP RF front end in suitable steps so that we can examine a lot of spectrum, although not all at the same instant.
4) The usrp_spectrum_sense shows the way how it can be done.It steps across the spectrum and make the RF measurements. This application can
sense a large bandwidth, but not in real time, and it can do the frequency sweep over the required frequency range,
Theory:
-------
1) To use N points complex FFT X(W) analysis, we have to get N time samples x(t) which are sampled at Fs.
2) These N time samples must be time windowed using a known window function to reduce spectral leakage.
3) Performing N points complex FFT analysis.
4) The output of the complex FFT will represent the frequency spectrum contents as follows:
a) The first value of the FFT output (bin 0 == X[0]) is the passband center frequency.
b) The first half of the FFT (X[1] to X[N/2-1] contains the positive baseband frequencies,which corresponds to the passband spectrum from the center frequency out to the maximum passband frequency (from center frequency to +Fs/2).
c) The second half of the FFT (X[N/2] to X[N-1]) contains the negative baseband frequencies,which correspond to the lowest passband frequency up to the passband center frequency (from -Fs/2 to center frequency).
You will get efficient and thoughtful service from Highmesh.
Additional resources:
Example
-------
Let us assume that we have (I and Q) samples gathered using a tuner centered at 20MHz. And let us assume that the sampling frequency was 8MHz.
Doing points complex FFT means:
1) FFT Frequency resolution is : 8MHz / = .5 KHz
2) The output of the FFT X[0] represents the spectrum at 20MHz.
3) The output of the FFT X[1] to X[511] represents the frequencies from 20. MHz to 23. MHz (about 4MHz above center frequency).
4) The output of the FFT X[512] to X[] represents the frequencies from 16. MHz to 19. MHz (about 4MHz bellow center frequency).
RF Frequency Sweeping
---------------------
1) Let us suppose that we want to scan RF spectrum band from 10MHz to 52 MHz.
2) Let us remember that USRP can analyze 8MHz of frequency at a time.
3) So theoretically we have to step our RF center frequency as follows:
First step is 14MHz (it will cover frequency band from 10MHz to 18MHz),
Second step is 22MHz (it will cover frequency band from 18MHz to 26MHz),
Third step is 30MHz (it will cover frequency band from 26MHz to 34MHz),
Fourth step is 38MHz (it will cover frequency band from 34MHz to 42MHz),
Fifth step is 46MHz (it will cover frequency band from 42MHz to 50MHz),
and finally the Sixth step is 54MHz (it will cover frequency band from 50MHz to 58MHz). Remember that we want the frequencies up to 52MHz only, so we have to discard some FFT points from the Sixth analysis.
4) Paralytically we have to use FFT overlapping to reduce the non linearity response of the Digital Down Converter (the DDC frequency response is not Flat from -Fs/2 to + Fs/2) and to fill the frequency holes that will be present at the FFT analysis edges (10MHz, 18MHz, 26MHz, 34MHz, 42MHz, 50 MHz).
So if we choose to use an overlap of 25%, this means that our step size will be 6MHz (8MHz*(1-.25)), thus practically we have to step our RF center frequency as follows:
First step is 13MHz (it will cover frequency band from 9MHz to 17MHz),
Second step is 19MHz (it will cover frequency band from 15MHz to 23MHz),
Third step is 25MHz (it will cover frequency band from 21MHz to 29MHz),
Fourth step is 31MHz (it will cover frequency band from 27MHz to 35MHz),
Fifth step is 37MHz (it will cover frequency band from 33MHz to 41MHz),
Sixth step is 43MHz (it will cover frequency band from 39MHz to 47MHz),
and finally the Seventh step is 49MHz (it will cover frequency band from 45MHz to 53MHz),
Changing RF center Frequency
----------------------------
1) To change USRP RF center frequency we have to send a tunning command to the USRP every time we complete the analysis of the current frequency chunk.
2) Before gnuradio revision [], all USRP RF daughterboards tunning were done using Python functions and classes. After that revision, tunning the USRP daughterboards from withen C++ code is possible.
3) In usrp_spectrum_sense.py, the DSP C++ written code is allowed to transparently invoke Python code USRP tune function. This tunning control is done in gr_bin_statistics_f sink function.
Tunning Delay Problem:
---------------------
When we command the usrp RF daughterboard to change its center frequency, we have to wait until (right) ADC samples arrive to our FFT engine and we have to insure that it belongs to the wanted center frequency. This represents a problem since there are many delays along the digitization path (RF synthesizer settling time, and pipeline propagation delay [FPGA FIFO filling time, USB transferring time...etc]). To overcome this problem we have to use enough tune delay time in order to be sure that the samples entering our FFT block are belong to the requested center frequency. This is done simply by dropping the incoming received samples over a specified tunning delay time.
usrp_spectrum_sense Implementation
----------------------------------
1) The engine of the usrp_spectrum_sense depends mainly on bin_statistics sink function.
2) bin_statistics function combines statistics gathering with a state machine for controlling the USRP RF tuning (frequency sweeping). It determines max values (keeps track of the maximum power in each FFT bin) of vectors (with length vlen) over a time period determined by dwell_delay (after converting it to a number of FFT vectors). This operation is performed after discarding tune_delay samples.
3) After processing N = dwell_delay samples, bin_statistics composes a message and inserts it in a message queue.
4) Each message from bin_statistics consists of a vector of max values, prefixed by the center frequency corresponding to the associated samples,
i.e., it is the center frequency value of the delivered input samples to bin_statistics.
Choosing Tune and Dwell delay times
----------------------------------
1) We have to play with the --tune-delay and --dwell-delay command line options to determine appropriate timming values. The most important one is the tune delay time.
2) The choose of tune-delay should include time for the front end PLL to settle, plus time for the new samples to propagate through the pipeline. The default value is 1ms, which is probably in the ballpark on the RFX** boards. The TV RX board is much slower. The tuner data sheets says it could take 100ms to settle.
3) The tune delay timing parameter passed to bin_statistics is calculated in FFT frames which depends on USRP rate and FFT length as in :
tune_delay_passed_to_bin_statistics = int(round(required_tune_delay_in_sec*usrp_rate/fft_size))
if this calculated value is less than "1", then we should make it at least "1" FFT frame.
For example:
If the :
required_tune_delay_in_sec = 10e-3
and usrp_rate = (decimation =8)
and FFT size is
Then :
tune_delay_passed_to_bin_stats = 78 (FFT Frames)
This means we have to skip 78 incoming vectors (FFT frames) before we actually use the acquired samples in our spectrum statistics.
4) Beside tunning time depends on the hardware (RF synthesizer speed),one should remember that the time needed to collect samples
with decimation rate=8 (minimum USRP decimation) is 128 usec, while the time needed to collect samples with decimation rate=256 (maximum USRP decimation) is 4.096 msec.
This means that the tune delay in the case of decimation rate =256 should be larger than that used for decimation = 8.
4) A working tune delay value (which gives accurate results) can be known by experiments (for given decimation rate and FFT length).
Interrupting Output Spectrum
-----------------------------
The actual mapping from the levels at the daughterboard antenna input port to the output analysis values depends on a lot of factors including the used daughterboard RF gain and decimation specific gain in the digital down converter. You'll need to calibrate the system if you need something that maps to dBm.Currently, the output of usrp_spectrum_sense is the magnitude squared of the FFT output. That is, for each FFT bin[i], the output is Y[i] = re[X[i]]*re[X[i]] + im[X[i]]*im[X[i]]. If you want power, take the square root of the output.
"""
from gnuradio import gr, gru, eng_notation, optfir, window
from gnuradio import audio
from gnuradio import usrp
from gnuradio.eng_option import eng_option
from optparse import OptionParser
from usrpm import usrp_dbid
import sys
import math
import struct
import time
import os
class tune(gr.feval_dd):
"""
This class allows C++ code (bin_statistics_f.cc) to callback into python to change USRP RF center Frequency.
"""
def __init__(self, tb):
gr.feval_dd.__init__(self)
self.tb = tb
def eval(self, ignore):
"""
This method is called from gr.bin_statistics_f when it wants to change
the center frequency. This method tunes the front end to the new center
frequency, and returns the new frequency as its result.
"""
try:
# We use this try block so that if something goes wrong from here
# down, at least we'll have a prayer of knowing what went wrong.
# Without this, you get a very mysterious:
#
# terminate called after throwing an instance of 'Swig::DirectorMethodException'
# Aborted
#
# message on stderr. Not exactly helpful ;)
new_freq = self.tb.set_next_freq()
return new_freq
except Exception, e:
print "tune: Exception: ", e
"""
Class to parse the incomming messages sent by bin_statistics.
1) It extracts the center frequncy and the incomming data vector length
2) Convert received data string to a floating point format.
3) Store the output in the "data" array
"""
class parse_msg(object):
def __init__(self, msg):
self.center_freq = msg.arg1()
self.vlen = int(msg.arg2())
assert(msg.length() == self.vlen * gr.sizeof_float)
Want more information on HM USRP E Series? Feel free to contact us.
# FIXME consider using Numarray or NumPy vector
t = msg.to_string()
self.raw_data = t
self.data = struct.unpack('%df' % (self.vlen,), t)
class my_top_block(gr.top_block):
def __init__(self):
gr.top_block.__init__(self)
usage = "usage: %prog [options] min_freq max_freq"
parser = OptionParser(option_class=eng_option, usage=usage)
parser.add_option("-R", "--rx-subdev-spec", type="subdev", default=(0,0),
help="select USRP Rx side A or B (default=B)")
parser.add_option("-g", "--gain", type="eng_float", default=None,
help="set gain in dB (default is midpoint)")
parser.add_option("", "--tune-delay", type="eng_float", default=1e-3, metavar="SECS",
help="time to delay (in seconds) after changing frequency [default=%default]")
parser.add_option("", "--dwell-delay", type="eng_float", default=10e-3, metavar="SECS",
help="time to dwell (in seconds) at a given frequncy [default=%default]")
parser.add_option("-F", "--fft-size", type="int", default=256,
help="specify number of FFT bins [default=%default]")
parser.add_option("-d", "--decim", type="intx", default=16,
help="set decimation to DECIM [default=%default]")
parser.add_option("", "--real-time", action="store_true", default=False,
help="Attempt to enable real-time scheduling")
parser.add_option("-B", "--fusb-block-size", type="int", default=0,
help="specify fast usb block size [default=%default]")
parser.add_option("-N", "--fusb-nblocks", type="int", default=0,
help="specify number of fast usb blocks [default=%default]")
(options, args) = parser.parse_args()
if len(args) != 2:
parser.print_help()
sys.exit(1)
self.min_freq = eng_notation.str_to_num(args[0])
self.max_freq = eng_notation.str_to_num(args[1])
if self.min_freq > self.max_freq:
self.min_freq, self.max_freq = self.max_freq, self.min_freq # swap them
self.fft_size = options.fft_size
if not options.real_time:
realtime = False
else:
# Attempt to enable realtime scheduling
r = gr.enable_realtime_scheduling()
if r == gr.RT_OK:
realtime = True
else:
realtime = False
print "Note: failed to enable realtime scheduling"
# If the user hasn't set the fusb_* parameters on the command line,
# pick some values that will reduce latency.
if 1:
if options.fusb_block_size == 0 and options.fusb_nblocks == 0:
if realtime: # be more aggressive
options.fusb_block_size = gr.prefs().get_long('fusb', 'rt_block_size', )
options.fusb_nblocks = gr.prefs().get_long('fusb', 'rt_nblocks', 16)
else:
options.fusb_block_size = gr.prefs().get_long('fusb', 'block_size', )
options.fusb_nblocks = gr.prefs().get_long('fusb', 'nblocks', 16)
#print "fusb_block_size =", options.fusb_block_size
#print "fusb_nblocks =", options.fusb_nblocks
# build graph
self.u = usrp.source_c(fusb_block_size=options.fusb_block_size,
fusb_nblocks=options.fusb_nblocks)
adc_rate = self.u.adc_rate() # 64 MS/s
usrp_decim = options.decim
self.u.set_decim_rate(usrp_decim)
usrp_rate = adc_rate / usrp_decim
self.u.set_mux(usrp.determine_rx_mux_value(self.u, options.rx_subdev_spec))
self.subdev = usrp.selected_subdev(self.u, options.rx_subdev_spec)
#print "Using RX d'board %s" % (self.subdev.side_and_name(),)
s2v = gr.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
mywindow = window.blackmanharris(self.fft_size)
fft = gr.fft_vcc(self.fft_size, True, mywindow)
#This loop is calculating the gain of the applied window
power = 0
for tap in mywindow:
power += tap*tap
c2mag = gr.complex_to_mag_squared(self.fft_size)
# use c2mag = gr.complex_to_mag(self.fft_size) if you want the power
# FIXME the log10 primitive is dog slow
log = gr.nlog10_ff(10, self.fft_size,
-20*math.log10(self.fft_size)-10*math.log10(power/self.fft_size))
# Set the freq_step to 75% of the actual data throughput.
# This allows us to discard the bins on both ends of the spectrum.
self.freq_step = 0.06 *1e9
self.min_center_freq = self.min_freq
nsteps = math.ceil((self.max_freq - self.min_freq) / self.freq_step)
self.max_center_freq = self.max_freq
self.next_freq = self.min_center_freq
tune_delay = max(0, int(round(options.tune_delay * usrp_rate / self.fft_size))) # in fft_frames
dwell_delay = max(1, int(round(options.dwell_delay * usrp_rate / self.fft_size))) # in fft_frames
self.msgq = gr.msg_queue(16)
self._tune_callback = tune(self) # hang on to this to keep it from being GC'd
stats = gr.bin_statistics_f(self.fft_size, self.msgq,
self._tune_callback, tune_delay, dwell_delay)
# FIXME leave out the log10 until we speed it up
#self.connect(self.u, s2v, fft, c2mag, log, stats)
self.connect(self.u, s2v, fft, c2mag, stats)
if options.gain is None:
# if no gain was specified, use the mid-point in dB
g = self.subdev.gain_range()
options.gain = float(g[0]+g[1])/2
self.set_gain(options.gain)
#print "gain =", options.gain
def set_next_freq(self):
target_freq = self.next_freq
self.next_freq = self.next_freq + self.freq_step
if self.next_freq > self.max_center_freq:
self.next_freq = self.min_center_freq
if not self.set_freq(target_freq):
print ""
print "Failed to set frequency to", target_freq
return target_freq
def set_freq(self, target_freq):
"""
Set the center frequency we're interested in.
@param target_freq: frequency in Hz
@rypte: bool
Tuning is a two step process. First we ask the front-end to
tune as close to the desired frequency as it can. Then we use
the result of that operation and our target_frequency to
determine the value for the digital down converter.
"""
return self.u.tune(0, self.subdev, target_freq)
def set_gain(self, gain):
self.subdev.set_gain(gain)
def main_loop(tb):
global free_band,status
j=0
while 1:
j= j + 1
# Get the next message sent from the C++ code (bin_statistics)
# It contains the center frequency and the mag squared of the fft
m = parse_msg(tb.msgq.delete_head()) # This is a blocking call.
# Print center freq so we know that something is happening...
signalPower = 0
noise_floor =
for bin in m.data:
signalPower += bin
print signalPower
if signalPower>noise_floor and j > 1:
print"PU Present"
print m.center_freq
free_band = m.center_freq
signalPower = 0
print"Checking next frequency band "
status =1
break
if signalPower
print"PU absent"
print m.center_freq
free_band = m.center_freq
signalPower = 0
status =0
break
# FIXME do something useful with the data...
# m.data are the mag_squared of the fft output (they are in the
# standard order. I.e., bin 0 == DC.)
# m.raw_data is a string that contains the binary floats.
# You could write this as binary to a file.
> You can do that (obviously, it works!), but I'd recommend you just use
> a channelizer with N=125 instead of 117! Your channel map can be used
> to ignore the channels you don't need (which seem to be the 4 upper and
> lower "edge" ones), and by only increasing the workload of the PFB ever
> so slightly (probably even actually decreasing it, because an 125=5³-
> FFT is likely to be much more efficient than an 117=3²·13-FFT), you get
> rid of the rational resampler, which not only brings more CPU load, but
> also a resampling filter response that might have ripple and aliases.
Thanks for that suggestion, Marcus! I have the resampler version
running right now but will change to use the channel count idea instead.
John
_______________________________________________
Discuss-gnuradio mailing list
https://lists.gnu.org/mailman/listinfo/discuss-gnuradio
First, thanks for catching the typo in the channel map. There was no
plan to skip any channels; the goal is to get each channel in frequency
order (out0 = 540 kHz; out116 = kHz).
I did an experiment using a signal generator feeding the Red Pitaya
receiver and testing various channels. I started at the center channel,
kHz, and found there was zero offset from nominal. I then went 1,
2, and 3 channels both lower and higher and came up with this map:
+2.05 kHz
+1.37 kHz
+0.68 kHz
0.00 kHz
-0.68 kHz
-1.37 kHz
-2.05 kHz
Now, the sample rate coming out of the channelizer (hardware sample rate
of 1.25 msps divided by 117) is ~10.683 ksps. Gee, that 683 is pretty
close to the per-channel offset.
I didn't test further, but I strongly suspect this offset of ~680
Hz/channel continues in both directions, so that the signal has moved
far from the expected point as you get further away.
So, I think this tells me that the sample rate going into the PFB,
divided by the number of channels, needs to match the channel spacing.
That means a rational resampler going into the PFB to change 1.25 msps
to 1.17 msps.
I don't think I've ever read anything before that the sample_in rate has
to equal (channel_num x channel_spacing). It makes perfect sense when
you think about it, though. Your reference to the "channel raster" was
enough to make the light bulb turn on for me, and thanks very much for that!
Now to test with the resampler... Thanks much for helping me work
through this!
John
----
On 11/29/ 01:20 PM, Müller, Marcus (CEL) wrote:
> Hi John,
>
> given the fact that the frequency shifting of the individual sub-
> filters is actually done via a DFT implemented by an FFT, and that
> should have negligible phase accumulation error for benign FFT length
> (i.e. channel numbers, let's say <1e6), hm.
>
> First gut feeling, and easiest to check:
>
>> The output channels seem to have an offset in the range > of 400 to
> 700 Hz versus the unchannelized input.
>
> Make double sure that (sampling rate going into the channelizer) / 117
> is actually exactly the raster you want.
>
> If I interpret your file correctly, there's 1.25 MHz going into the
> channelizer, so channel raster is / 117 kHz = 10.684 kHz. Hope
> that's right!
>
> What I could imagine are artifacts due to the non-perfect filter phase
> linearity; but that would actually be a pretty intense speculation;
> unless we're leaving the areas where our floating point numbers are
> accurate enough, there shouldn't be non-linear (i.e. frequency-
> shifting) behaviour.
>
> I just threw together this:
>
> from gnuradio.filter import firdes
>
> file_sample_rate = 2.5e6
> decimation = 2
>
> pfb_taps = firdes.low_pass(2.0, file_sample_rate/decimation,,,
> firdes.WIN_HAMMING, 6.76)
>
> (pretty much lifted straight from your GRC)
>
> Inspecting pfb_taps yielded a length of 603 taps, which we'll "evenly
> as possible" have to distribute across the channels (117 of which
> exist, as your channelizer map has length 116, but is missing the 56).
>
> You could build a simple "unit test": Use a Vector source that pushes
>
> [1 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 4 … 0 0 0 0 0 0 0 117
> 0 0 0 0 0 0 0 ]
>
> through a length=117*8 IFFT, and push the result (after a vector-to-
> stream) through your channelizer. You should see single tones in all
> your channels. (The different amplitudes might help telling them
> apart). Do they end up in the center of your bins?
>
> Best regards,
> Marcus
>
> On Wed, -11-29 at 10:34 -, John Ackermann N8UR wrote:
>> I'm building a ridiculous flowgraph that breaks the AM broadcast band
>> (540 - kHz in the U.S.) into 117 10 kHz wide channels and measures
>> the energy in each. The thing is working but I see a frequency offset
>> in the output channels that is not present in the data before
>> channelizing. The output channels seem to have an offset in the range
>> of 400 to 700 Hz versus the unchannelized input.
>>
>> The signal chain is:
>>
>> 2.5 msps recording centered at 1.4e6 Hz -> xlating filter, decimation 2,
>> output centered at 1.12e6 -> PFB channelizer with 117 channels, yielding
>> a channel rate of 10,683....... samples per second.
>>
>> Looking at the spectrum at the output of the xlating filter, the carrier
>> frequencies are correct. Looking at the output of a channel, the
>> carriers are offset by several hundred Hertz, always high. (Given the
>> absolute frequency is in the 1 MHz range, these offsets are parts in
>> 1e3, a pretty large amount.)
>>
>> I wonder if the large number of PFB channels is causing a rounding error
>> that results in these frequency offsets. Or is there something else
>> going on?
>>
>> I can probably fudge the xlating filter frequency a bit to move the
>> carriers closer to nominal, but would like to understand what's happening.
>>
>> I'm attaching the (absurdly huge) .grc file. The canvas is pixels
>> tall, so the flowgraph is smaller than the screenshot. :-)
>> _______________________________________________
>> Discuss-gnuradio mailing list
>>
>> https://lists.gnu.org/mailman/listinfo/discuss-gnuradio
_______________________________________________
Discuss-gnuradio mailing list
https://lists.gnu.org/mailman/listinfo/discuss-gnuradio