Interfacing Hardware with a C++ OOT Module
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
Luckily, the class gr::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 no solely trust on this mechanism as it seems not to be called when start () fails. One work around might be to call stop () oneself as part of the exception handling in a catch block. 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. 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, which is something nobody wants.