Runtime: Difference between revisions
(Imported from Redmine) |
mNo edit summary |
||
Line 1: | Line 1: | ||
= Developing GNU Radio Runtime = | = Developing GNU Radio Runtime = | ||
Revision as of 14:08, 28 September 2023
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
I'll focus on qa_buffer.cc since it has to do with my GSoC. 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 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 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 write_pointer() function:
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 complimentary 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 buf by simply advancing the rp.
I have added a test that shows the replacement in action:
{{collapse(Buffer Write/Read)
TBD
}}
This ignores a lot of issues such as the buffers going through the factory and the graph theory stuff but it is helpful to understand how GNU Radio buffers work at the basic level. For instance we can search for those functions in the code base to see where the buffer manipulation is happening and now that we understand what the functions are doing, it will be easier to modify.
Sequential Approach
When we create a flowgraph in GRC, 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
I think we can do -DCMAKE_BUILD_TYPE=Debug as an alternative but that requires rebuilding all of GNU Radio. We can just edit the files in runtime for a shorter build:
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 explanation):
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
Needs work...
tb = top_block() tb.start() tb.wait()
We can see that top_block start function is being executed (ignoring SWIG, switching to the C++ perspective) which can be found in the file 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). Separate class allows 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; }
We finally get to the interesting part 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