GR4 tutorial: flowgraphs 101

From GNU Radio
Revision as of 09:47, 23 August 2024 by Destevez (talk | contribs)
Jump to navigation Jump to search

This tutorial explains the basics of C++ flowgraphs in GNU Radio 4.0.

Minimal flowgraph

The following is an example of a minimal flowgraph. It connects a Null Source to a Null Sink and runs the flowgraph forever. The code for the flowgraph can be found in minimal_flowgraph.cpp.

#include <fmt/core.h>
#include <gnuradio-4.0/Graph.hpp>
#include <gnuradio-4.0/Scheduler.hpp>
#include <gnuradio-4.0/packet-modem/null_sink.hpp>
#include <gnuradio-4.0/packet-modem/null_source.hpp>
#include <boost/ut.hpp>

int main()
{
    using namespace boost::ut;

    gr::Graph fg;
    auto& source = fg.emplaceBlock<gr::packet_modem::NullSource<int>>();
    auto& sink = fg.emplaceBlock<gr::packet_modem::NullSink<int>>();
    expect(eq(gr::ConnectionResult::SUCCESS, fg.connect<"out">(source).to<"in">(sink)));

    gr::scheduler::Simple sched{ std::move(fg) };
    expect(sched.runAndWait().has_value());

    return 0;
}

Here the boost-ut expect() function is used for error handling, since it is also used for unit testing in GNU Radio 4.0, but any other form of error handling can be used.

Creating a flowgraph in GNU Radio 4.0 begins with the instantiation of a gr::Graph object.

The emplaceBlock() method of the gr::Graph is used to add blocks to the flowgraph. Note that this method takes a template parameter which specifies the block class. The block classes can be templates, and they often have template parameters to indicate the input and output types. Here we are creating a NullSource<int>, which has int as output type, and similarly for the null sink. As in GNU Radio 3.10, blocks can get parameters when they are created. These are passed as a gr::property_map argument to emplaceBlock(). In this case, the Null Source and Null Sink don't take any parameters, so emplaceBlock() is called without arguments. We will see an example of blocks with parameters below.

A connection between blocks is done by calling the connect() method of the gr::Graph. Note how the syntax works, using template parameters to specify the name of the ports. In GNU Radio 3.10, block ports have numbers 0, 1, 2, etc. These numbers are specified when calling connect, and 0 is implicit if the number is omitted. In GNU Radio 4.0, all the ports have names which are a string. In simple blocks which only have an input or an output, "in" and "out" are often used as port names. The connect() method checks that the connection is valid (the types of the ports match, etc.) and returns a gr::ConnectionResult value indicating if the connection could be made.

Once all the blocks have been added to the flowgraph and connected, the flowgraph can be run. To do this, it is first necessary to put the flowgraph in a scheduler. GNU Radio 3.10 has a single scheduler implemented in the runtime. In GNU Radio 4.0, schedulers are modular and users can implement their own, so any scheduler could be used here. In this example we use the gr::Scheduler::Simple scheduler, which is included in the GNU Radio 4.0 core. By default it is a single-threaded scheduler, meaning that it runs all of the blocks in the same thread (except for dedicated IO threads). It can also be instantiated as a multi-threaded scheduler that uses a one-thread-per-block approach similar to GNU Radio 3.10.

After the scheduler is created, its runAndWait() method is called. This is equivalent to the GNU Radio 3.10 run() method. It runs the flowgraph until the flowgraph finishes on its own (because the end of an input file has been reached, the limit on a Head block has been hit, etc.) The runAndWait() method also returns if there is an error, such as a block raising an unhandled exception. The method returns a std::expected, so the has_value() method can be used to check if there was an error.

Block parameters

To show how block parameters can be passed when calling emplaceBlock(), we will add a Head block between the Null Source and the Null Sink in the flowgraph above. The relevant lines of code are the following. The complete example is in head.cpp.

    auto& source = fg.emplaceBlock<gr::packet_modem::NullSource<int>>();
    auto& head =
        fg.emplaceBlock<gr::packet_modem::Head<int>>({ { "num_items", 1000UL } });
    auto& sink = fg.emplaceBlock<gr::packet_modem::NullSink<int>>();
    expect(eq(gr::ConnectionResult::SUCCESS, fg.connect<"out">(source).to<"in">(head)));
    expect(eq(gr::ConnectionResult::SUCCESS, fg.connect<"out">(head).to<"in">(sink)));

The argument of the emplaceBlock() method is a gr::property_map, which is defined as std::map<std::string, pmtv::pmt>. The pmtv library is a replacement for the Polymorphic Types (PMTs) in GNU Radio 3.10 using modern C++ features. All the simple C++ types that you would expect can be converted to and from a pmtv::pmt. When calling emplaceBlock(), it is often convenient to create the gr::property_map by using C++ initializer lists, as is done in this example. Here the initializer list is { { "num_items", 1000UL } }. Several parameters of different types can also be passed quite naturally, such as for example:

{ { "foo", "var" }, { "amplitude", 37.1f }, { "samples", std::vector<double>{ 1.3, 2.5, -4.7 } } }

Warning: This is the only way of passing parameters to a block on construction. Unlike in GNU Radio 3.10, it is not possible to have a C++ constructor that takes a list of arguments of arbitrary C++ types.

A natural question is how to find the parameters that a block has. We can check the C++ source for the block (here head.hpp). The parameters that can be set with a gr::property_map are declared in the reflection macro, which in this case is:

ENABLE_REFLECTION_FOR_TEMPLATE(gr::packet_modem::Head, in, out, num_items);

Note that the reflection macro also declares all the ports of the block. The parameters must be public class members. We can look at the class declaration to find their types. In this case we have the following:

    gr::PortIn<T> in;
    gr::PortOut<T> out;
    uint64_t num_items;

Warning: types are important when constructing the gr::property_map used to pass parameters to the block. In this case, using { { "num_items", 1000 } } would cause a runtime error, because 1000 is an int, but the block expects num_items to be a uint64_t. Beware of numeric literals in C++. Often the type needs to be specified explicitly for these.

Another thing to note from this example is that connections are always done between two blocks. Unlike in GNU Radio 3.10, there is no shorthand notation to string connections through a list of multiple blocks.