BlocksCodingGuide

= Terminology =

-

= Coding Structure =

Public Header Files
The public header files are defined in include/foo and get installed into $prefix/include/foo.

The accessors (set/get) functions that are to be exported are defined as pure virtual functions in these header files.

A skeleton of a typical public header file looks like:

Implementation Header File
The private implementation header files are defined in lib and do not get installed. We normally define these files to use the same name as the public file and class with a '_impl' suffix to indicate that this is the implementation file for the class.

In some cases, this file might be specific to a particular implementation and multiple implementations might be available for a given block but with the same public API. A good example is the use of the FFTW library for implementing the fft_filter blocks. This is only one of many possible ways to implement an FFT, so the implementation was named fft_filter_ccc_fftw. Another library that implements an FFT specific to a platform or purpose could then be slotted in as a new implementation like fft_filter_ccc_myfft.

All member variables are declared private and use the prefix 'd_'. As much as possible, all variables should have a set and get function. The set function looks like void set_var(dtype var), and the get function looks like dtype var. While it does not always make sense to have a set or get for a particular variable, all efforts should be made to accommodate this standard.

The Doxygen comments that will be included in the manual are defined in the public header file. There is no need for Doxygen markup in the private files, but of course, any comments or documentation that make sense should always be used.

The skeleton of the a typical private header file looks like:

Implementation Source File
The source file is lib/bar.cc and implements the actual code for the class.

This file defines the make function for the public class. This is a member of the class, which means that we can, if necessary, do interesting things, define multiple factor functions, etc. Most of the time, this simply returns an sptr to the implementation class.

SWIG Interface File
Because of the use of the public header file to describe what we want publicly accessible, we can simply include the headers in the main interface file. So in the directory swig is a single interface file foo_swig.i:

NOTE: We are using "GR_SWIG_BLOCK_MAGIC2" for the definitions now. When we are completely converted over, this will be replaced by "GR_SWIG_BLOCK_MAGIC".

= Block Structure =

The work function
To implement processing, the user must write a "work" routine that reads inputs, processes, and writes outputs.

An example work function implementing an adder in c++

Parameter definitions:


 * noutput_items: total number of items in each output buffer
 * input_items: vector of input buffers, where each element corresponds to an input port
 * output_items: vector of output buffers, where each element corresponds to an output port

Some observations:


 * Each buffer must be cast from a void* pointer into a usable data type.
 * The number of items in each input buffer is implied by noutput_items (more information on this in later sections).
 * The number of items produced is returned, this can be less than noutput_items.

IO signatures
When creating a block, the user must specify the following:


 * The number of input ports
 * The number of output ports
 * The item size of each port

An IO signature describes the number of ports a block may have and the size of each item in bytes. Each block has 2 IO signatures: an input signature, and an output signature.

Some example signatures in c++

Some observations:


 * Use gr_make_io_signature for blocks where all ports are homogenous in size
 * Use gr_make_io_signaturev for blocks that have heterogeneous port sizes

The first two parameters are min and max number of ports, this allows blocks to have a selectable number of ports at runtime.

-

= Block types =

To take advantage of the gnuradio framework, users will create various blocks to implement the desired data processing. There are several types of blocks from which to choose:


 * Synchronous Blocks (1:1)
 * Decimation Blocks (N:1)
 * Interpolation Blocks (1:M)
 * Basic Blocks (N:M)

Synchronous Block
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.

An example sync block in c++

Some observations:


 * noutput_items is the length in items of all input and output buffers
 * an input signature of gr_make_io_signature(0, 0, 0) makes this a source block
 * an output signature of gr_make_io_signature(0, 0, 0) makes this a sink block

Decimation Block
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.

An example decimation block in c++

Some observations:


 * The gr_sync_decimator constructor takes a 4th parameter, the decimation factor
 * The user should assume that the number of input items = noutput_items*decimation

Interpolation Block
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.

An example interpolation block in c++

Some observations:


 * The gr_sync_interpolator constructor takes a 4th parameter, the interpolation factor
 * The user should assume that the number of input items = noutput_items/interpolation

Basic Block
The basic block provides no relation between the number of input items and the number of output items. All other blocks are just simplifications of the basic block. Users should choose to inherit from basic block when the other blocks are not suitable.

The adder revisited as a basic block in c++

Some observations:


 * This class overloads the general_work method, not work
 * The general work has a parameter: ninput_items
 * ninput_items is a vector describing the length of each input buffer
 * Before return, general_work must manually consume the used inputs
 * The number of items in the input buffers is assumed to be noutput_items
 * Users may alter this behaviour by overloading the forecast method

-

= Other Types of Blocks =

Hierarchical Block
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 define an input and output stream much like normal blocks. To connect input i to a hierarchical block, the source is (in Python):

Similarly, to send the signal out of the block on output stream o:

Top Block
The top block is the main data structure of a GNU Radio flowgraph. All blocks are connected under this block. The top block has the functions that control the running of the flowgraph. Generally, we create a class that inherits from a top block:

class my_topblock(gr.top_block): def __init__(self, ): gr.top_block.__init__(self)

def main: tb = my_topblock tb.run The top block has a few main member functions:


 * start(N): starts the flow graph running with N as the maximum noutput_items any block can receive.
 * stop: stops the top block
 * wait: blocks until top block is finished
 * run(N): a blocking start(N) (calls start then wait)
 * lock: locks the flowgraph so we can reconfigure it
 * unlock: unlocks and restarts the flowgraph

The N concept allows us to adjust the latency of a flowgraph. By default, N is large and blocks pass large chunks of items between each other. This is designed to maximize throughput and efficiency. Since large chunks of items incurs latency, we can force these chunks to a maximum size to control the overall latency at the expense of efficiency. A set_max_noutput_items(N) method is defined for a top block to change this number, but it only takes effect during a lock/unlock procedure.

-

= Stream Tags =

A tag decorates a stream with metadata. A tag is associated with a particular item in a stream. An item may have more than one tag associated with it. The association of an item and tag is made through an absolute count. Every item in a stream has an absolute count. Tags use this count to identify which item in a stream to which they are associated.

A tag has the following members:


 * offset: the unique item count
 * key: a PMT key unique to the type of contents
 * value: a PMT holding the contents of this tag
 * srcid: a PMT id unique to the producer of the tag (optional)

A PMT is a special data type in gnuradio to serialize arbitrary data. To learn more about PMTs see https://wiki.gnuradio.org/index.php/Polymorphic_Types_(PMTs)

Reading stream tags
Tags can be read from the work function using get_tags_in_range. Each input port/stream can have associated tags.

Example reading tags in c++

Writing stream tags
Tags can be written from the work function using add_item_tag. Each output port/stream can have associated tags.

Example writing tags in C++:

= Tips and Tricks =

This is the part of the guide where we give tips and tricks for making blocks that work robustly with the scheduler.

Blocking calls
If a work function contains a blocking call, it must be written in such a way that it can be interrupted by boost threads. When the flow graph is stopped, all worker threads will be interrupted. Thread interruption occurs when the user calls unlock or stop on the flow graph. Therefore, it is only acceptable to block indefinitely on a boost thread call such a sleep or condition variable, or something that uses these boost thread calls internally such as pop_msg_queue. If you need to block on a resource such as a file descriptor or socket, the work routine should always call into the blocking routine with a timeout. When the operation times out, the work routine should call a boost thread interruption point or check boost thread interrupted and exit if true.

Saving state
Because work functions can be interrupted, the block's state variables may be indeterminate next time the flow graph is run. To make blocks robust against indeterminate state, users should overload the blocks start and stop functions. The start routine is called when the flow graph is started before the work thread is spawned. The stop routine is called when the flow graph is stopped after the work thread has been joined and exited. Users should ensure that the state variables of the block are initialized property in the start routine.