Gnuradio: Developing your own blocks - 2020-UQ-Communication-Systems/public GitHub Wiki
The authoritative reference used for this guide is Gnuradio/OutOfTreeModules
You can create both Python and C++ modules. Python blocks are slower, and C++ blocks are faster.
Here we take an example of a modified BMC demodulator. The same module is made in Matlab/Octave, Python and pure C++.
As most people are relatively familiar with Matlab code, we will use the demodm2.m function as a template for the next two sections. This example is a little more involved than more trivial ones often used to explain the progress. It requires initialization of two parameters, the number of samples per bit as well as the threshold used to distinguish ones and zeros.
The first step is to decide on your "module" name. There is a nice tool called gr_modtool
for this. In the example below I've selected uqcoms as the module name. Next, I'm creating a block called demodm2. Note filenames with dashes in them will cause problems - avoid it if you can.
$ gr_modtool newmod uqcoms
$ cd uqcoms
$ gr_modtool add demodm2
We answer that the code type is general
, and the argument list is int samples_per_bit, float threshold
. See the end of this section for the output we expect to see. To be compatible with grc, the argument list cannot use unsigned char
as a type. Using int, short, float, gr_complex are all OK.
Next we go into the lib/
directory. There is a demodm2_impl.cc
and demodm2_impl.h
, which are the C++ code for the block we are writing.
First let's modify demodm2_impl.h
. To store the two arguments we set. Under private we add these two lines
private:
// local variables
unsigned char my_samples_per_bit;
float my_threshold;
Next, let's modify demodm2_impl.cc
to do the work we want it to do.
Modify the constructor, we need to set the block to have a max/min of 1 stream for both input and output. Our input is of type float, our output is of type unsigned char (byte). Next we want to initialize the local variables that we specified for the block.
demodm2_impl::demodm2_impl(unsigned char samples_per_bit, float threshold)
: gr_block("demodm2",
gr_make_io_signature(1, 1, sizeof(float)),
gr_make_io_signature(1, 1, sizeof(unsigned char))),
my_samples_per_bit(samples_per_bit),
my_threshold(threshold)
Next, we modify the forecast function. As we have samples_per_bit input samples, converting into one output sample, we need to tell gnuradio about this.
void
demodm2_impl::forecast (int noutput_items, gr_vector_int &ninput_items_required)
{
int n = noutput_items * my_samples_per_bit;
ninput_items_required[0] = (n==0 ? 1 : n);
}
Now the work function, replace <+ITYPE*> with float, and <+OTYPE*> with unsigned char.
Also, we can now add our code. Having the same purpose, each line of code in matlab has very similar code in the C++ function. The biggest difference between the two is that to make a gnuradio module many "decorator" functions are required to allow this block to be used as part of a larger system.
The final implementation in C++ is demodm2_impl.cc and is inside gnuradio/gr-uqcoms/lib/demodm2_impl.cc
After all the code is written we go to the root directory and build it:
mkdir build
cd build
cmake ../
make
sudo make install
ldconfig
So that this block works in gnuradio-companion we need to build an XML description for it. We can do this with the command gr_modtool makexml demodm2
.
We get the following information coming up.
GNU Radio module name identified: uq-coms
Enter code type: general
Language: C++
Block/code identifier: demodm2
Enter valid argument list, including default arguments: int samples_per_bit, float threshold
Add Python QA code? [Y/n] Y
Add C++ QA code? [y/N] Y
Adding file 'demodm2_impl.h'...
Adding file 'demodm2_impl.cc'...
Adding file 'demodm2.h'...
Adding file 'qa_demodm2.cc'...
Adding file 'qa_demodm2.h'...
Editing swig/uq-coms_swig.i...
Adding file 'qa_demodm2.py'...
Editing python/CMakeLists.txt...
Adding file 'uq-coms_demodm2.xml'...
Editing grc/CMakeLists.txt...
Next we can now open gnuradio-companion, and add this new custom block. It will be in a group called 'UQCOMS'. (C++ version - note that the python version will be in UQComs)
To test our module inside Gnuradio, follow the screenshot of the flowgraph in the figure below:
NOTE: to make your own images of your flowgrpahs you can use File... Screen Capture.
.
The basic flow graph includes vector source
=> vector to stream
=> demodm2
=> throttle
=> char to float
=> scope sink
In the diagram you can see the input being specified as a vector source. This is a convenient way to specify a vector of inputs. Note that the vector has a length of 8
which means that two "bits" are being presented in this vector. The vector you specify is very similar to how you would test this in Matlab itself (or any other programming language).
NOTE: the reason for using a throttle is that there is no physical device (eg SDR) connected to our flow graph. Without it, gnuradio will let the computer execute this as fast as possible, so to ensure smooth operation - use a throttle block!
For python you can also use the tool gr_modtool
. However to shortcut the process we can just create the XML file and python file separately.
In the python file you need a class and at least a work
function.
To mirror the example above, the general block becomes a plain gr.basic_block
, which means
we need to implement the forecast
and general_work
functions.
NOTE: if your input and output are completely synchronized, then you can use a gr.sync_block
which means
the forecast will know that # inputs = # outputs.
The local variables would go right after the class definition.
my_samples_per_bit
my_threshold
Then the class initializer would be
def __init__(self, samples_per_bit, threshold):
gr.basic_block.__init__(self,
name="demodm2",
in_sig=[numpy.float32],
out_sig=[numpy.int8])
self.my_threshold = threshold
self.my_samples_per_bit = samples_per_bit
Next, the forecast function would be similar to the C++ one
def forecast(self, noutput_items, ninput_items_required):
n = noutput_items * self.my_samples_per_bit
ninput_items_required[0] = 1 if (n==0) else n
Lastly, the work function is defined as follows. Note: we need to consume the input to make sure gnuradio knows we have finished with that amount of input.
def general_work(self, input_items, output_items):
inp = input_items[0]
i = 0
# until we run-out of space in the output - or the input.
for o in range(min(len(input_items[0]), len(output_items[0]))):
# for the oth output symbol, lets get out data
Ts = self.my_samples_per_bit # samples per bit
tsum = [0, 0, 0, 0]; # 4 parts of the bit.
for z in range(i, i+Ts/4):
tsum[0] += inp[z]
for z in range(i+Ts/4+1, i+Ts/2):
tsum[1] += inp[z]
for z in range(i+Ts/2+1, i+Ts*3/4):
tsum[2] += inp[z]
for z in range(i+Ts*3/4, i+Ts):
tsum[3] += inp[z]
i += Ts;
decide = numpy.abs((tsum[0]+tsum[2]) - (tsum[1]+tsum[3]));
# how to debug an output... NOT recommended if the data rate is high
# print "%d\n" % decide
# set output (note it is reversed like in the MATLAB/Octave code)
output_items[0][o] = 0 if decide > self.my_threshold else 1;
self.consume(0, len(output_items[0]))
return len(output_items[0])
If a block is a sync block, you do not need to manually consume, or write a forecast function.
You can see the final two files inside the repository. As these files were made manually you will need to modify the XML file to include the correct path to your grc_gnuradio directory.