Creating OOT Module in GR 4.0

From GNU Radio
Jump to navigation Jump to search


The process to create a GR 4.0 custom block in an OOT is very simple:

  • Create the OOT
  • Create the Block
  • Edit the .yml file
  • Implement the constructor
  • Implement the work method

Creating the OOT

In place of modtool, there is a script in the gnuradio source tree that will perform the same function

python3 $GR_PREFIX/src/gnuradio/utils/modtool/ myOOT

This will create a skeleton OOT with the following structure:

├── blocklib
│   └── myOOT
│       ├── include
│       │   └── gnuradio
│       │       └── myOOT
│       │           └──
│       ├── lib
│       │   └──
│       ├── python
│       │   └── myOOT
│       │       └──
│       └── test
│           └──
└── meson_options.txt

Creating a Block

To create a block, there is a helper scripts in the gnuradio source tree:

cd gr4-myOOT
python3 $GR_PREFIX/src/gnuradio/utils/modtool/ myblock

This will create the necessary files for compiling a block by placing a minimal set of files in the following structure:

├── blocklib
│   └── myOOT
│       ├── myblock
│       │   ├──
│       │   ├── myblock_cpu.h
│       │   └── myblock.yml

All of the files needed to create the block are located in one place, and all of the normal boilerplate will be automatically generated.

From here let's start with the .yml file

YML File

Top level properties

module: myOOT
block: myblock
label: myblock
blocktype: sync_block

The first section contains the top level properties. Module and block need to match the module/filename.

Label corresponds to how it would be displayed in GRC or other interface.

Blocktype currently can be block, sync_block, or grc and corresponds to the type of block that will be implemented


# Example Parameters
-   id: itemsize
    label: Item Size
    dtype: size
    settable: false
    default: 0
        hide: part

Next we declare the parameters in the block. These correspond to the constructor of the block, as well as setters and getters if desired. The example parameter that is given is for an itemsize parameter, which is by default set to 0 - this can be used to attach an untyped port to an arbitrarily typed port, in the case of e.g. a head or throttle block that doesn't really consider the type of the incoming/outgoing data


dtype uses sigmf data type names, and should be from the set (listed as c++, python, and grc type)

type_lookup = {
    'cf64': ['std::complex<double>', 'complex', 'complex'],
    'cf32': ['std::complex<float>', 'complex', 'complex'],
    'rf64': ['double', 'float', 'float'],
    'rf32': ['float', 'float', 'float'],
    'ri64': ['int64_t', 'int', 'int'],
    'ri32': ['int32_t', 'int', 'int'],
    'ri16': ['int16_t', 'int', 'short'],
    'ri8': ['int8_t', 'int', 'byte'],
    'ru64': ['uint64_t', 'int', 'int'],
    'ru32': ['uint32_t', 'int', 'int'],
    'ru16': ['uint16_t', 'int', 'short'],
    'ru8': ['uint8_t', 'int', 'byte'],
    'size': ['size_t', 'int', 'int'],
    'string': ['std::string', 'str', 'string'],
    'bool': ['bool', 'bool', 'bool'],


This gives the parameter a name, which will show up in the constructor arguments as well as the pmt object instantiated to represent this parameter in the base block


If the settable is set true (default false), the parameter can be changed at runtime via an autogenerated callback method settable=true also enables a getter method - but settable=false/gettable=true will only provide the getter


The value set for default is a hard default in the constructor arguments. All parameters after this must also have a default set. Parameters with a default value are given keyword arguments in the python bindings


The grc section is passed directly to the generated grc bindings as these pertain specifically to grc fields. These include: - category - hide - default (a soft default when a block is placed on the canvas)


# Example Ports
-   domain: stream
    id: in
    direction: input
    type: untyped
    size: parameters/itemsize

-   domain: stream
    id: out
    direction: output
    type: untyped
    size: parameters/itemsize

The ports section includes both message and streaming ports, which can be of different types


stream or message


The unique (for this block) id for the port


input or output


untyped - This is a sized port where the block knows nothing about the type of the data being consumed. {cf32, rf32, ri32, ...} - The sigmf types described above. Field not used for message ports


For untyped ports, this fixes the size of the port so it is checked at flowgraph initialization. If set to 0, that check is bypassed and it is up to the block to infer the size of the data from the incoming buffer.


-   id: cpu
# -   id: cuda

The Implementations section allows multiple implementations for each block. cpu is the default, which would then automatically generate the compile targets for

If an implementation is python, this needs to be indicated as follows

-   id: cpu
-   id: numpy
    lang: python
    domain: cpu

Templated Blocks

As was commonly, but difficultly implemented in GR 3.x, templated blocks use c++ templating to allow selectable types, and this workflow removes much of the boilerplate code associated with this mechanism.

In the .yml file, a typekeys section is added that specifies a series of supported types for the block implementations. Multiple types can be implemented with different ids

  - id: T
    type: class
        - cf32
        - rf32
        - ri32
        - ri16
        - ri8

For the above typekeys, blocks will be compiled as myblock_c, myblock_f, etc. corresponding to the suffix that maps to the type option

Implementation Files

The and myblock_cpu.h are designed to get the developer as quickly to the work method as possible

#include <gnuradio/myOOT/myblock.h>

namespace gr {
namespace myOOT {

class myblock_cpu : public virtual myblock
    myblock_cpu(block_args args);
    work_return_t work(work_io& wio) override;

    // private variables here

The provided .h file is very simple and only contains the constructor and work method and derives from an autogenerated base block (lives in the build/ directory)


First, notice that the arguments to the constructor are contained in a block_args struct - this is to minimize the number of things that need to be changed when adding/removing/changing a parameter. This struct will contain in order and with provided defaults the parameters from the .yml file and be updated when that file changes.

Class Variables

Two things that are not necessary to implement

- Setters/Getters
- Local variables that correspond to block parameters

This is because the parameter mechanism handles this in a consistent fashion that we will explore later.

The file implements the constructor (some boilerplate abstracted from the INHERITED_CONSTRUCTORS macro and the work method

#include "myblock_cpu.h"
#include "myblock_cpu_gen.h"

namespace gr {
namespace myOOT {

myblock_cpu::myblock_cpu(block_args args) : INHERITED_CONSTRUCTORS {}

work_return_t myblock_cpu::work(work_io& wio)
    // Do <+signal processing+>
    // Block specific code goes here
    return work_return_t::OK;

} // namespace myOOT
} // namespace gr

Autogenerated Files