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 ()