SDR first project: initial setup, node-hackrf, GNU Radio on Linux, RPi 3 FM tuner

https://medium.com/@rxseger/sdr-first-project-initial-setup-node-hackrf-gnu-radio-on-linux-os-x-rpi-3-w-fm-tuner-ee16cdc8fd82

my goal is receiving FM broadcast radio (the so-called “hello world” of SDR).

GNU Radio on Linux

I highly recommend Michael Ossmann’s online course Software Defined Radio with HackRF for an introduction to SDR with the GNU Radio toolkit.

For expediency I first tried to run GNU Radio under Linux in a virtual machine via VirtualBox on OS X. There is a GNU Radio Live DVD with everything needed preinstalled.

Plug in the HackRF, then attach it to the guest OS by selecting it under Devices > USB > Great Scott Gadgets HackRF One [0100]. Add an OsmoCom source and WX GUI FFT Sink, as described in the course. This was not successful — I could not get anything out of the HackRF over the VM, likely due to bandwidth limitations in virtualizing USB.

Time to install Linux natively. To keep it separated from OS X in the internal SSD of the iMac, I installed on a USB external drive using Mac Linux USB Loader (not UNetbootin). Booted with ‘nomodeset’ kernel option to fix missing video on the iMac Retina, installed gnuradio and gr-osmosdr packages, then executed gnuradio-companion from the command-line.

Follow the steps in lesson #1 to create a software FM radio. Here’s my flow graph:

 

Image for post

FM radio receiver

And the FFT when receiving FM broadcast radio over the air:

 

Image for post

With the Audio Sink, you can hear these transmissions

Success!

GNU Radio on Linux works well with the HackRF, but running Linux on this iMac is an awkward experience, for various reasons. So I’ll run it under Mac OS X. Easier said than done, but it is possible.

GNU Radio on Raspberry Pi 3

Starting with this kit also from Sparkfun:

Unboxed the power adapter, case, board, SD card, and put it together. Inserted the preinstalled SD card into the SD slot on the board, plugged in an Ethernet cable and power. The system received an IP address from the DHCP server on my network (using the hostname recovery), I could ping it by no ports were open.

Connected the included FTDI 232R USB breakout board, to the wedge, and then to the GPIO port over the ribbon cable. Plugged in a USB cable, the Mac recognized the port and it appeared as /dev/tty.usbserial-AH03FLVX, attempted to connect with:

screen /dev/tty.usbserial-AH03FLVX 115200

but there was no output. What’s going on?

Ideally, one could attach a USB keyboard and HDMI video display to interact with the console. But I had neither handy.

Turns out, the default install is NOOBS, a graphical installer to select which OS you want. Raspbian does support headless operation, but you need to install it. Removed the SD card, inserted it into the adapter, plugged into the Mac, and imaged the latest Raspbian OS.

Now it boots up with the DHCP node name raspberrypi, with SSH enabled: username pi, password raspberry, as expected. Complete the headless setup described here, then install GNU Radio:

sudo apt install gnuradio
sudo apt install gr-osmosdr

But our FM radio receiver created in the previous section does not work: top_block reports errors about the window size. Turns out apt installed GNU Radio 3.7.5, instead of the latest 3.7.9.1 which we had with Homebrew on OS X. Fortunately the compatibility issue is easy to fix: double-click on the top block, and enter “1280, 1024" in the Window Size field, matching the default with a new flowgraph.

 

Image for post

Fixing GNU Radio 3.7.9.1 to 3.7.5 compatibility on Raspberry Pi

The flow graph is now ready to run.. but it fails with another error:

wx._core.PyAssertionError: C++ assertion ok failed at ../src/unix/glx11.cpp(442) in GetGLXVersion(): GLX version not found

Solution, from lukbesudo apt install libgl1-mesa-swx11

Disable the audio sink for now, only showing the FFT, and run. We see something, but the slower Raspberry Pi CPU cannot keep up with the 20e6 sample rate, hence the “O” overflows:

 

Image for post

Raspberry Pi 3 CPU: 1.2 GHz quad-core ARM Cortex A53, overflowing GNU Radio

Experimentation reveals a modest 5e6 samp_rate is achievable.

Re-enabling the audio sink reveals another error:

audio: using audio_alsa audio_alsa_sink[hw:0,0]: set_channels failed: Invalid argument

For some reason, the default device for the Audio Sink is not good enough. Double-click the block and enter “hw:0,1” in the Device Name field. Execute the flowgraph…and:

 

Image for post

Getting closer, but 5M osmosdr samp_rate is still too much for this RPi 3

The audio is choppy and the FFT display lags. “OOOaUO” indicates the system cannot keep up, we are asking too much of it. htop confirms the processors are nearing their capacity. To lighten the load, reduce the samp_rate to 1e6. At last, the FM radio station is audible.

But its not a complete success. The WX slider control (and seemingly the entire GUI) is non-responsive during reception, presumably the GUI updating in addition to the digital signal processing overburdens the ARM CPU, even with GNU Radio’s Volk machine optimizations, neon_hardfp_orc on the Raspberry Pi for advanced SIMD NEON instructions.

A more practical RPi FM radio could use some other means to tune than GUI widgets on the console. Fortunately, the device includes a general-purpose I/O port and the starter kit includes a few buttons, all we need to make a physical digital tuner.

Using GPIO for volume/channel buttons on the Raspberry Pi

Wire up the Pi Wedge and plug it into the Raspberry Pi GPIO port and breadboard, insert each of the four push buttons, and jumper them to the GPIO ports as follows:

 

Image for post

Wiring up the buttons to Raspberry Pi GPIO: G22, G27, G18, G17 on one side, and 3.3V on another

Pressing the buttons should now show a 1 instead of 0 for GPIO. 0–3 in `gpio readall`. These correspond to “Pi 3 physical pins” 11, 12, 13, and 15 and can be accessed with the Python RPi.GPIO module in BOARD mode:

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
GPIO.setup([11, 12, 13, 15], GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
while True:
print GPIO.input(11), GPIO.input(12), GPIO.input(13), GPIO.input(15)

Now we step away from GNU Radio Companion and edit the generated Python directly. Add to the generated source (default top_block.py) code to create a new thread using the threading module, poll the GPIO pins, and call set_audio_gain and set_channel_freq on the top block, as appropriate.

While I was at it, removed the WX GUI so this program can run headless. No need to use VNC anymore, it can be executed over SSH:

pi@raspberrypi:~/fm $ python top_block.py
linux; GNU C++ version 4.9.1; Boost_105500; UHD_003.007.003–0-unknown
Using Volk machine: neon_hardfp_orc
gr-osmosdr 0.1.3 (0.1.3) gnuradio 3.7.5
built-in source types: file osmosdr fcd rtl rtl_tcp uhd miri hackrf bladerf rfspace airspy
Using HackRF One with firmware 2014.08.1
gr::log :INFO: audio source — Audio sink arch: alsa
Press Enter to quit:

and use the buttons to change the volume and channel. You can find the final script here:


#!/usr/bin/env python

import threading
import time
import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BOARD)

# GPIO pins

# gpio readall
# ...
#[pi wedge]                   [physical GPIO.BOARD]
# |  17 |   0 | GPIO. 0 |   IN | 0 | 11 || 12 | 0 | IN   | GPIO. 1 | 1   | 18  |
# |  27 |   2 | GPIO. 2 |   IN | 0 | 13 || 14 |   |      | 0v      |     |     |
# |  22 |   3 | GPIO. 3 |   IN | 0 | 15 || 16 | 0 | IN   | GPIO. 4 | 4   | 23  |
# ...

BUTTON_VOLUME_UP = 11
BUTTON_VOLUME_DOWN = 12
BUTTON_CHANNEL_UP = 13
BUTTON_CHANNEL_DOWN = 15

GPIO.setup([BUTTON_VOLUME_UP, BUTTON_VOLUME_DOWN, BUTTON_CHANNEL_UP, BUTTON_CHANNEL_DOWN],
    # enable internal pull-down resistors to default the inputs low
    # see https://sourceforge.net/p/raspberry-gpio-python/wiki/Inputs/
    GPIO.IN, pull_up_down=GPIO.PUD_DOWN)


##################################################
# Gnuradio Python Flow Graph
# Title: Top Block
# Generated: Sun Jun  5 09:59:50 2016
##################################################

from gnuradio import analog
from gnuradio import audio
from gnuradio import blocks
from gnuradio import eng_notation
from gnuradio import filter
from gnuradio import gr
from gnuradio.eng_option import eng_option
from gnuradio.fft import window
from gnuradio.filter import firdes
from optparse import OptionParser
import osmosdr

class top_block(gr.top_block):

    def __init__(self):
        gr.top_block.__init__(self, "Top Block")

        ##################################################
        # Variables
        ##################################################
        self.samp_rate = samp_rate = 1e6
        self.channel_width = channel_width = 200e3
        self.channel_freq = channel_freq = 97.7e6
        self.center_freq = center_freq = 97.9e6
        self.audio_gain = audio_gain = 1.5

        ##################################################
        # Blocks
        ##################################################
        self.rational_resampler_xxx_0 = filter.rational_resampler_ccc(
                interpolation=12,
                decimation=5,
                taps=None,
                fractional_bw=None,
        )
        self.osmosdr_source_0 = osmosdr.source( args="numchan=" + str(1) + " " + "" )
        self.osmosdr_source_0.set_sample_rate(samp_rate)
        self.osmosdr_source_0.set_center_freq(97.9e6, 0)
        self.osmosdr_source_0.set_freq_corr(0, 0)
        self.osmosdr_source_0.set_dc_offset_mode(0, 0)
        self.osmosdr_source_0.set_iq_balance_mode(0, 0)
        self.osmosdr_source_0.set_gain_mode(False, 0)
        self.osmosdr_source_0.set_gain(0, 0)
        self.osmosdr_source_0.set_if_gain(20, 0)
        self.osmosdr_source_0.set_bb_gain(20, 0)
        self.osmosdr_source_0.set_antenna("", 0)
        self.osmosdr_source_0.set_bandwidth(0, 0)
          
        self.low_pass_filter_0 = filter.fir_filter_ccf(int(samp_rate/channel_width), firdes.low_pass(
        	1, samp_rate, 75e3, 25e3, firdes.WIN_BLACKMAN, 6.76))
        self.blocks_multiply_xx_0 = blocks.multiply_vcc(1)
        self.blocks_multiply_const_vxx_0 = blocks.multiply_const_vff((audio_gain, ))
        self.audio_sink_0 = audio.sink(48000, "hw:0,1", True)
        self.analog_wfm_rcv_0 = analog.wfm_rcv(
        	quad_rate=480e3,
        	audio_decimation=10,
        )
        self.analog_sig_source_x_0 = analog.sig_source_c(samp_rate, analog.GR_COS_WAVE, center_freq - channel_freq, 1, 0)

        ##################################################
        # Connections
        ##################################################
        self.connect((self.analog_sig_source_x_0, 0), (self.blocks_multiply_xx_0, 1))
        self.connect((self.osmosdr_source_0, 0), (self.blocks_multiply_xx_0, 0))
        self.connect((self.rational_resampler_xxx_0, 0), (self.analog_wfm_rcv_0, 0))
        self.connect((self.low_pass_filter_0, 0), (self.rational_resampler_xxx_0, 0))
        self.connect((self.blocks_multiply_xx_0, 0), (self.low_pass_filter_0, 0))
        self.connect((self.blocks_multiply_const_vxx_0, 0), (self.audio_sink_0, 0))
        self.connect((self.analog_wfm_rcv_0, 0), (self.blocks_multiply_const_vxx_0, 0))



    def get_samp_rate(self):
        return self.samp_rate

    def set_samp_rate(self, samp_rate):
        self.samp_rate = samp_rate
        self.analog_sig_source_x_0.set_sampling_freq(self.samp_rate)
        self.osmosdr_source_0.set_sample_rate(self.samp_rate)
        self.low_pass_filter_0.set_taps(firdes.low_pass(1, self.samp_rate, 75e3, 25e3, firdes.WIN_BLACKMAN, 6.76))

    def get_channel_width(self):
        return self.channel_width

    def set_channel_width(self, channel_width):
        self.channel_width = channel_width

    def get_channel_freq(self):
        return self.channel_freq

    def set_channel_freq(self, channel_freq):
        self.channel_freq = channel_freq
        self.analog_sig_source_x_0.set_frequency(self.center_freq - self.channel_freq)

    def get_center_freq(self):
        return self.center_freq

    def set_center_freq(self, center_freq):
        self.center_freq = center_freq
        self.analog_sig_source_x_0.set_frequency(self.center_freq - self.channel_freq)

    def get_audio_gain(self):
        return self.audio_gain

    def set_audio_gain(self, audio_gain):
        self.audio_gain = audio_gain
        self.blocks_multiply_const_vxx_0.set_k((self.audio_gain, ))

class ButtonThread(threading.Thread):
    def __init__(self, tb):
        self.tb = tb
        threading.Thread.__init__(self)

    def run(self):
        tb = self.tb

        while True:
            #print GPIO.input(BUTTON_VOLUME_UP), GPIO.input(BUTTON_VOLUME_DOWN), GPIO.input(BUTTON_CHANNEL_UP), GPIO.input(BUTTON_CHANNEL_DOWN)

            # Volume controls
            if GPIO.input(BUTTON_VOLUME_UP) == 1:
                tb.set_audio_gain(tb.get_audio_gain() + 1)
                print tb.get_audio_gain()
                # TODO: smaller steps when near zero?
                # TODO: don't let go below zero and wrap around

            if GPIO.input(BUTTON_VOLUME_DOWN) == 1:
                tb.set_audio_gain(tb.get_audio_gain() - 1)
                print tb.get_audio_gain()
                # TODO: limit to reasonable value that causes distortion

            # Channel controls
	    # TODO: ?
            if GPIO.input(BUTTON_CHANNEL_UP) == 1:
                # 0.4 MHz for expediency, most channels this far apart (though some 0.2)
                tb.set_channel_freq(tb.get_channel_freq() + 0.4e6)
                print tb.get_channel_freq()
            if GPIO.input(BUTTON_CHANNEL_DOWN) == 1:
                tb.set_channel_freq(tb.get_channel_freq() - 0.4e6)
                print tb.get_channel_freq()

            # TODO: edge-triggered?
            time.sleep(0.1)

if __name__ == '__main__':
    import ctypes
    import sys
    parser = OptionParser(option_class=eng_option, usage="%prog: [options]")
    (options, args) = parser.parse_args()
    tb = top_block()
    ButtonThread(tb).start()
    tb.start()
    try:
        raw_input('Press Enter to quit: ')
    except EOFError:
        pass
    tb.stop()
    tb.wait()

view rawtop_block.py hosted with ❤ by GitHub

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值