C++ Gnuradio
C++ Gnuradio
C++ Gnuradio
This is a standard question that every one of us needs to consider before starting our own OOT
module. Tutorial 3 already addressed ways to select the programming language of choice for the
blocks in OOT module using gr_modtool
or
$ gr_modtool add -t sync -l cpp # This is the default
Apart from the compiler and interpreter, there are many differences out there. To decide upon the
choice of the programming language, it is important that we limit the differences from the GNU
Radio perspective. Primarily, it depends more on the objective of the OOT module. As far as the
performance is concerned, implementing the blocks in C++ makes more sense, and if the
performance of the OOT module is the not main issue Python would be a good choice, as it is
concise yet simple. Moreover, Python allows faster prototyping as we don't have to compile to
test the modules.
For a detailed explanation of gr_modtool commands, go here, or have a quick peek at the cheat
sheet.
4.2.1 Objective
When this tutorial is complete, we will be able to build this flow graph:
The flowgraph demonstrates a QPSK transceiver chain with the block My QPSK Demodulator
block module under the OOT tutorial. We will be building this block using C++. All other
blocks are standard GNU Radio blocks.
As in the previous tutorial, My QPSK Demodulator consumes QPSK symbols which are
complex valued floats at the input and produces the alphabets as bytes at output. We will plot the
binary values (from 0 through 3) as well as the transmitted complex symbols during operation.
xyz@comp:mydir$ cd gr-tutorial
xyz@comp:mydir/gr-tutorial$ ls
apps cmake CMakeLists.txt docs examples grc include lib python swig
my_qpsk_demod_cb represents class name of the block, where the suffix, 'cb' is added to the
block name, which conform to the GNU Radio nomenclature. 'cb' states the block established
that takes complex data as input and spits byte as output.
Enter code type: general
In GNU Radio, there exist different kinds of blocks: general, sync, interpolator/decimator,
source/sink, Hierarchical, etc. Depending on the choice of our block, gr_modtool adds the
corresponding code and functions. As illustrated, for my_qpsk_demod_cb block, we opt for a
general block. The following section will discuss the purpose of the specific blocks in detail.
In many cases, the block demands a user interface. For my_qpsk_demod_cb, gray_code is
selected to be "default arguments".
Enter valid argument list, including default arguments: bool gray_code
Moreover, GNU Radio provides an option of writing test cases. This provides quality assurance
to the code written. If selected, the gr_modtool adds the quality assurance files corresponding to
python and C++.
Add Python QA code? [Y/n]
Add C++ QA code? [y/N] y
With this, we have already established the GNU Radio semantics for our block coupled with the
OOT module. In the following sections, we will focus on the implementation of our block.
The detailed description of coding structure for the block can be found here
/*!
* The private constructor
*/
my_qpsk_demod_cb_impl::my_qpsk_demod_cb_impl(bool gray_code)
: gr::block("my_qpsk_demod_cb",
gr::io_signature::make(<+MIN_IN+>, <+MAX_IN+>,
sizeof(<+ITYPE+>)),
gr::io_signature::make(<+MIN_OUT+>, <+MAX_OUT+>,
sizeof(<+OTYPE+>)))
{}
<ITYPE> and <OTYPE> indicates the datatypes for the input and output port/s which
needs to be filled out manually.
Next, we need to modify the constructor. After modification, it looks like this:
/*!
* The private constructor
*/
my_qpsk_demod_cb_impl::my_qpsk_demod_cb_impl(bool gray_code)
: gr::block("my_qpsk_demod_cb",
gr::io_signature::make(1, 1, sizeof(gr_complex)),
gr::io_signature::make(1, 1, sizeof(char))),
d_gray_code(gray_code)
{}
The option gray_code is copied to the class attribute d_gray_code. Note that we need
to declare this a private member of the class in the header file
my_qpsk_demod_cb_impl.h,
private:
bool d_gray_code;
Also inside this class is the method general_work(), which is pure virtual in
gr::block, so we definitely need to override that. After running gr_modtool,
the skeleton version of this function will look something like this:
int
my_qpsk_demod_cb_impl::general_work (int noutput_items,
gr_vector_int &ninput_items,
gr_vector_const_void_star &input_items,
gr_vector_void_star &output_items)
{
const <+ITYPE*> *in = (const <+ITYPE*> *) input_items[0];
<+OTYPE*> *out = (<+OTYPE*> *) output_items[0];
// Do <+signal processing+>
// Tell runtime system how many input items we consumed on
// each input stream.
consume_each (noutput_items);
There is one pointer to the input- and one pointer to the output buffer, respectively, and a
for-loop which processes the items in the input buffer and copies them to the output
buffer. Once the demodulation logic is implemented, the structure of the work function
has the form
int
my_qpsk_demod_cb_impl::general_work (int noutput_items,
gr_vector_int &ninput_items,
gr_vector_const_void_star &input_items,
gr_vector_void_star &output_items)
{
const gr_complex *in = (const gr_complex *) input_items[0];
unsigned char *out = (unsigned char *) output_items[0];
gr_complex origin = gr_complex(0,0);
// Perform ML decoding over the input iq data to generate
alphabets
for(int i = 0; i < noutput_items; i++)
{
// ML decoder, determine the minimum distance from all
constellation points
out[i] = get_minimum_distances(in[i]);
}
unsigned char
my_qpsk_demod_cb_impl::get_minimum_distances(const gr_complex
&sample)
{
if (d_gray_code) {
unsigned char bit0 = 0;
unsigned char bit1 = 0;
// The two left quadrants (quadrature component < 0) have this
bit set to 1
if (sample.real() < 0) {
bit0 = 0x01;
}
// The two lower quadrants (in-phase component < 0) have this
bit set to 1
if (sample.imag() < 0) {
bit1 = 0x01 << 1;
}
return bit0 | bit1;
} else {
// For non-gray code, we can't simply decide on signs, so we
check every single quadrant.
if (sample.imag() >= 0 and sample.real() >= 0) {
return 0x00;
}
else if (sample.imag() >= 0 and sample.real() < 0) {
return 0x01;
}
else if (sample.imag() < 0 and sample.real() < 0) {
return 0x02;
}
else if (sample.imag() < 0 and sample.real() >= 0) {
return 0x03;
}
}
}
Note the function declaration also needs to be added to the class header
(my_qpsk_demod_cb_impl.h).
The function get_minimum_distances is a maximum likelihood decoder for the QPSK
demodulater. Theoretically, the function should compute the distance from each ideal QPSK
symbol to the received symbol (It is mathematically equivalent to determining the Voronoi
regions of the received sample). For a QPSK signal, these Voronoi regions are simply four
quadrants in the complex plane. Hence, to decode the sample into bits, it makes sense to map the
received sample to these quadrants.
Now, let's consider the forecast() function. The system needs to know how much data is
required to ensure validity in each of the input arrays. As stated before, the forecast() method
provides this information, and you must therefore override it anytime you write a gr::block
derivative (for sync blocks, this is implicit).
Although the 1:1 implementation worked for my_qpsk_demod_cb, it wouldn't be appropriate for
interpolators, decimators, or blocks with a more complicated relationship between
noutput_items and the input requirements. That said, by deriving your classes from
gr::sync_block, gr::sync_interpolator or gr::sync_decimator instead of gr::block,
you can often avoid implementing forecast.
Refilling the private constructor and overriding the general_work() and forecast() will
suffice the coding structure of our block. However, in the gr::block class there exists more
specific functions. These functions are covered under advanced topics section
Default version:
<?xml version="1.0"?>
<block>
<name>my_qpsk_demod_cb</name>
<key>tutorial_my_qpsk_demod_cb</key>
<category>tutorial</category>
<import>import tutorial</import>
<make>tutorial.my_qpsk_demod_cb($gray_code)</make>
<!-- Make one 'param' node for every Parameter you want settable from the
GUI.
Sub-nodes:
* name
* key (makes the value accessible as $keyname, e.g. in the make node)
* type -->
<param>
<name>...</name>
<key>...</key>
<type>...</type>
</param>
<?xml version="1.0"?>
<param>
<name>Gray Code</name>
<key>gray_code</key>
<value>True</value>
<type>bool</type>
<option>
<name>Yes</name>
<key>True</key>
</option>
<option>
<name>No</name>
<key>False</key>
</option>
</param>
Like the work function, the datatypes for the input and output ports represented by <sink> and
<source> tags should be modified.
<sink>
<name>in</name>
<type>complex</type>
</sink>
<source>
<name>out</name>
<type>byte</type>
</source>
After all the necessary modification the "tutorial_my_qpsk_demod_cb.xml" looks like this:
Modified version:
<?xml version="1.0"?>
<block>
<name>My QPSK Demodulator</name>
<key>tutorial_my_qpsk_demod_cb</key>
<category>tutorial</category>
<import>import tutorial</import>
<make>tutorial.my_qpsk_demod_cb($gray_code)</make>
<param>
<name>Gray Code</name>
<key>gray_code</key>
<value>True</value>
<type>bool</type>
<option>
<name>Yes</name>
<key>True</key>
</option>
<option>
<name>No</name>
<key>False</key>
</option>
</param>
<sink>
<name>in</name>
<type>complex</type>
</sink>
<source>
<name>out</name>
<type>byte</type>
</source>
</block>
sequence:
By following the steps of writing the OOT module, we did manage to produce the byte stream at
the output of QPSK demodulator, still, it doesn't guarantee the correct working of our block. In
this situation, it becomes significant to write unit test for our module that certifies the clean
implementation of the QPSK demodulator.
Below we see the source of code of the qa_qpsk_demod.py can be found under python/
Full QA code
if __name__ == '__main__':
gr_unittest.run(qa_qpsk_demod, "qa_qpsk_demod.xml")
It can be easily noticed that the qa_qpsk_demod is implemented in python, in spite of we opted
C++ in the first case for writing our blocks. This is because, GNU Radio inherits the python
unittest framework to support quality assurance. And, if you remember it correctly from previous
tutorials, swig as part of GNU Radio framework, provides python bindings for the C++ code.
Hence, we are able to write the unit test for our block qa_qpsk_demod in python.
So lets gather a bit of know how on how to write test cases for the block. Okay, lets consider the
header part first:
from gnuradio import gr, gr_unittest and from gnuradio import blocks are the
standard lines that includes gr, gr_unittest functionality in the qa_ file. import tutorial_swig
as tutorial import the python bidden version of our module, which provides an access our
block my_qpsk_demod_cb. Finally, from numpy import array includes array.
if __name__ == '__main__':
gr_unittest.run(qa_qpsk_demod, "qa_qpsk_demod.xml")
The qa_ file execution start by calling this function. The gr_unittest automatically calls the
functions in a specific order def setUp (self) for creating the top block at the start, tearDown
(self) for deleting the top block at the end. In between the setUp and tearDown the test cases
defined are executed. The methods starting with prefix test_ are recognized as test cases by
gr_unittest. We have defined two test cases test_001_gray_code_enabled and
test_002_gray_code_disabled. The usual structure of a test cases comprises of a known input
data and the expected output. A flowgraph is created to include the source (input data), block to
be tested (processor) and sink (resulted output data). In the end the expected output is compared
with the resulted output data.
self.assertTupleEqual(expected_result, result_data)
self.assertEqual(len(expected_result), len(result_data))
determine the result of test cases as passed or failed. The test cases are executed before
installation of the module by running make test: Output
In the output above, one of the test failed, however, Test 3 belonging to the qa_qpsk_demod,
claims to have passed the test cases.
Congratulations, we have just finished writing our OOT module gr-tutorial and a C++ block
my_qpsk_demodulator.
To add physical meaning to the discussion, we have taken assistance of the existing modules.
The source code excerpts are included thereof. Enthusiastic readers are suggested to open the
source code in parallel and play around with their functionalities.
4.3.1 Specific functions related to block
In the last section, we managed out implementation of our block by defining functions like
general_work and forecast(). But sometimes special functions need to be defined for the
implementation. The list is long, but we try to discuss same of these functions in the following
subsections.
4.3.1.1 set_history()
If your block needs a history (i.e., something like an FIR filter), call this in the constructor.
Here is an example
GNU Radio then makes sure you have the given number of 'old' items available.
The smallest history you can have is 1, i.e., for every output item, you need 1 input item. If you
choose a larger value, N, this means your output item is calculated from the current input item
and from the N-1 previous input items.
The scheduler takes care of this for you. If you set the history to length N, the first N items in the
input buffer include the N-1 previous ones (even though you've already consumed them).
The history is stored in the variable d_history.
The set_history() is defined in gnuradio/gnuradio-runtime/block.cc
4.3.1.2 set_output_multiple()
When implementing your general_work() routine, it's occasionally convenient to have the run
time system ensure that you are only asked to produce a number of output items that is a multiple
of some particular value. This might occur if your algorithm naturally applies to a fixed sized
block of data. Call set_output_multiple in your constructor to specify this requirement,
code
Lets consider an example, say we want to generate outputs only in a 64 elements chunk, by
setting d_output_multiple to 64 we can achieve this, but note that we can also get multiples of 64
i.e. 128, 256 etc
The definition of set_output_multiple can be found in gnuradio/gnuradio-runtime/block.cc
d_output_multiple_set = true;
d_output_multiple = multiple;
}
Block Functionality
General This block a generic version of all blocks
Source/Sinks The source/sink produce/consume the input/output items
The interpolation/decimation block is another type of fixed rate block
Interpolation/Decimation where the number of output/input items is a fixed multiple of the
number of input/output items.
The sync block allows users to write blocks that consume and produce
an equal number of items per port. From the user perspective, the GNU
Sync
Radio scheduler synchronizes the input and output items, it has
nothing to with synchronization algorithms
Hierarchical blocks Hierarchical blocks are blocks that are made up of other blocks.
In the next subsections we discuss these blocks in detail. Again, enthusiastic readers can find
these blocks in the GNU Radio source code.
4.3.2.1 General
howto_square_ff::howto_square_ff ()
: gr::block("square_ff",
gr::io_signature::make(MIN_IN, MAX_IN, sizeof (float)),
gr::io_signature::make(MIN_OUT, MAX_OUT, sizeof (float)))
{
// nothing else required in this example
}
Source
Some observations:
Because it connected with the hardware USRP, the gr uhd usrp sink is a sub class of
sync_block.
Sink
Some observations:
Because it connected with the hardware USRP, the gr uhd usrp sink is a sub class of
sync_block.
4.3.2.3 Sync
The sync block allows users to write blocks that consume and produce an equal number of items
per port. A sync block may have any number of inputs or outputs. When a sync block has zero
inputs, its called a source. When a sync block has zero outputs, its called a sink.
Decimators
The decimation block is another type of fixed rate block where the number of input items is a
fixed multiple of the number of output items.
#include <gr_sync_decimator.h>
The user must assume that the number of input items = noutput_items*decimation. The
value ninput_items is therefore implicit.
Interpolation
The interpolation block is another type of fixed rate block where the number of output items is a
fixed multiple of the number of input items.
#include <gnuradio/sync_interpolator.h>
The user must assume that the number of input items = noutput_items/interpolation
Hierarchical blocks are blocks that are made up of other blocks. They instantiate the other GNU
Radio blocks (or other hierarchical blocks) and connect them together. A hierarchical block has a
"connect" function for this purpose.
Hierarchical blocks provides us modularity in our flowgraphs by abstracting simple blocks, that
is hierarchical block helps us define our specific blocks at the same time provide us the
flexibility to change it, example, we would like to test effect of different modulation schemes for
a given channel model. However our synchronization algorithms are specific or newly published.
We define our hier block as gr-my_sync that does synchronization followed equalizer and
demodulation. We start with BPSK, the flowgraph looks like
Now, our flowgraph looks decent. Secondly, we abstracted the complex functionality of our
synchronization. Shifting to QPSK, where the synchronization algorithm remains the same, we
just replace the gr-bpsk_demod with gr-qpsk_demod
Hierarchical blocks define an input and output stream much like normal blocks. To connect input
i to a hierarchical block, the source is (in Python):
class ofdm_receiver(gr.hier_block2)
and instantiated as
gr.hier_block2.__init__(self, "ofdm_receiver",
gr.io_signature(1, 1, gr.sizeof_gr_complex), #
Input signature
gr.io_signature2(2, 2,
gr.sizeof_gr_complex*occupied_tones, gr.sizeof_char)) # Output signature
Some main tasks performed by the OFDM receiver include channel filtering, synchronization
and IFFT tasks. The individual tasks are defined inside the hierarchical block.
Channel filtering
Synchronization
self.chan_filt = blocks.multiply_const_cc(1.0)
nsymbols = 18 # enter the number of symbols per packet
freq_offset = 0.0 # if you use a frequency offset, enter it here
nco_sensitivity = -2.0/fft_length # correct for fine frequency
self.ofdm_sync = ofdm_sync_fixed(fft_length,
cp_length,
nsymbols,
freq_offset,
logging)
ODFM demodulation
Finally, the individual blocks along with hierarchical are connected among each to form a flow
graph.
Connection between the hierarchical block OFDM receiver to channel filter block
Connection between the channel filter block to the OFDM synchronization block.
self.connect(self.chan_filt, self.ofdm_sync)
and so forth.
Hierarchical blocks can also be nested, that is blocks defined in hierarchical blocks could also be
hierarchical blocks. For example, OFDM sync block is also an hierarchical block. In this
particular case it is implemented in C++. Lets have a look into it.
Underneath is instant of the hierarchical block. Don't panic by looking at its size, we just need to
grab the concept behind creating hierarchical blocks.
OFDM impl
gr::blocks::complex_to_mag_squared::sptr
normalizer_magsquare(gr::blocks::complex_to_mag_squared::make());
gr::filter::fir_filter_ccf::sptr delay_ma(gr::filter::fir_filter_ccf::make(1,
std::vector<float>(fft_len/2, use_even_carriers ? 1.0 : -1.0)));
gr::blocks::complex_to_mag_squared::sptr
delay_magsquare(gr::blocks::complex_to_mag_squared::make());
gr::blocks::divide_ff::sptr
delay_normalize(gr::blocks::divide_ff::make());
gr::blocks::complex_to_mag_squared::sptr
normalizer_magsquare(gr::blocks::complex_to_mag_squared::make());
gr::filter::fir_filter_fff::sptr
normalizer_ma(gr::filter::fir_filter_fff::make(1, std::vector<float>(fft_len,
0.5)));
gr::blocks::multiply_ff::sptr
normalizer_square(gr::blocks::multiply_ff::make());
gr::blocks::complex_to_arg::sptr
peak_to_angle(gr::blocks::complex_to_arg::make());
gr::blocks::sample_and_hold_ff::sptr
sample_and_hold(gr::blocks::sample_and_hold_ff::make());
gr::blocks::plateau_detector_fb::sptr
plateau_detector(gr::blocks::plateau_detector_fb::make(cp_len));
// Delay Path
connect(self(), 0, delay, 0);
connect(delay, 0, delay_conjugate, 0);
connect(delay_conjugate, 0, delay_corr, 1);
connect(self(), 0, delay_corr, 0);
connect(delay_corr, 0, delay_ma, 0);
connect(delay_ma, 0, delay_magsquare, 0);
connect(delay_magsquare, 0, delay_normalize, 0);
// Energy Path
connect(self(), 0, normalizer_magsquare, 0);
connect(normalizer_magsquare, 0, normalizer_ma, 0);
connect(normalizer_ma, 0, normalizer_square, 0);
connect(normalizer_ma, 0, normalizer_square, 1);
connect(normalizer_square, 0, delay_normalize, 1);
// Fine frequency estimate (output 0)
connect(delay_ma, 0, peak_to_angle, 0);
connect(peak_to_angle, 0, sample_and_hold, 0);
connect(sample_and_hold, 0, self(), 0);
// Peak detect (output 1)
connect(delay_normalize, 0, plateau_detector, 0);
connect(plateau_detector, 0, sample_and_hold, 1);
connect(plateau_detector, 0, self(), 1);
#ifdef SYNC_ADD_DEBUG_OUTPUT
// Debugging: timing metric (output 2)
connect(delay_normalize, 0, self(), 2);
#endif
}
Let's understand the code piece wise. The hierarchical block is C++ is instantiated as follows:
The individual blocks inside the ofdm_sync_sc_cfb block are defined as follows:
gr::blocks::complex_to_mag_squared::sptr
normalizer_magsquare(gr::blocks::complex_to_mag_squared::make());