Software Defined Radio - how does it work? Part 4
- Tutorial
Hi, Habr.
The third part described how to access the SDR receiver using Python. Now we will get acquainted with the GNU Radio program - a system that allows you to create a fairly complex configuration of a radio device without writing a single line of code.

As an example, consider the problem of parallel reception of several FM stations on one receiver. We will use the same RTL SDR V3 as the receiver.
Continued under the cut.
To get started, GNU Radio needs to be installed, the distribution kit for Windows can be downloaded here . This system is cross-platform, there are also versions for Linux and OSX (it seems that GNU Radio was successfully launched on the Raspberry Pi, but I can’t give a 100% guarantee).
In fact, GNU Radio is a whole framework for digital signal processing, in which the program is "assembled" from separate modules. There are a large number of ready-made blocks, if you wish, you can also create your own. The modules themselves are written in C ++, and Python interacts with each other. Those who wish can look at the API in more detail , but in practice this is most likely not useful - all actions can be done visually in the GNU Radio Companion program.
The system is focused on processing data streams, so that each block usually has an input and an output. Next, connecting the blocks in the editor, we get a ready-made system. The GNU Radio interface itself is quite simple, the difficulty is in understanding what a block is doing. As mentioned earlier, low-level work with SDR has a high input threshold and requires some knowledge of DSP and math. But we will consider a simple task for which no special knowledge is required. So let's get started.
We start the GNU Radio Companion, create a new project, select the project type WX GUI, add to the screen and connect the two blocks, as shown in the screenshot.

We see two types of blocks - Source (source) and Sink (output, "drain"). RTL-SDR is our receiver, FFT GUI is a virtual spectrum analyzer.
The Sample Rate variable is set to 2048000, this is the sample rate of our receiver. The default RTL-SDR frequency is 100 MHz.
We start the project - everything works, we see a range of FM stations. The first program for GNU Radio is ready!

If we look at the log, we will see such lines.
Generating: 'D: \\ MyProjects \\ GNURadio \\ top_block.py'
Executing: C: \ Python27 \ python.exe -u D: \ MyProjects \ GNURadio \ top_block.py
Yes, we can see the top_block.py file that GNU Radio Companion generated for us. True Jedi can write directly in Python, but the required code, as we see, is quite large. We created it in 1 minute.
However, if we remove the cumbersome initialization, we will see that there are not so many key lines of code.
So basically, it can be written manually. But it is still faster with a mouse. Although the ability to change the code can sometimes come in handy if you want to add some non-standard logic.
Now try to take one of the stations. As was seen from the screenshots, the center frequency of the receiver is 100 MHz and the bandwidth is about 2 MHz. In the spectrum we see two stations, at 100.1 MHz and 100.7 MHz, respectively.
The first step is to transfer the spectrum of the station to the center, now it is 100KHz to the right. To do this, we recall the school formula for multiplying cosines - as a result there will be two frequencies, the sum and the difference - the desired station will move to the center, which is what we need (and we filter out the excess).
We create two variables for storing the freq_center = 100000000 and freq_1 = 100100000 frequencies, we also add a signal generator with freq_center frequency - freq_1.

Because Since the system is based on Python, we can use expressions in the parameter input fields, which is quite convenient.
As a result, the circuit should look like this:

Now you need to add several blocks at once - reduce the clock frequency of the input signal (it is 2048KHz), filter the signal, apply it to the FM decoder, then reduce the clock frequency again to 48KHz.
The result is shown in the picture:

We consider carefully. We divide the clock speed of 2048KHz by 4 times with the Rational Resampler block (we get 512KHz), then after the Low Pass filter there is a WBFM decoder with decimation 10 (we get 51.2KHz). In principle, this signal can already be fed to the sound card, but the pitch will be slightly different. Once again, we change the clock frequency to 48/51, as a result, the clock frequency will be 48.2 KHz, the difference can already be neglected.
The second important point is the type of inputs. Integrated from receiverIQ signal (inputs / outputs in blue), a material signal is output from the FM decoder - inputs and outputs are yellow. If mixed up, nothing will work. More was already on Habré , it is enough for us to understand the general principle.
In general, run, make sure that everything works. You can run the program and listen to the radio. We will go further - we still have Software Defined radio - we will add the simultaneous reception of the second station.
The second receiver is added by your favorite programming method - Ctrl + C / Ctrl + V. Add the freq_2 variable, copy the blocks and connect them the same way.

The result is quite surreal - you can listen to two FM stations simultaneously. Using the same method (Ctrl + V), you can add a third station.
Listening to two stations in an original way, but in practice is not very useful. We’ll do something more necessary, for example, add sound recording to separate files. This can be quite convenient - several channels can be recorded simultaneously from one physical receiver.
Add a File Sink component to each output, as shown in the screenshot.

The Windows version for some reason requires absolute file paths, otherwise the recording does not work. We start, we are convinced that everything is normal. The size of the saved files is quite large, because the default format is float. Entry in int format will leave the readers as homework.
The resulting files can be opened in Cool Edit and make sure that the sound is recorded normally.


Of course, the number of recorded channels can be increased, it is limited only by the receiver bandwidth and computer power. In addition to File Sink, UDP Sink can also be used, so the program can be used for broadcast over the network.
And the last one. If you use the program autonomously, for example, for multi-channel recording, then the UI, in principle, is not needed. In the upper left block of Options, change the Run Options parameter to No UI. Run the program again, make sure that everything works. Now we save the generated file top_block.py - we can just run it from the command line, for example from a bat-file or from the console.

If anyone is interested, the generated file is saved under a spoiler.
It is also convenient that the system is cross-platform, and the resulting program can run on Linux, Windows and OSX.
We can say that GNU Radio is a rather complicated system, not in terms of drawing blocks of course, but in terms of understanding how it all works. But to do some simple things is quite feasible and interesting. GNU Radio is also conveniently used as a “virtual laboratory” for training - you can connect a virtual oscilloscope or spectrum analyzer to any part of the circuit and see how the signal looks.
If there are no separate wishes, the topic of SDR reception can probably be closed - all the main points have already been considered, and the number of views from the first to the third part drops almost exponentially(although you can still write about the transfer, but it requires more expensive hardware for tests than RTL SDR). Nevertheless, I hope that some understanding of how this works remains with the readers. Well, all successful experiments.
The third part described how to access the SDR receiver using Python. Now we will get acquainted with the GNU Radio program - a system that allows you to create a fairly complex configuration of a radio device without writing a single line of code.

As an example, consider the problem of parallel reception of several FM stations on one receiver. We will use the same RTL SDR V3 as the receiver.
Continued under the cut.
Installation
To get started, GNU Radio needs to be installed, the distribution kit for Windows can be downloaded here . This system is cross-platform, there are also versions for Linux and OSX (it seems that GNU Radio was successfully launched on the Raspberry Pi, but I can’t give a 100% guarantee).
In fact, GNU Radio is a whole framework for digital signal processing, in which the program is "assembled" from separate modules. There are a large number of ready-made blocks, if you wish, you can also create your own. The modules themselves are written in C ++, and Python interacts with each other. Those who wish can look at the API in more detail , but in practice this is most likely not useful - all actions can be done visually in the GNU Radio Companion program.
The system is focused on processing data streams, so that each block usually has an input and an output. Next, connecting the blocks in the editor, we get a ready-made system. The GNU Radio interface itself is quite simple, the difficulty is in understanding what a block is doing. As mentioned earlier, low-level work with SDR has a high input threshold and requires some knowledge of DSP and math. But we will consider a simple task for which no special knowledge is required. So let's get started.
Beginning of work
We start the GNU Radio Companion, create a new project, select the project type WX GUI, add to the screen and connect the two blocks, as shown in the screenshot.

We see two types of blocks - Source (source) and Sink (output, "drain"). RTL-SDR is our receiver, FFT GUI is a virtual spectrum analyzer.
The Sample Rate variable is set to 2048000, this is the sample rate of our receiver. The default RTL-SDR frequency is 100 MHz.
We start the project - everything works, we see a range of FM stations. The first program for GNU Radio is ready!

If we look at the log, we will see such lines.
Generating: 'D: \\ MyProjects \\ GNURadio \\ top_block.py'
Executing: C: \ Python27 \ python.exe -u D: \ MyProjects \ GNURadio \ top_block.py
Yes, we can see the top_block.py file that GNU Radio Companion generated for us. True Jedi can write directly in Python, but the required code, as we see, is quite large. We created it in 1 minute.
top_blocks.py
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
##################################################
# GNU Radio Python Flow Graph
# Title: Top Block
# Generated: Wed May 22 22:05:14 2019
##################################################
if __name__ == '__main__':
import ctypes
import sys
if sys.platform.startswith('linux'):
try:
x11 = ctypes.cdll.LoadLibrary('libX11.so')
x11.XInitThreads()
except:
print "Warning: failed to XInitThreads()"
from gnuradio import eng_notation
from gnuradio import gr
from gnuradio import wxgui
from gnuradio.eng_option import eng_option
from gnuradio.fft import window
from gnuradio.filter import firdes
from gnuradio.wxgui import fftsink2
from grc_gnuradio import wxgui as grc_wxgui
from optparse import OptionParser
import osmosdr
import time
import wx
class top_block(grc_wxgui.top_block_gui):
def __init__(self):
grc_wxgui.top_block_gui.__init__(self, title="Top Block")
##################################################
# Variables
##################################################
self.samp_rate = samp_rate = 2048000
##################################################
# Blocks
##################################################
self.wxgui_fftsink2_0 = fftsink2.fft_sink_c(
self.GetWin(),
baseband_freq=0,
y_per_div=10,
y_divs=10,
ref_level=0,
ref_scale=2.0,
sample_rate=samp_rate,
fft_size=1024,
fft_rate=15,
average=False,
avg_alpha=None,
title='FFT Plot',
peak_hold=False,
)
self.Add(self.wxgui_fftsink2_0.win)
self.rtlsdr_source_0 = osmosdr.source( args="numchan=" + str(1) + " " + '' )
self.rtlsdr_source_0.set_sample_rate(samp_rate)
self.rtlsdr_source_0.set_center_freq(100e6, 0)
self.rtlsdr_source_0.set_freq_corr(0, 0)
self.rtlsdr_source_0.set_dc_offset_mode(0, 0)
self.rtlsdr_source_0.set_iq_balance_mode(0, 0)
self.rtlsdr_source_0.set_gain_mode(False, 0)
self.rtlsdr_source_0.set_gain(10, 0)
self.rtlsdr_source_0.set_if_gain(20, 0)
self.rtlsdr_source_0.set_bb_gain(20, 0)
self.rtlsdr_source_0.set_antenna('', 0)
self.rtlsdr_source_0.set_bandwidth(0, 0)
##################################################
# Connections
##################################################
self.connect((self.rtlsdr_source_0, 0), (self.wxgui_fftsink2_0, 0))
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.wxgui_fftsink2_0.set_sample_rate(self.samp_rate)
self.rtlsdr_source_0.set_sample_rate(self.samp_rate)
def main(top_block_cls=top_block, options=None):
tb = top_block_cls()
tb.Start(True)
tb.Wait()
if __name__ == '__main__':
main()
However, if we remove the cumbersome initialization, we will see that there are not so many key lines of code.
from gnuradio import gr
from gnuradio.wxgui import fftsink2
import osmosdr
class top_block(grc_wxgui.top_block_gui):
def __init__(self):
grc_wxgui.top_block_gui.__init__(self, title="Top Block")
self.samp_rate = samp_rate = 2048000
self.wxgui_fftsink2_0 = fftsink2.fft_sink_c(...)
self.Add(self.wxgui_fftsink2_0.win)
self.rtlsdr_source_0 = osmosdr.source(args="numchan=" + str(1) + " " + '' )
self.connect((self.rtlsdr_source_0, 0), (self.wxgui_fftsink2_0, 0))
def main(top_block_cls=top_block, options=None):
tb = top_block_cls()
tb.Start(True)
tb.Wait()
So basically, it can be written manually. But it is still faster with a mouse. Although the ability to change the code can sometimes come in handy if you want to add some non-standard logic.
Receive FM Radio
Now try to take one of the stations. As was seen from the screenshots, the center frequency of the receiver is 100 MHz and the bandwidth is about 2 MHz. In the spectrum we see two stations, at 100.1 MHz and 100.7 MHz, respectively.
The first step is to transfer the spectrum of the station to the center, now it is 100KHz to the right. To do this, we recall the school formula for multiplying cosines - as a result there will be two frequencies, the sum and the difference - the desired station will move to the center, which is what we need (and we filter out the excess).
We create two variables for storing the freq_center = 100000000 and freq_1 = 100100000 frequencies, we also add a signal generator with freq_center frequency - freq_1.

Because Since the system is based on Python, we can use expressions in the parameter input fields, which is quite convenient.
As a result, the circuit should look like this:

Now you need to add several blocks at once - reduce the clock frequency of the input signal (it is 2048KHz), filter the signal, apply it to the FM decoder, then reduce the clock frequency again to 48KHz.
The result is shown in the picture:

We consider carefully. We divide the clock speed of 2048KHz by 4 times with the Rational Resampler block (we get 512KHz), then after the Low Pass filter there is a WBFM decoder with decimation 10 (we get 51.2KHz). In principle, this signal can already be fed to the sound card, but the pitch will be slightly different. Once again, we change the clock frequency to 48/51, as a result, the clock frequency will be 48.2 KHz, the difference can already be neglected.
The second important point is the type of inputs. Integrated from receiverIQ signal (inputs / outputs in blue), a material signal is output from the FM decoder - inputs and outputs are yellow. If mixed up, nothing will work. More was already on Habré , it is enough for us to understand the general principle.
In general, run, make sure that everything works. You can run the program and listen to the radio. We will go further - we still have Software Defined radio - we will add the simultaneous reception of the second station.
Multichannel reception
The second receiver is added by your favorite programming method - Ctrl + C / Ctrl + V. Add the freq_2 variable, copy the blocks and connect them the same way.

The result is quite surreal - you can listen to two FM stations simultaneously. Using the same method (Ctrl + V), you can add a third station.
Record
Listening to two stations in an original way, but in practice is not very useful. We’ll do something more necessary, for example, add sound recording to separate files. This can be quite convenient - several channels can be recorded simultaneously from one physical receiver.
Add a File Sink component to each output, as shown in the screenshot.

The Windows version for some reason requires absolute file paths, otherwise the recording does not work. We start, we are convinced that everything is normal. The size of the saved files is quite large, because the default format is float. Entry in int format will leave the readers as homework.
The resulting files can be opened in Cool Edit and make sure that the sound is recorded normally.


Of course, the number of recorded channels can be increased, it is limited only by the receiver bandwidth and computer power. In addition to File Sink, UDP Sink can also be used, so the program can be used for broadcast over the network.
Run from the command line
And the last one. If you use the program autonomously, for example, for multi-channel recording, then the UI, in principle, is not needed. In the upper left block of Options, change the Run Options parameter to No UI. Run the program again, make sure that everything works. Now we save the generated file top_block.py - we can just run it from the command line, for example from a bat-file or from the console.

If anyone is interested, the generated file is saved under a spoiler.
recorder.py
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
##################################################
# GNU Radio Python Flow Graph
# Title: Top Block
# Generated: Fri May 24 21:47:03 2019
##################################################
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.filter import firdes
from optparse import OptionParser
import osmosdr
import time
class top_block(gr.top_block):
def __init__(self):
gr.top_block.__init__(self, "Top Block")
##################################################
# Variables
##################################################
self.samp_rate = samp_rate = 2048000
self.freq_center = freq_center = 100000000
self.freq_2 = freq_2 = 100700000
self.freq_1 = freq_1 = 100100000
##################################################
# Blocks
##################################################
self.rtlsdr_source_0 = osmosdr.source( args="numchan=" + str(1) + " " + '' )
self.rtlsdr_source_0.set_sample_rate(samp_rate)
self.rtlsdr_source_0.set_center_freq(freq_center, 0)
self.rtlsdr_source_0.set_freq_corr(0, 0)
self.rtlsdr_source_0.set_dc_offset_mode(0, 0)
self.rtlsdr_source_0.set_iq_balance_mode(0, 0)
self.rtlsdr_source_0.set_gain_mode(False, 0)
self.rtlsdr_source_0.set_gain(10, 0)
self.rtlsdr_source_0.set_if_gain(20, 0)
self.rtlsdr_source_0.set_bb_gain(20, 0)
self.rtlsdr_source_0.set_antenna('', 0)
self.rtlsdr_source_0.set_bandwidth(0, 0)
self.rational_resampler_xxx_1_0 = filter.rational_resampler_fff(
interpolation=48,
decimation=51,
taps=None,
fractional_bw=None,
)
self.rational_resampler_xxx_1 = filter.rational_resampler_fff(
interpolation=48,
decimation=51,
taps=None,
fractional_bw=None,
)
self.rational_resampler_xxx_0_0 = filter.rational_resampler_ccc(
interpolation=1,
decimation=4,
taps=None,
fractional_bw=None,
)
self.rational_resampler_xxx_0 = filter.rational_resampler_ccc(
interpolation=1,
decimation=4,
taps=None,
fractional_bw=None,
)
self.low_pass_filter_0_0 = filter.fir_filter_ccf(1, firdes.low_pass(
1, samp_rate/4, 100000, 500000, firdes.WIN_HAMMING, 6.76))
self.low_pass_filter_0 = filter.fir_filter_ccf(1, firdes.low_pass(
1, samp_rate/4, 100000, 500000, firdes.WIN_HAMMING, 6.76))
self.blocks_multiply_xx_0_0 = blocks.multiply_vcc(1)
self.blocks_multiply_xx_0 = blocks.multiply_vcc(1)
self.blocks_file_sink_0_0 = blocks.file_sink(gr.sizeof_float*1, 'D:\\Temp\\1\\audio2.snd', False)
self.blocks_file_sink_0_0.set_unbuffered(False)
self.blocks_file_sink_0 = blocks.file_sink(gr.sizeof_float*1, 'D:\\Temp\\1\\audio1.snd', False)
self.blocks_file_sink_0.set_unbuffered(False)
self.audio_sink_0 = audio.sink(48000, '', True)
self.analog_wfm_rcv_0_0 = analog.wfm_rcv(
quad_rate=samp_rate/4,
audio_decimation=10,
)
self.analog_wfm_rcv_0 = analog.wfm_rcv(
quad_rate=samp_rate/4,
audio_decimation=10,
)
self.analog_sig_source_x_0_0 = analog.sig_source_c(samp_rate, analog.GR_COS_WAVE, freq_center - freq_2, 1, 0)
self.analog_sig_source_x_0 = analog.sig_source_c(samp_rate, analog.GR_COS_WAVE, freq_center - freq_1, 1, 0)
##################################################
# Connections
##################################################
self.connect((self.analog_sig_source_x_0, 0), (self.blocks_multiply_xx_0, 1))
self.connect((self.analog_sig_source_x_0_0, 0), (self.blocks_multiply_xx_0_0, 1))
self.connect((self.analog_wfm_rcv_0, 0), (self.rational_resampler_xxx_1, 0))
self.connect((self.analog_wfm_rcv_0_0, 0), (self.rational_resampler_xxx_1_0, 0))
self.connect((self.blocks_multiply_xx_0, 0), (self.rational_resampler_xxx_0, 0))
self.connect((self.blocks_multiply_xx_0_0, 0), (self.rational_resampler_xxx_0_0, 0))
self.connect((self.low_pass_filter_0, 0), (self.analog_wfm_rcv_0, 0))
self.connect((self.low_pass_filter_0_0, 0), (self.analog_wfm_rcv_0_0, 0))
self.connect((self.rational_resampler_xxx_0, 0), (self.low_pass_filter_0, 0))
self.connect((self.rational_resampler_xxx_0_0, 0), (self.low_pass_filter_0_0, 0))
self.connect((self.rational_resampler_xxx_1, 0), (self.audio_sink_0, 0))
self.connect((self.rational_resampler_xxx_1, 0), (self.blocks_file_sink_0, 0))
self.connect((self.rational_resampler_xxx_1_0, 0), (self.audio_sink_0, 1))
self.connect((self.rational_resampler_xxx_1_0, 0), (self.blocks_file_sink_0_0, 0))
self.connect((self.rtlsdr_source_0, 0), (self.blocks_multiply_xx_0, 0))
self.connect((self.rtlsdr_source_0, 0), (self.blocks_multiply_xx_0_0, 0))
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.rtlsdr_source_0.set_sample_rate(self.samp_rate)
self.low_pass_filter_0_0.set_taps(firdes.low_pass(1, self.samp_rate/4, 100000, 500000, firdes.WIN_HAMMING, 6.76))
self.low_pass_filter_0.set_taps(firdes.low_pass(1, self.samp_rate/4, 100000, 500000, firdes.WIN_HAMMING, 6.76))
self.analog_sig_source_x_0_0.set_sampling_freq(self.samp_rate)
self.analog_sig_source_x_0.set_sampling_freq(self.samp_rate)
def get_freq_center(self):
return self.freq_center
def set_freq_center(self, freq_center):
self.freq_center = freq_center
self.rtlsdr_source_0.set_center_freq(self.freq_center, 0)
self.analog_sig_source_x_0_0.set_frequency(self.freq_center - self.freq_2)
self.analog_sig_source_x_0.set_frequency(self.freq_center - self.freq_1)
def get_freq_2(self):
return self.freq_2
def set_freq_2(self, freq_2):
self.freq_2 = freq_2
self.analog_sig_source_x_0_0.set_frequency(self.freq_center - self.freq_2)
def get_freq_1(self):
return self.freq_1
def set_freq_1(self, freq_1):
self.freq_1 = freq_1
self.analog_sig_source_x_0.set_frequency(self.freq_center - self.freq_1)
def main(top_block_cls=top_block, options=None):
tb = top_block_cls()
tb.start()
try:
raw_input('Press Enter to quit: ')
except EOFError:
pass
tb.stop()
tb.wait()
if __name__ == '__main__':
main()
It is also convenient that the system is cross-platform, and the resulting program can run on Linux, Windows and OSX.
Conclusion
We can say that GNU Radio is a rather complicated system, not in terms of drawing blocks of course, but in terms of understanding how it all works. But to do some simple things is quite feasible and interesting. GNU Radio is also conveniently used as a “virtual laboratory” for training - you can connect a virtual oscilloscope or spectrum analyzer to any part of the circuit and see how the signal looks.
If there are no separate wishes, the topic of SDR reception can probably be closed - all the main points have already been considered, and the number of views from the first to the third part drops almost exponentially(although you can still write about the transfer, but it requires more expensive hardware for tests than RTL SDR). Nevertheless, I hope that some understanding of how this works remains with the readers. Well, all successful experiments.