Working Weaver SSB demodulation sample
A demo of this gnuradio project is now online more-or-less permenantly here
I'm really enjoying the 'record to disk drive' capabilities of GNU/Radio
and the USRP. Every chance I get out to the woods on weekends, string
up a random long wire from my car to a tree, connect it to the tuner and
HF RF amp, filter and USRP and get a couple of hours of the 80 meter
ham band, using complex samples centered on 3800Khz, about 600Khz wide
so it covers from 3.5 to 4.1Mhz. A typical spectrum looks like this:
The peak around 3.9Mhz is due to the tuner. It can be adjusted to any
part of interest. On this particular weekend propagation conditions were
excellent and there was a SSB contest going on.
Here's the script to record to disk:
#!/usr/bin/env python
#
#
from gnuradio import gr
from gnuradio import usrp
from gnuradio import audio
import sys
def build_graph ():
usrp_decim = 100
rf_sample_rate = 64e6 / usrp_decim
fir_decimation = 20
af_sample_rate = 32e3
fg = gr.flow_graph ()
src = usrp.source_c(0, usrp_decim)
mux = 0xf0f0f0f0
src.set_pga(0,20)
src.set_mux(mux)
src.set_rx_freq(0,-3.8e6)
dst = gr.file_sink (gr.sizeof_gr_complex, "ssb_data-3.8e6-c_3-6-2005_6-35pm")
fg.connect ( src, dst ) # split )
return fg
def main ():
fg = build_graph ()
fg.start ()
raw_input ('Press Enter to quit: ')
fg.stop ()
if __name__ == '__main__':
main ()
Which, you can see, is pretty simple. The USRP does all the work, decimating
by 100 gets the data rate down to 640kS/s so over an hour recording fits in
20Gb of disk space. Using complex sampling allows us to tune above and below
the center freq of 3.8Mhz without losing phase or folding.
This next script will display the recorded data in an fft plot. It used
to have the problem of NO CLOCK! Just running as fast as possible so the
display was no longer 'real time'. In this script I've added a couple of
blocks to tie the file->fft display to the sound card clock so it runs
at realistic speeds.
#!/usr/bin/env python
from gnuradio import gr
from gnuradio import audio
#from gnuradio import usrp
from gnuradio import eng_notation
from gnuradio.eng_option import eng_option
from gnuradio.wxgui import stdgui, fftsink
from optparse import OptionParser
import wx
class app_flow_graph (stdgui.gui_flow_graph):
def __init__(self, frame, panel, vbox, argv):
stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
self.frame = frame
self.panel = panel
self.u = gr.file_source (gr.sizeof_gr_complex, "/usr/src/tests/usrp_tests/ssb_data-3.8e6-c_3-6-2005_6-35pm", 1)
input_rate = 64e6 / 100
audio_rate = 32000
a_sink = audio.sink(audio_rate)
a_src = gr.sig_source_c(input_rate,gr.GR_SIN_WAVE,440,.001,0)
combine = gr.interleave(gr.sizeof_gr_complex)
split = gr.deinterleave(gr.sizeof_gr_complex)
decim_coeffs = gr.firdes.low_pass (1, input_rate, 20e3,20e3,gr.firdes.WIN_HAMMING)
decim = gr.fir_filter_ccf(20, decim_coeffs)
split2 = gr.complex_to_float()
block, fft_win = fftsink.make_fft_sink_c (self, panel, "", 512, input_rate)
self.connect (self.u, (combine, 0))
self.connect (a_src, (combine, 1))
self.connect (combine, split)
self.connect ((split, 0), block)
self.connect ((split, 1), decim)
self.connect (decim, split2)
self.connect (split2, a_sink)
vbox.Add (fft_win, 1, wx.EXPAND)
# build small control area at bottom
hbox = wx.BoxSizer (wx.HORIZONTAL)
hbox.Add ((1, 1), 1, wx.EXPAND)
hbox.Add (wx.StaticText (panel, -1, "Set ddc freq: "), 0, wx.ALIGN_CENTER)
self.tc_freq = wx.TextCtrl (panel, -1, "", style=wx.TE_PROCESS_ENTER)
hbox.Add (self.tc_freq, 0, wx.ALIGN_CENTER)
wx.EVT_TEXT_ENTER (self.tc_freq, self.tc_freq.GetId(), self.handle_text_enter)
hbox.Add ((1, 1), 1, wx.EXPAND)
# add it to the main vbox
vbox.Add (hbox, 0, wx.EXPAND)
self.update_status_bar ()
def handle_text_enter (self, event):
pass
def update_status_bar (self):
ddc_freq = 3800e3
decim_rate = 100
sample_rate = 64e6 / decim_rate
msg = "decim: %d %sS/s DDC: %s" % (
decim_rate,
eng_notation.num_to_str (sample_rate),
eng_notation.num_to_str (ddc_freq))
self.frame.GetStatusBar().SetStatusText (msg, 1)
def main ():
app = stdgui.stdapp (app_flow_graph, "USRP FFT")
app.MainLoop ()
if __name__ == '__main__':
main ()
Finally, this is the scipt I use to listen to the weekend's capture
at work. Recent additions include Automatic Gain Control and option
parsing to start it on a desired frequency instead of having to twist
the knob a lot. It uses the PowerMate USB knob in four modes: frequency tune,
volume control, time control, filter control, changing mode by pressing
down on the knob. The mode is signalled by the light in the base of the
knob - no light is frequency, solid light is volume, fast pulse is time
control and slow pulse for filter. Filters range from 600 to 3600Hz wide.
The module "pm.py" is from
this page and is called "powermate.py"
#!/usr/bin/env python
#
# Weaver SSB demodulation
#
#
# af_loi
# |
# --[lpf2i]---(X)---|
# | af_mixi |
# | |
# signal --[freq xlating]--[split] (+)----- ssb_demod
# [fir filter ] | | + USB
# | af_mixq | - LSB
# --[lpf2q]---(X)---|
# |
# af_loq
#
from gnuradio import gr
# from gnuradio import usrp
from gnuradio import audio
import sys
import pm
from gnuradio.eng_option import eng_option
from optparse import OptionParser
def build_graph (freq,scale):
usrp_decim = 100
rf_sample_rate = 64e6 / usrp_decim
fir_decimation = 20
af_sample_rate = 32e3
rf_LO = 3.8e6 - freq + 2.5e3
af_LO = 1.8e3
fg = gr.flow_graph ()
src = gr.file_source (gr.sizeof_gr_complex, "/usr/src/tests/usrp_tests/ssb_data-3.8e6-c_3-6-2005_6-35pm", 1)
xlate_taps = gr.firdes.low_pass ( \
1.0, rf_sample_rate, 20e3, 10e3, gr.firdes.WIN_HAMMING )
xlate = gr.freq_xlating_fir_filter_ccf ( \
fir_decimation, xlate_taps, rf_LO, rf_sample_rate )
split = gr.complex_to_float ()
af_loi = gr.sig_source_f (af_sample_rate,gr.GR_COS_WAVE,af_LO,1,0)
af_loq = gr.sig_source_f (af_sample_rate,gr.GR_SIN_WAVE,af_LO,1,0)
lpf2_taps = gr.firdes.low_pass ( \
1.0, af_sample_rate, 2e3, 600, gr.firdes.WIN_HAMMING)
lpf2i = gr.fir_filter_fff (1, lpf2_taps)
lpf2q = gr.fir_filter_fff (1, lpf2_taps)
af_mixi = gr.multiply_ff ()
af_mixq = gr.multiply_ff ()
sum = gr.add_ff () # sub for USB, add for LSB
audio_lpf_coeffs = gr.firdes.low_pass ( \
1.0,af_sample_rate,3000,600,gr.firdes.WIN_HAMMING)
audio_lpf = gr.fir_filter_fff (1, audio_lpf_coeffs)
scale = gr.multiply_const_ff(scale)
# AGC
sqr1 = gr.multiply_ff()
int = gr.iir_filter_ffd ( [.004, 0], [0, .999] )
offset = gr.add_const_ff(1)
agc = gr.divide_ff()
# out = gr.file_sink (gr.sizeof_float, "ssb_demod")
out = audio.sink (long(af_sample_rate))
fg.connect (src, xlate)
fg.connect (xlate, split)
fg.connect ((split, 0), lpf2i)
fg.connect (lpf2i, (af_mixi, 0))
fg.connect (af_loi, (af_mixi, 1))
fg.connect (af_mixi, (sum, 0))
fg.connect ((split, 1), lpf2q)
fg.connect (lpf2q, (af_mixq, 0))
fg.connect (af_loq, (af_mixq, 1))
fg.connect (af_mixq, (sum, 1))
fg.connect (sum, audio_lpf)
fg.connect (audio_lpf, scale)
# wire AGC
fg.connect (scale, (sqr1, 0))
fg.connect (scale, (sqr1, 1))
fg.connect (sqr1, int)
fg.connect (int, offset)
fg.connect (offset, (agc, 1))
fg.connect (scale, (agc, 0))
fg.connect (agc, out)
return fg, xlate, scale, src, audio_lpf
def main ():
parser = OptionParser (option_class=eng_option)
parser.add_option ("-f", "--freq", type="eng_float", default=3900e3,
help="set waveform frequency to FREQ")
parser.add_option ("-a", "--amplitude", type="eng_float", default=.005,
help="set waveform amplitude to AMPLITUDE", metavar="AMPL")
(options, args) = parser.parse_args ()
freq = options.freq # initial settings
scale = options.amplitude
fg = build_graph (freq,scale)
fg[0].start ()
# calculate filters
af_sample_rate = 32e3
filter=24
audio_lpf_x = []
fkey = []
for co in range(600,3601,100):
fkey += [co]
fc=gr.firdes.low_pass(1.0,af_sample_rate,co,300,gr.firdes.WIN_HAMMING)
audio_lpf_x += [fc]
# audio_lpf_x[0] is 600, [1] is 900, [2] is 1200, etc
knob = pm.PowerMate() # get instance
knob_state = "freq"
pm.PowerMate.SetLEDState(knob,0,255,1,0,0) # turn on freq signal
while 1:
control = pm.PowerMate.WaitForEvent(knob,30) # get event, if any
if control != None: # something happened
if control[3] == 256: # button pushed, change mode
if control[4] == 1: # press
if knob_state == "freq":
knob_state = "volume"
pm.PowerMate.SetLEDState(knob,255,255,1,0,0)
elif knob_state == "volume":
knob_state = "time"
pm.PowerMate.SetLEDState(knob,255,264,1,0,1)
elif knob_state == "time":
knob_state = "filter"
pm.PowerMate.SetLEDState(knob,255,258,1,0,1)
elif knob_state == "filter":
knob_state = "freq"
pm.PowerMate.SetLEDState(knob,0,255,1,0,0)
if control[3] == 7: # knob turned - check mode
if knob_state == "freq": # and act accordingly
if control[4] == 1:
freq += 100
fg[1].set_center_freq(3.8e6 - freq + 2.5e3)
print "Listening to", freq
if control[4] == -1:
freq -= 100
fg[1].set_center_freq(3.8e6 - freq + 2.5e3)
print "Listening to", freq
if knob_state == "volume":
if control[4] == 1:
scale += .0001
fg[2].set_k(scale)
print "Volume", scale
if control[4] == -1:
scale -= .0001
if scale <= .00001:
scale = 0
fg[2].set_k(scale)
print "Volume", scale
if knob_state == "time":
if control[4] == 1:
fg[3].seek(1000000,gr.SEEK_CUR)
if control[4] == -1:
fg[3].seek(-1000000,gr.SEEK_CUR)
if knob_state == "filter":
if control[4] == 1:
filter += 1
if filter > 30:
filter = 30
fg[4].set_taps(audio_lpf_x[filter])
print "Using filter: ", fkey[filter],"hz"
if control[4] == -1:
filter -= 1
if filter < 0:
filter = 0
fg[4].set_taps(audio_lpf_x[filter])
print "Using filter: ", fkey[filter],"hz"
if __name__ == '__main__':
main ()