GR4 tutorial: blocks 101
Structure of a block
A good example to show how a GNU Radio 4.0 block is structured is the Head block. This block has an input and an output, and only lets the first num_items
input items make it through to the output. The code for the block is shown below, without the required #include
s. Typically the code for the block will be placed in a .hpp
file with include guards and at least <gnuradio-4.0/Block.hpp>
and <gnuradio-4.0/reflection.hpp>
will need to be included. Each part of the code is explained in detail below.
namespace gr::my_oot {
template <typename T>
class Head : public gr::Block<Head<T>>
{
public:
using Description = gr::Doc<R""(
@brief Head block. Only lets the first N items go through.
This block passes the first `num_items` items from the input to the
output. After `num_items` items have been produced, the block signals that it is
done.
)"">;
size_t _published = 0;
gr::PortIn<T> in;
gr::PortOut<T> out;
uint64_t num_items = 0;
void start() { _published = 0; }
gr::work::Status processBulk(const gr::ConsumableSpan auto& inSpan,
gr::PublishableSpan auto& outSpan)
{
if (_published >= num_items) {
if (!inSpan.consume(0)) {
thow gr::exception("consume failed");
}
outSpan.publish(0);
return gr::work::Status::DONE;
}
const size_t can_publish =
std::min({ num_items - _published, outSpan.size(), inSpan.size() });
std::ranges::copy_n(
inSpan.begin(), static_cast<ssize_t>(can_publish), outSpan.begin());
if (!inSpan.consume(can_publish)) {
thow gr::exception("consume failed");
}
outSpan.publish(can_publish);
_published += can_publish;
return _published == num_items ? gr::work::Status::DONE : gr::work::Status::OK;
}
};
} // namespace gr::my_oot
ENABLE_REFLECTION_FOR_TEMPLATE(gr::my_oot::Head, in, out, num_items);
A block in GNU Radio 4.0 is a class that inherits from gr::Block
. The gr::Block
class uses the curiously recurring template pattern, so it is a template that takes the derived block as a template parameter (here gr::Block<Head<T>>
). Many blocks in GNU Radio 4.0 are templates. Often, input and output data types are declared as template parameters, so that the block can be instantiated for any kind of data types. Commonly, the template parameter T
is used to declare an item data type. For instance, here we have
template <typename T>
class Head : public gr::Block<Head<T>>
The template parameter T
is the data type for the input and output ports of the Head block.
Usually, a block has some documentation that is entered with the using Description = gr::Doc<R""(
syntax, as shown above.
After this, we have the declarations of the class members. This includes:
- Any variable that is needed internally by the block (in this case,
_published
, which counts the number of items that have been written to the output so far). - Input and output ports (in this case,
in
, andout
). - Any parameter that can be set when the block is added to the flowgraph (in this case
num_items
). These parameters can be updated at runtime also, for instance by tags in the input samples.
size_t _published = 0;
gr::PortIn<T> in;
gr::PortOut<T> out;
uint64_t num_items = 0;
Warning: Note that all these members are public, even _published
, which is intended as a private variable for the block. Because of C++ class initialization technicalities, GNU Radio 4.0 blocks cannot have non-static private members. The coding style to distinguish members which are intended to be private is to prepend them with an underscore, as in _published
.
The block ports and the parameters which are settable when the block is added to the flowgraph are declared in a reflection macro call. Here this is
ENABLE_REFLECTION_FOR_TEMPLATE(gr::my_oot::Head, in, out, num_items);
Note that the macro call is outside of the namespace where the block is defined and it includes the full namespace path to the block.
Warning: All the block parameters that are settable when the block is added to the flowgraph should have reasonable defaults in their member declaration (here we use num_items = 0
), because the user can omit parameter values when adding the block.
Some blocks might need to override the start()
method. This method is called before the flowgraph runs. It can be used to set up any state that is needed for the block. This Head block uses the following:
void start() { _published = 0; }
The start()
method ensures that _published
is zero before the flowgraph starts running. Even though _published
is initialized to zero when the block is instantiated, setting it in start()
is a good practice, because in some cases the flowgraph might be stopped and reset to its initial state without instantiating the blocks again.
The processBulk()
method is the equivalent to GNU Radio 3.10's work function. Here is where the block reads input items and writes output items. In this block, the processBulk()
method is declared as follows:
gr::work::Status processBulk(const gr::ConsumableSpan auto& inSpan,
gr::PublishableSpan auto& outSpan)
The method always returns gr::work::Status
, but its arguments depend on the ports that have been declared in the block. An argument qualified as const gr::ConsumableSpan auto&
is used for each input port, and an argument qualified as gr::PublishableSpan auto&
is used for each output port. The arguments appear in the same order as the ports are declared (in the reflection macro call). A gr::ConsumableSpan
behaves similarly to a C++ std::span
, but it also has a method consume()
that serves to indicate how many input items the block has actually consumed. A gr::PublishableSpan
behaves similarly to a C++ std::span
, but it also has a method publish()
that serves to indicate how many input items the block has actually published (or produced, in GNU Radio 3.10's terminology).
The return value for processBulk()
is usually gr::work::Status::OK
, but some blocks can indicate that they are done by returning gr::work::Status::DONE
. This means that they will not process any more data (for instance, this Head block is done whenever it has published the required number of items, and a File Source is done whenever it has reached the end of the file). The scheduler uses this information to decide if the flowgraph execution should be finished.
The body of processBulk()
for this Head block is quite straightforward. First the block checks the special case in which it is already at or past its maximum number of output items. In this case, it notifies the scheduler that it doesn't consume or produce anything, and that it is done.
if (_published >= num_items) {
if (!inSpan.consume(0)) {
thow gr::exception("consume failed");
}
outSpan.publish(0);
return gr::work::Status::DONE;
}
In the general case, the block determines how many items can be published, copies them from the input span to the output span, informs the scheduler of how many items were consumed and published, and returns either DONE
or OK
depending on whether num_items
has been reached.
const size_t can_publish =
std::min({ num_items - _published, outSpan.size(), inSpan.size() });
std::ranges::copy_n(
inSpan.begin(), static_cast<ssize_t>(can_publish), outSpan.begin());
if (!inSpan.consume(can_publish)) {
thow gr::exception("consume failed");
}
outSpan.publish(can_publish);
_published += can_publish;
return _published == num_items ? gr::work::Status::DONE : gr::work::Status::OK;