Interfacing Hardware with a C++ OOT Module

From GNU Radio
Revision as of 09:30, 27 April 2023 by JohnHawkinson (talk | contribs) (→‎Linking against Driver Library: Add lang= to <syntaxhighlight> to eliminate category error. Is that the last of them? w00t!)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

This page aims to give a rough overview about what needs to be considered developing a GNURadio OOT module, that accesses hardware as for instance a SDR.

Linking against Driver Library

The first thing you will need when writing a block, that connects your design to hardware, is getting the driver compiled and linked against your module. To do this make sure you have installed the driver library and corresponding dependencies correctly on your system e.g. by compiling a small example application executing some basic routines of the driver. What you need next is to tell cmake where to find the libraries you want to include. It is known as best practice to do this writing a find module for every external library. Modules for cmake in GNURadio OOTs are located under cmake/Modules. In this directory create a file and give it a proper name e.g. FindLimeSuite.cmake. The find module can for instance look as follows:

# - Looking for LimeSuite on the machine
# Once done this will define
#
# LIMESUITE_INCLUDE_DIRS - Where to find the header files.
# LIMESUITE_LIBRARIES - Where to find the dynamically loaded lib. files (.so).
# LIMESUITE_FOUND - True if LIMESUITE found.

find_path(
                LIMESUITE_INCLUDE_DIR
                LimeSuite.h
                PATHS /usr/include /usr/include/lime /usr/local/include/lime
                DOC "LimeSuite include file."
)

find_library(
                LIMESUITE_LIBRARY
                LimeSuite
                PATHS /usr/lib/x86_64-linux-gnu /usr/lib /usr/local/lib
                DOC "LimeSuit shared library obejct file."
)

if(LIMESUITE_INCLUDE_DIR AND LIMESUITE_LIBRARY)
  set(LIMESUITE_FOUND 1)
  set(LIMESUITE_LIBRARIES ${LIMESUITE_LIBRARY})
  set(LIMESUITE_INCLUDE_DIRS ${LIMESUITE_INCLUDE_DIR})
else()
  set(LIMESUITE_FOUND 0)
  set(LIMESUITE_LIBRARIES)
  set(LIMESUITE_INCLUDE_DIRS)
endif()

mark_as_advanced(LIMESUITE_INCLUDE_DIR)
mark_as_advanced(LIMESUITE_LIBRARY)
mark_as_advanced(LIMESUITE_FOUND)

if(NOT LIMESUITE_FOUND)
        set(LIMESUITE_DIR_MESSAGE "LimeSuite was not found. Has it been installed?")
        if(NOT LIMESUITE_FIND_QUIETLY)
                message(STATUS "${LIMESUITE_DIR_MESSAGE}")
        else()
                if(LIMESUITE_FIND_REQUIRED)
                        message(FATAL_ERROR "${LIMESUITE_DIR_MESSAGE}")
                endif()
        endif()
else()
        message(STATUS "Found LimeSuite: ${LIMESUITE_LIBRARIES} and ${LIMESUITE_INCLUDE_DIRS}.")
endif()

As one can see this module, silently assumed it is written correctly, sets three variables: LIMESUITE_INCLUDE_DIRS, LIMESUITE_LIBRARIES and LIMESUITE_FOUND. These variables are put to use later on. Our module is executed by calling it in the CMakeLists.txt, located in the root of the OOT module:

# Call the module
find_package(LimeSuite)
if(NOT LIMESUITE_FOUND)
        message(FATAL_ERROR "LimeSuite required to compile limesdr.")
endif()

The code above can be placed into the segment where cmake searches for GNURadio dependencies.

So far so good. We triggered cmake to seek the library and used LIMESUITE_FOUND to see if cmake was successful. However, we have not made use of the other two variables yet, right? LIMESUITE_LIBRARIES holds the directory where the dynamically linked library (a .so file) is installed on the system. cmake needs to know, that we want to link this library against our code. Therefore, it provides a macro, which is called in lib/CMakeLists.txt.

target_link_libraries(gnuradio-limesdr ${Boost_LIBRARIES} ${GNURADIO_ALL_LIBRARIES} ${LIMESUITE_LIBRARIES})

The final step is to take the last unused variable and communicate to cmake, that there are header files we want to have added to our project.

########################################################################
# Setup the include and linker paths
########################################################################
include_directories(
    ${CMAKE_SOURCE_DIR}/lib
    ${CMAKE_SOURCE_DIR}/include
    ${CMAKE_BINARY_DIR}/lib
    ${CMAKE_BINARY_DIR}/include
    ${Boost_INCLUDE_DIRS}
    ${CPPUNIT_INCLUDE_DIRS}
    ${GNURADIO_ALL_INCLUDE_DIRS}
    ${LIMESUITE_INCLUDE_DIRS}
)

In case you did not mess up something, you will be able to use the the driver library from now own in your code with a normal include like

#include <LimeSuite.h>

If something is wrong, you perhaps will get a quite nasty and really annoying error message similar to AttributeError: 'module' object has no attribute 'MODULE_NAME'. Usually, this can be interpreted as, that you did configure something wrong in the steps described above and you should iterate through the whole procedure again.

Fortunately, there is a way provided by a program called ldd ("print shared object dependencies", says man ldd) to find out if you linked the dynamic library properly against your library (produced and deployed when you type sudo make install):

ldd /usr/local/lib/libgnuradio-limesdr.so | grep LimeSuite

The output should be something like

libLimeSuite.so.18.02-1 => /usr/local/lib/libLimeSuite.so.18.02-1 (0x00007f5b86be8000)

and can be interpreted as a success of the previous steps. Hurray! If you can not scream the 'Hurray!' yet, since it does not work, the following might be some helpful piece code for you

########################################################################
# Debugging
########################################################################
set(PRINT_ALL_VARS 1)
if(PRINT_ALL_VARS)
        get_cmake_property(_variableNames VARIABLES)
        foreach (_variableName ${_variableNames})
                message(STATUS "${_variableName}=${${_variableName}}")
        endforeach()
endif(PRINT_ALL_VARS)

which is borrowed from here.

Using gr::block Facilities

But how is the hardware actually correctly initialized and deinitialized within GNURadio? What to do when something goes wrong? This section tries to answer these questions.

Luckily, the class gr::block, which is the base class for every block, provides functions, that can be used to initialize and deinitialize hardware. These member functions are virtual bool start () and virtual bool stop ().

start () is generally used to initialize the hardware and is not coupled with the block construction, when the hole flowgraph is instantiated, i.e. the scheduler can take up working without having to wait for the hardware. The constructor of the block in the module is called before start () is invoked. stop () facilitates hardware deinitialization, such as turning off a transceiver, destroying a stream etc. GNURadio calls stop () when something goes wrong, for instance when a exception is thrown in the work () function.

However, one can not solely trust on this mechanism as it seems not to be called when start () fails. One approach for a sane design might be to call stop () oneself as part of the exception handling in a catch block. An example implementation of start() is the following:

try
{
  LOG << "start() called." << std::endl;
  // Try to initialize HW. Throws if HW ends up in a faulty state.
  init_hw(d_settings);
}
catch(const std::exception& e)
{
  LOG << "Problem during initialization: "<< e.what();
  stop();
  // Tell GNURadio.
  throw;
}

The above example tries to initialize the HW, if it fails, the device will be deinitialized again as stop() is called.

It is in any case better to use exceptions combined with these two functions instead of relying on constructor and destructor designing blocks to interface with hardware. When a flowgraph is exited normally (e.g. using your preferred window manager), the blocks destructors are called as well as the stop () function of your hardware I/O block, which closes the connection to your device properly, given, that you implemented the stop() function correctly.

It is a well-known fact, that throwing exceptions in a destructor can be dangerous (cf. here). This is the same with the stop () function. If the exception is allowed to escape the function std::terminate() might be called acc. to standard C++, which is something nobody wants, right? So DO NOT THROW EXCEPTIONS IN stop() unless you know exactly what you are doing.

The implementation of the stop function can be much simpler as it avoids exceptions:

LOG << "stop() called.";
// Deinitialize HW.
deinit_hw();

One more thing to mention is, that having a signal source with a fixed sample rate, should embed the function set_output_multiple(...) provided by gr::block somewhere in the constructor. The argument of this function should be the size of the buffer for communication between the (hopefully) asynchronous driver and the GNURadio block. This makes sure, that the data size the work function has to output (noutput_items) is always a multiple of buffer's size and it is always OK to return less samples. Yet, it is not recommended to produce less samples than specified with noutput_items.