Runtime

From GNU Radio
Jump to navigation Jump to search

Developing GNU Radio Runtime

This page contains information on the inner workings of GNU Radio. It is an effort to increase knowledge on gnuradio-runtime so that development can be done to the core workings to allow for additional functionality. Currently there are two major parts: QA Tests and Sequential. The QA Tests section goes through the testing of a few components in runtime such as writing and reading buffers. The Sequential section attempts to go through the entire setup starting from the debug options left to us by the GNU Radio pioneers and then going from the python file to the core.

QA Test Approach

QA tests allow us to break up the massively interconnected tasks into smaller more manageable tasks. Understanding the tests can help us understand what runtime is supposed to do and thus provide us a better understanding of how to modify it. GNU Radio uses CppUnit which is invoked by CMake CTest. There are currently 4 qa files in runtime which test a number of different things:

- qa_buffer: test buffer writers and buffer readers
- qa_circular_file: tests writing/reading on a file
- qa_io_signature: test io_signature creation and item numbers
- qa_vmcircbuf: tests the factories

The file qa_runtime is used to setup the tests in the runtime directory. If more tests are needed, we need to add the files there and add tests using the others as examples.
The file qa_logger is used to make sure the output is shown to the console. Doesn't test any other functionality.

The way to test the runtime is to first go to the build directory and run
ctest -I 2,2 -V

This allows us to run the tests in the runtime directory ONLY and with verbose output. We should get a pass and not much else information.

Constructing a list of tests
Done constructing a list of tests
Checking test dependency graph...
Checking test dependency graph end
test 2
    Start 2: gr-runtime-test

2: Test command: /bin/sh "/home/muniza/gnuradio/build/gnuradio-runtime/lib/gr-runtime-test_test.sh"
2: Test timeout computed to be: 1500
2: 
2: ..............NOTICE: test from c++ NOTICE
2: DEBUG: test from c++ DEBUG
2: INFO: test from c++ INFO
2: WARN: test from c++ WARN
2: ERROR: test from c++ ERROR
2: FATAL: test from c++ FATAL
2: Testing gr::vmcircbuf_createfilemapping_factory...
2: vmcircbuf_createfilemapping: createfilemapping is not available
2: ....... gr::vmcircbuf_createfilemapping_factory: Doesn't work
2: Testing gr::vmcircbuf_sysv_shm_factory...
2: ....... gr::vmcircbuf_sysv_shm_factory: OK
2: Testing gr::vmcircbuf_mmap_shm_open_factory...
2: ....... gr::vmcircbuf_mmap_shm_open_factory: OK
2: Testing gr::vmcircbuf_mmap_tmpfile_factory...
2: ....... gr::vmcircbuf_mmap_tmpfile_factory: OK
2: ........
2: 
1/1 Test #2: gr-runtime-test ..................   Passed    3.36 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   3.90 sec

Debugging Tests Further

From what I can tell there isn't really a good way to display output since the other asserts such as CPPUNIT_ASSERT_EQUAL_MESSAGE don't work properly. What I've found that works is putting std::cout messages because when an assert fails, the program will stop therefore placing it before and after asserts can tell us where tests have failed. For instance in the header we can include

#include 

then in a function we can do

std::cout << "---- Starting the test does 0 == 1? ----" << std::endl;
CPPUNIT_ASSERT_EQUAL(0, 1);
std::cout << "---- Test 0 == 1 PASS ----" << std::endl;

Of course 0 does not equal 1 but the test will terminate before it prints out "Test 0 == 1 PASS" therefore we know exactly where our failure is.

It should be possible to modify the file

gnuradio-runtime/include/gnuradio/logger.h.in

with a function to include both assertions from CPPUNIT and output from GR_LOG. Changing this file requires a rebuilding of about 80% of GNU Radio so make sure to make -j8.

The QA Buffer Test

This file tests the functionality of the circular buffer system which is important to understand for integrating coprocessors. Currently there are four different tests:
- single writer
- single writer, single reader
- single writer, single reader, wrap-around
- single writer, N readers, randomized order and lengths

Essentially GNU Radio creates one circular buffer for each input port and one for each output port. It then uses writers and readers to manipulate the buffers. The QA test ensures that we are able to have multiple readers so that we can connect one block's output to many block's input.

Buffer Writers

To write to a buffer, first we need to create one with:

gr::buffer_sptr buf(gr::make_buffer(nitems, sizeof(), gr::block_sptr()));

This line creates a circular buffer of size bufsize = 4096/sizeof(<type>) where bufsize is the number of positions we can place values in. For instance if we have

gr::buffer_sptr buf(gr::make_buffer(nitems, sizeof(int), gr::block_sptr())); //sizeof(int) = 4

then our buffer will contain 1024 values (then we wrap around). This value can be found using the gr::buffer::space_available() function. It should be noted that space_available() returns the number of values that are unaltered so if we write 2 values and run gr::buffer::update_write_pointer(2), space_available() will give 2 less the next time we call it.

int sa = buf->space_available();

We can then can get a pointer to write data to the buffer with the gr::buffer::write_pointer() function:

Editor's note: code is missing here

The pointer points to the data in the first position of the buffer. We can write data to the buffer easily

*p = 1 //puts the value of 1 in the first slot
*(p+3) = 3 //puts the value of 3 in the fourth slot

GNU Radio likes to advance the buffer using things like *p++ = i which essentially moves the pointer position and assigns the value of i. This is very useful for loops.

We can pass the number of writes we have made to update_write_pointer() so that when we call space_available() again, we can figure out how many values are not written to. For instance if we initially have a buffer with 8 positions and we write to 2. If we run space_available() after writing, we will get 8. However if we run update_write_pointer(2) then run space_available(), we will get 6. This is useful so that we keep a white_box for circling around our circular buffer. (MAYBE OFF BY 1?)

Buffer Readers

To interface our buffer with GNU Radio, we need to create a buffer reader. This is done in a similar way to how we created buffers and wrote to them. First to create the buffer reader:

gr::buffer_reader_sptr r1(gr::buffer_add_reader(buf, 0, gr::block_sptr()));

Once we have a reader, we can use the complementary function to space_available() which is items_available() in order to see how many items we can read:

int ia = r1->items_available();

Then we can create a read pointer using the read_pointer() function:

int *rp = (int*)r1->read_pointer();

We can then read the values from our buffer by simply advancing rp.


Sequential Approach

When we create a flowgraph in GNU Radio Companion, we generate a python file. We run this python file and it does everything we need. How does it really work? Before we begin it is useful to enable all the Debug options that we can.

Full Debug Mode

We can do -DCMAKE_BUILD_TYPE=Debug but that requires rebuilding all of GNU Radio. Instead, we can just edit the indicated lines in the following files in gnuradio/gnuradio-runtime/lib/runtime:

block_executor.cc: #define ENABLE_LOGGING 1
flat_flowgraph.cc: #define FLAT_FLOWGRAPH_DEBUG 1
flowgraph.cc: #define FLOWGRAPH_DEBUG 1
hier_block2_detail.cc: #define HIER_BLOCK2_DETAIL_DEBUG 1
hier_block2.cc: #define GR_HIER_BLOCK2_DEBUG 1
single_threaded_scheduler.cc: #define ENABLE_LOGGING 1
top_block_impl.cc: #define GR_TOP_BLOCK_IMPL_DEBUG 1

The logs for block_executor can be found in the file sst-'*.log where the python file was run. All others will be displayed on the terminal.

Debug Sequence

When we run a simple signal_source -> audio_sink python file, we get the following output (line numbers added for clarity):

1  connecting: sig_source_f0:0 -> audio_alsa_sink0:0
2  ** Flattening Top Block
3  Flattening stream connections: 
4  Flattening edge sig_source_f0:0->audio_alsa_sink0:0
5  Block sig_source_f(2) is a leaf node, returning.
6  Block audio_alsa_sink(1) is a leaf node, returning.
7  sig_source_f0:0->audio_alsa_sink0:0
8  Flattening msg connections: 
9  Validating block: audio_alsa_sink(1)
10 Validating block: sig_source_f(2)
11 Creating block detail for audio_alsa_sink(1)
12 Creating block detail for sig_source_f(2)
13 Allocated buffer for output sig_source_f(2):0
14 Setting input 0 from edge sig_source_f0:0->audio_alsa_sink0:0

We can see that GNU Radio goes in a sequence:

(1)     connecting the blocks which is done by hier_block2_detail::connect
(2-4)   flattening the top block, ie putting it in a sequence, resolving edges done by hier_block2_detail::flatten_aux
(5-6)   resolving the endpoints, checking for hier blocks done by hier_block2_detail::resolve_endpoint
(7-8)   done by hier_block2_detail::flatten_aux
(9-10)  done by flowgraph::validate()
(11-13) done by flat_flowgraph::allocate_block_detail
(14)    done by flat_flowgraph::connect_block_inputs

The GNU Radio pioneers found the debug options helpful so we should find them useful as well for making modifications to runtime.

From GRC to Runtime

tb = top_block()
tb.start()
tb.wait()

gr::top_block is located in the file gnuradio/gnuradio-runtime/lib/top_block.cc:

  void
  top_block::start(int max_noutput_items)
  {
    d_impl->start(max_noutput_items);

    if(prefs::singleton()->get_bool("ControlPort", "on", false)) {
      setup_rpc();
    }
  }

This in turn calls the d_impl->start() function. d_impl is the actual implementation of top_block (top_block_impl). Separating classes allows for decoupling of changes from dependent classes. So we are calling top_block_impl::start which can be found in top_block_impl.cc.

  void
  top_block_impl::start(int max_noutput_items)
  {
    gr::thread::scoped_lock l(d_mutex);

    d_max_noutput_items = max_noutput_items;

    if(d_state != IDLE)
      throw std::runtime_error("top_block::start: top block already running or wait() not called after previous stop()");

    if(d_lock_count > 0)
      throw std::runtime_error("top_block::start: can't start with flow graph locked");

    // Create new flat flow graph by flattening hierarchy
    d_ffg = d_owner->flatten();

    // Validate new simple flow graph and wire it up
    d_ffg->validate();
    d_ffg->setup_connections();

    // Only export perf. counters if ControlPort config param is
    // enabled and if the PerfCounter option 'export' is turned on.
    prefs *p = prefs::singleton();
    if(p->get_bool("ControlPort", "on", false) && p->get_bool("PerfCounters", "export", false))
      d_ffg->enable_pc_rpc();

    d_scheduler = make_scheduler(d_ffg, d_max_noutput_items);
    d_state = RUNNING;
  }

To be continued...

We continue with the functions flatten(), setup_connections, and make_scheduler. Again, we see the d_xxxx structure used throughout which again allows decoupling of changes from dependent classes. d_owner is from top_block and top_block has a public implementation of hier_block2 so hier_block2::flaten() can be found in hier_block2.cc:

Flatten

  flat_flowgraph_sptr
  hier_block2::flatten() const
  {
    flat_flowgraph_sptr new_ffg = make_flat_flowgraph();
    d_detail->flatten_aux(new_ffg);
    return new_ffg;
  }

We can see that we are calling make_flat_flowgraph() which simply allocates memory for a boost shared ptr. d_detail derives from hier_block2_detail so again we get thrown to a new file hier_block2_detail which contains hier_block2_detail::flatten_aux:

GNU Radio Blocks

The work function actually exposes the input and output buffers.
d_input_items[i] = d->input(i)->read_pointer();
d_output_items[i] = d->output(i)->write_pointer();
int n = m->general_work(noutput_items, d_ninput_items,
d_input_items, d_output_items);

In our block we can get the virtual address of the read and write pointers with
&in[0]
&out[0]

some sample data for floats where ptr1 is &out[0] and ptr2 is &out[noutput_items]
write_ptr1: 0x7f0f99476000 | delta = 32764
write_ptr2: 0x7f0f9947e000 | -1 for the white box
write_ptr1: 0x7f0f9947e000 |
write_ptr2: 0x7f0f99485ffc |
write_ptr1: 0x7f0f99485ffc |
write_ptr2: 0x7f0f9948dffc |

write_ptr1: 0x7f0f9947dffc // this is new! delta = 65536
write_ptr2: 0x7f0f99485ff8

Appears to be a circular buffer within a circular buffer.

TODO:
- explain at a high level the graph theory that is used in gnuradio
- finish up the journey through runtime
- add a qa test for coprocessor work