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:
FM radio receiver
And the FFT when receiving FM broadcast radio over the air:
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.
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 lukbe: sudo 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:
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:
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:
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