Building an FM Receiver with C++ and gnuradio

The previous tutorial describes a typical workflow for using a Per Vices radio with gnuradio and provides an overview of how FM Broadcast Receivers work. Today we will write a Linux commandline program to use your Per Vices radio as a FM broadcast receiver in C++ without gnuradio-companion. Our program will resemble the program from the previous tutorial and will use the gnuradio C++ library.

In this tutorial, we will cover:

Note

You can find a copy of the code for this tutorial here.

Requirements

Hardware Requirements

Software Requirements

Make sure you have the following programs installed. If you don’t have them installed yet, stop everything now and go install them. You can find the instructions to install them here.

Note

Most of the common package managers (like apt-get, brew, pacman) have a different version of UHD. Make sure you compile the Per Vices supported version from scratch.

Setup

Let’s start-off with a small program and make sure that we can get it to compile with all the necessary libraries.

Creating our First C++ Program

We will need a folder to store our code. As convention dictates, we should create a folder called source. Inside the source directory, we create a fm_radio.cpp where we will code everything up. Here is a small program that has some UHD libraries. We can use it to make sure that our code compiles properly:

#include <iostream>
#include <uhd/utils/safe_main.hpp>
#include <uhd/utils/thread.hpp>

int UHD_SAFE_MAIN(int argc, char *argv[])
{
    uhd::set_thread_priority_safe();
    std::cout << std::endl
              << "done!"
              << std::endl;
    return EXIT_SUCCESS;
}

We have our starting point. Next let’s make sure this code can compile!

Setting up CMake and make

CMake is the canonical meta-build system for C++ development. We will use it to include all our external libraries and generate the Makefile that will compile our code. Once we have a Makefile, we can use the make command to build our dependencies.

To setup our program:

  1. First we will need a CMakeLists.txt file that will guide cmake in generating a makefile for all our dependencies. You can obtain a copy of the one used for this project here. The file can be obtained in one of two ways:

    a) Using the command line, go to the root of the project and type wget https://raw.githubusercontent.com/pervices/examples/develop/grc-fm-receiver-cpp/CMakeLists.txt, or

    b) Download the file and move it to the root of the project directory

  2. You may want to skim through the CMakeLists.txt file. Most of the contents is rather dull declarations that ensure that our C++ compilers use the correct set of libraries.

  3. Create a build directory. This is where our commandline program will exist once we make it.
  4. Run CMake .. within the build directory. You should now have a series of new files, the most important of which is the Makefile.
  5. You can now build the project by running make within the build/ directory
  6. You can now largely ignore the CMakeLists.txt file. Our libraries are linked, we have a Makefile, and cmake has served it’s purpose. Each time you make changes to the source/fm_radio.cpp file, you will need to run make clean and make from within the build directory.

Note

If you want to choose a different filename for the source/fm_radio.cpp program or the build/fm_radio executable, make the necessary changes in the CMakeLists.txt and redo steps 2 through 6.

Your project should now have a directory structure that resembles the following structure:

├── source
| └── fm_radio.cpp
├── build
| ├── fm_radio
| ├── CMakeCache.txt
| ├── CMakeFiles
| ├── Makefile
| ├── cmake_install.cmake
| └── compile_commands.json
└── CMakeLists.txt

Running our program

The fm_radio file is the command line radio utility that we will incrementally be building. You should be able to run it from the command line by either typing ./build/fm_radio from the root of your project or ./fm_radio from the build directory.

Usage

We have our libraries in the right place and our code compiling! Our program is able to print some output, so let’s make that output helpful. More specifically, let’s have a function that will remind us how to use our executable.

void print_usage(po::options_description desc)
{
    std::cout << "Usage: fm_radio [OPTIONS] ..." << std::endl
              << "Receives frequency modulated signals from the specified station." << std::endl
              << std::endl
              << desc << std::endl
              << std::endl
              << "Examples:" << std::endl
              << "    fm_radio --help" << std::endl
              << std::endl;
}

We will want to make sure we can call this function within our main program. To do that we will need to add some more code to the main function:

po::options_description desc;
if (argc <= 1) {
    print_usage(desc);
    return ~0;
}

This is how our file should look so far:

#include <boost/program_options.hpp>
#include <iostream>
#include <uhd/utils/safe_main.hpp>
#include <uhd/utils/thread.hpp>

namespace po = boost::program_options;

void print_usage(po::options_description desc)
{
    std::cout << "Usage: fm_radio [OPTIONS] ..." << std::endl
              << "Receives frequency modulated signals from the specified station." << std::endl
              << std::endl
              << desc << std::endl
              << std::endl
              << "Examples:" << std::endl
              << "    fm_radio --help" << std::endl
              << std::endl;
}


int UHD_SAFE_MAIN(int argc, char *argv[])
{
    uhd::set_thread_priority_safe();

    po::options_description desc;
    if (argc <= 1) {
        print_usage(desc);
        return ~0;
    }

    std::cout << std::endl << "done!" << std::endl;
    return EXIT_SUCCESS;
}

Program Arguments

Programs that can accept commandline arguments are more versatile, but I often forget which flags to pass. With the help of the program option tool from the boost library, let’s add program arguments and document them properly. The #include <boost/program_options.hpp> header will assist us greatly. Typing out boost::program_options every time we wish to interact with the library will become unwieldy. For convenience, we can assign it to a namespace.

namespace po = boost::program_options;

Once we collect the inputs, we will need a structure to store them. Consider using either a struct or a class. If you do choose to use a struct, make sure that you allocate and deallocate memory for it properly.

To declare a class:

class UserArgs
{
public:
    std::string program_name;
    ...
    double sample_rate;
    ...
    size_t channel;
};

Then to set the program args, we can specify them as follows:

po::options_description set_program_args(std::shared_ptr<UserArgs> user_args)
{
    po::options_description desc("Allowed options");
    desc.add_options()
    ("help",         "help message")
    ("station",      po::value<double>(&(user_args->station))->default_value(99.9),
                     "the fm station you wish to tune into")
    ...
    ("setup",        po::value<double>(&(user_args->setup_time))->default_value(1.0),
                     "seconds of setup time");
    return desc;
}

Here is a list of all the arguments we will want to accept:

Argument Type Default Value Purpose
program_name string fm_broadcast_receiver a descriptive name for the program
device_addr string ”“ the fm station you wish to tune into
ref_src string internal should be an integer between 0 and 10
sample_rate double 1e6 rate of incoming samples
output_rate double 43e3 the rate at which samples will be fed to the computer’s audio card
volume double 5.0 should be an integer between 0 and 10
setup_time double 1.0 seconds of setup time
deviation double 75e3 fm broadcast deviation
channel size_t 0 which channel to use

Just like last time, we will need to add some code to the main function to make sure that the set_program_args function is called with the correct arguments:

po::variables_map vm;
std::shared_ptr<UserArgs> user_args(new UserArgs());
po::options_description desc = set_program_args(user_args);
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);

if (vm.count("help") or argc <= 1) {
    print_usage(desc);
    return ~0;
}

Writing our program

Enough with printing text to the screen, let’s create an FM Broadcast Receiver like the tutorial promised!

Overview

The diagram above shows two flow graphs created using gnuradio. The diagram on top is a simplified version of the flow graph from the previous tutorial, while the diagram on the bottom is the flow graph for this tutorial. The only difference between them is that instead of using the FM Demod block, we will be replacing it with two other blocks that combine to fullfill the same purpose: the Quadrature Demod block and Decimating FIR Filter block. Other than that, we will be recreating the flow graph in much the same way.

Note

As we start building our flow graph, it’s a good idea to make sure that all the variables in the graph above are accounted for.

Preparing our device

Now using the program arguments, let’s get our radio into a more usable state. As always, a new task means we will use a separate function.

int setup_usrp_device(uhd::usrp::multi_usrp::sptr usrp_device, std::shared_ptr<UserArgs> user_args,
          double *actual_sample_rate, double *actual_gain, po::variables_map vm)
{
     // Instantiate Device
     if (not((user_args->ref_src.compare("internal")) == 0 or
         (user_args->ref_src.compare("external")) == 0 or
         (user_args->ref_src.compare("mimo")) == 0)) {
         std::cerr << "Invalid reference source. Reference source should be one of (internal, "
                   "external, mimo)"
                   << std::endl;
         return ~0;
     }

     std::cout << boost::format("Instantiating the usrp usrp_device device with address: %s...") %
               user_args->device_addr
               << std::endl;
     usrp_device->set_clock_source(user_args->ref_src);  // lock mboard clocks

     // Set the Sample Rate
     double desired_sample_rate = user_args->sample_rate;
     if (user_args->sample_rate <= 0.0) {
         std::cerr << boost::format("%s is not a valid sample rate.") % desired_sample_rate
                << "The sample rate needs to be a positive float"
                << "Please specify a valid sample rate." << std::endl;
         return ~0;
     }
     std::cout << boost::format("Setting RX Rate: %5.2f Msps...") % (desired_sample_rate / 1e6)
               << std::endl;
     usrp_device->set_rx_rate(desired_sample_rate, user_args->channel);
     *actual_sample_rate = usrp_device->get_rx_rate(user_args->channel);
     std::cout << boost::format("Actual  RX Rate: %f Msps...") % (*actual_sample_rate / 1e6)
               << std::endl;

     // Set the RF Gain
     double desired_gain = user_args->volume * 1e-1;
     std::cout << boost::format("Setting RX Gain: %f dB...") % desired_gain << std::endl;
     usrp_device->set_rx_gain(desired_gain, user_args->channel);
     *actual_gain = usrp_device->get_rx_gain(user_args->channel);
     std::cout << boost::format("Actual  RX Gain: %f dB...") % *actual_gain << std::endl;

     // Sleep a bit while the slave locks its time to the master
     std::this_thread::sleep_for(std::chrono::seconds(int64_t(user_args->setup_time)));

     return 0;
}

Like before, the function is useless unless it’s called.

double center_frequency = user_args->station * 1e6;
double sample_rate, gain;

uhd::usrp::multi_usrp::sptr usrp_device = uhd::usrp::multi_usrp::make(user_args->device_addr);
if (setup_usrp_device(usrp_device, user_args, &sample_rate, &gain, vm) != 0) {
    return ~0;
};

// create a receive streamer
std::vector<size_t> channel_nums;
channel_nums.push_back(0);
uhd::stream_args_t stream_args(STREAM_ARGS);
stream_args.channels = channel_nums;
uhd::rx_streamer::sptr rx_stream = usrp_device->get_rx_stream(stream_args);

gnuradio imports

Hooking up the flow graph

Now our radio is ready to program. We can start to create our flow graph.

Top Block

The first block in any gnuradio program is always the top block. In this case creating it is just a one-liner:

gr::top_block_sptr tb = gr::make_top_block(user_args->program_name);

Source Block

First we will need to create what will be the equivalent of the USRP Source block.

gr::uhd::usrp_source::sptr usrp_source;
usrp_source = gr::uhd::usrp_source::make(user_args->device_addr, stream_args);
usrp_source->set_samp_rate(sample_rate);
usrp_source->set_center_freq(center_frequency);

Resampler

To obtain more samples, we will need to decimate our samples using a Rational Resampler block.

std::vector<float> resampler_taps = design_filter(INTERPOL_FACTOR, DECIMATE_FACTOR_RR);
gr::filter::rational_resampler_base_ccf::sptr resampler =
     gr::filter::rational_resampler_base_ccf::make(INTERPOL_FACTOR, DECIMATE_FACTOR_RR,
                                                   resampler_taps);

The Rational Resampler block in gnuradio-companion comes with a default filter that is implemented using taps. Unfortunately, the C++ library does not have this luxury. Thus we will need to write our own filtering function to provide the taps:

std::vector<float> design_filter(int interpolation, int decimation, float fractional_bw = 0.4)
{
    if (fractional_bw >= 0.5 or fractional_bw <= 0) {
        std::cerr << "Invalid fractional_bandwidth, must be in (0, 0.5)";
    }

    const double halfband = 0.5;
    const double beta = 7.0;
    double rate = double(interpolation) / double(decimation);
    double trans_width, mid_transition_band;

    if (rate >= 1.0) {
        trans_width = halfband - fractional_bw;
        mid_transition_band = halfband - trans_width / 2.0;
    } else {
        trans_width = rate * (halfband - fractional_bw);
        mid_transition_band = rate * halfband - trans_width / 2.0;
    }

    std::vector<float> taps;
    taps = gr::filter::firdes::low_pass(interpolation, interpolation, mid_transition_band,
                                        trans_width, gr::filter::firdes::WIN_KAISER, beta);
    return taps;
}

Quadrature Demodulator

After we resample, our new sample rate is what we shall refer to as the channel_rate. We can use this to figure out the quadrature demodulator’s coefficient.

float channel_rate = sample_rate / DECIMATE_FACTOR_DEMOD;
float k = channel_rate / (2 * M_PI * user_args->deviation);
gr::analog::quadrature_demod_cf::sptr quad_demod = gr::analog::quadrature_demod_cf::make(k);

Fir Filter

At this point in time, our channel rate is still sample_rate / DECIMATE_FACTOR_DEMOD = 1MHz / 4 ~ 250 kHz. However, the sound card on most computers can only sample at ~ 48 kHz. This means that we will need to filter our output once more after the filtering. To to this, we can use a standard low pass filter.

std::vector<float> audio_taps;
gr::filter::fir_filter_fff::sptr fir_filter;
double transition_width = 1e3;
audio_taps = gr::filter::firdes::low_pass(gain, user_args->output_rate, 15e3, transition_width);
fir_filter = gr::filter::fir_filter_fff::make(DECIMATE_FACTOR_DEMOD, audio_taps);

Audio Sink

We will need this to be able to hear the audio output.

gr::audio::sink::sptr audio_sink = gr::audio::sink::make(user_args->output_rate);

Assembling flow graph

Now that we have all the blocks, let’s hook them together.

tb->connect(usrp_source, 0, resampler, 0);
tb->connect(resampler, 0, quad_demod, 0);
tb->connect(quad_demod, 0, fir_filter, 0);
tb->connect(fir_filter, 0, audio_sink, LEFT_CHANNEL);
tb->connect(fir_filter, 0, audio_sink, RIGHT_CHANNEL);

Polling the exit signal

Lastly, we will want to start the flow graph. The signal handler will ensure that the flow graph, i.e. our program, will keep running until you press Ctrl + C on the keyboard.

std::cout << "Starting flow graph" << std::endl;
tb->start();

if (DEBUG) {
    tb->dump();
}

std::signal(SIGINT, &sig_int_handler);
std::cout << "Press Ctrl + C to exit." << std::endl;
while (not stop_signal_called) {
    boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
}

tb->stop();

std::cout << std::endl << "done!" << std::endl;

The Final Product

Now when you run the binary using ./fm_radio from within the build folder, you should be able to hear the FM Broadcast.

Additional Resources