GNU Radio 3.7.2 C++ API
Digital Modulation

Introduction

This is the gr-digital package. It contains all of the digital modulation blocks, utilities, and examples. To use the digital blocks, the Python namespaces is in gnuradio.digital, which would be normally imported as:

from gnuradio import digital

See the Doxygen documentation for details about the blocks available in this package.

A quick listing of the details can be found in Python after importing by using:

help(digital)

Constellation Objects

GNU Radio supports the creation and use of Constellation objects for many of its digital communications needs. We define these constellations with a set of constellation points in complex space and the symbol mappings to those points. For a constellation that has 4 symbols, it then has log2(4) = 2 bits/symbol. We define this constellation with:

    constel_points = [c0, c1, c2, c3]
    symbols = [s0, s1, s2, s3]

In this case: $c_i \in C$ and $s_i \in [00, 01, 10, 11]$. Also, the mapping is a 1-to-1 for the items in both lists, so the symbol $s_0$ is positioned in complex space at the point $c_0$.

In the code itself, the symbols are referred to as the 'pre_diff_code' since this is the mapping before the application of differential modulation, if used.

The constellation object classes are defined in constellation.h. There is a hierarchy of classes for different purposes and which represent special classes of constellations. The all derive from the virtual class gr::digital::constellation. All constellations we will make are based on classes derived from this base:

gr::digital::constellation
    --> gr::digital::constellation_calcdist
    --> gr::digital::constellation_sector
        --> gr::digital::constellation_rect
            --> gr::digital::constellation_expl_rect
        --> gr::digital::constellation_psk
    --> gr::digital::constellation_bpsk
    --> gr::digital::constellation_qpsk
    --> gr::digital::constellation_dqpsk
    --> gr::digital::constellation_8psk

Each constellation class has a set of attributes and functions useful for manipulating the constellations and for converting symbols to and from complex points. One of the more important functions is the gr::digital::constellation::decision_maker function that takes in a sample in complex space and returns the symbol that it maps to. How this calculation is performed generally distinguishes the constellation classes from each other.

The gr::digital::constellation_calcdist is the most generic constellation class we can create. This takes in the constellation points, symbol mapping, a rotational symmetry, and the number of dimensions. The decision_maker function takes in a complex sample x and calculates the Euclidean distance between x and each point in the constellation map of the object. The constellation point that has the minimum Euclidean distance to x is selected as the best match. The decision_maker will then return the symbol value that matches to this selected constellation point.

We then have a concept of a constellation with a well-defined concept of sectors in the gr::digital::constellation_sector. This is farther refined if we know that the constellation is rectangular and can use the gr::digital::constellation_rect class. These classes have an overloaded decision_maker function that is specific to how the sectors are defined in the constructor. Essentially, the decision making math for this class is less costly than calculating the Euclidean distance for each point in the space. So if we can sectorize our constellation, using this class will be computationally cheaper.

Finally, we have a set of pre-defined, hard-coded constellations for BPSK (gr::digital::constellation_bpsk), QPSK (gr::digital::constellation_qpsk), DQPSK (gr::digital::constellation_dqpsk), and 8PSK (gr::digital::constellation_8psk). These derive directly from gr::digital::constellation and specifically overload the decision_maker function. We have very simple metrics for calculating decisions for each of these constellations. For BPSK, we simply slice on the real axis. Samples are based solely on whether the real part of the complex symbol x is greater than or less than 0. Similar, simple, decision makers are defined for the others.

Note that these specific constellations for the PSK modulations are defined for only one mapping of the constellation points to the symbols. Each is Gray coded, but for a specific Gray coding that is hard-coded into the class.

Constellation Objects in GRC

GRC provides two constellation representations that we can use to more easily define and interact with constellation objects. These are located in the 'Modulators' category as 'Constellation Object' and 'Constellation Rect. Object'. These allow us to easily specify the constellation points, symbol list, and other properties of the constellation objects. They return the base() of the object, so the variable's ID can be used directly with blocks that accept constellation objects.

These constellation blocks also allow us to specify the soft decision LUT if using the constellation object for soft decision outputs. The input can either be 'None' (default), a list of the soft bits that were generated externally or by another function, or 'auto' where the block will automatically calculate the soft decisions based on the constellation points and symbol map.

Python Constellation Helper Functions

A series of helper functions are defined in Python to create different, common constellations. There are various functions that have various levels of complexity in their definitions.

PSK Python Helpers

There are two modules imported directly into gnuradio.digital. The first is gr-digital/python/digital/psk.py and the second is gr-digital/python/digital/psk_constellations.py. The gr-digital/python/digital/psk.py module defines the following constellations:

    psk_constellation(m, mod_code, differential)

This function defines a PSK modulation of order 'm' (that is, there are m number of constellation points / symbols). The 'mod_code' is either mod_codes.GRAY_CODE or mode_codes.NO_CODE to set the symbol mapping up as either Gray coded or not. The 'differential' argument is either True to use differential coding or False for non-differential coding.

This function creates and returns a constellation object that can then be used by any block that takes a constellation (gr::digital::constellation_decoder_cb, gr::digital::constellation_receiver_cb, gr::digital::constellation_soft_decoder_cf, or gr::digital::lms_dd_equalizer_cc).

The gr-digital/python/digital/psk.py module also holds functions similar to digital.psk_constellation but that create a full modulator and demodulator chain derived from digital.generic_mod_demod.

    psk_mod(constellation_points, mod_code, differential, *args, **kwargs)
    psk_demod(constellation_points, mod_code, differential, *args, **kwargs)

The args and kwargs are parameters of the generic_mod or generic_demod passed directly to them. See The Generic Modulator/Demodulator for details of this interface.

There is another Python file full of helper functions to create different constellations. This is found in the gr-digital/python/digital/psk_constellation.py file. This file provides functions that build the vectors of constellation points and symbol mappings that can be used to create a constellation object. These are particularly helpful when using the Constellation Obj. and Constellation Rect. GUI elements in GRC.

The gr-digital/python/digital/psk_constellation.py file has extensive documentation that describes the naming scheme used for the different constellations that will not be repeated here. The main thing to understand is that these functions define constellations of the same order with different Gray code mappings. The function names are:

    (const_points, symbol_map) = psk_M_0xk_<permutation>()

Where M is the order of the modulation (2 for BPSK, 4 for QPSK, etc.), and k and <permutation> define a particular encoding for the Gray code mapping used. The documentation in the file explains how these two concepts define the Gray code mapping.

These functions are also simply named "psk_M_n" when n is an integer from 0 to N-1 for however many mappings are defined for that modulation. Not all modulations are fully defined, and the value for n has no other meaning except as a counter.

The functions return a tuple of lists. The first list in the tuple is the list of complex constellation points and the second list contains the symbols mapped to those points. These lists can then be passed to a constellation class directly to create a constellation of any Gray code mapping needed.

While not all Gray code mappings of the modulations are defined, there is a generator function to automatically build any rotation of a basis constellation:

    (const_points, symbol_map) = \
        constellation_map_generator(basis_cpoints, basis_symbols, k, pi)

We provide a basis constellation map and symbol map as the fundamental rotation of the constellation points. This function uses the k and pi inputs (see the discussion in psk_constellation.py for what these mean) to return a new rotation of the constellation's symbols. If the basis symbols are Gray coded than the output symbols will also be Gray coded. Note that this algorithm specifically depends on the constellation in complex space to be square to preserve the Gray code property.

QAM Python Helpers

Similar to defining PSK modulations, GNU Radio also has helpers for some QAM modulations, found in gr-digital/python/digital/qam.py and gr-digital/python/digital/qam_constellations.py. Similar functions to what has been described for PSK exist here:

    qam_constellation(constellation_points, differential, mod_code,
                      large_ampls_to_corners)
    qam_mod(constellation_points, differential, mod_code, *args, **kwargs)
    qam_demod(constellation_points, differential, mod_code,
              large_ampls_to_corner, *args, **kwargs)

The parameters to these functions is the same as for the PSK equivalents. The new argument 'large_ampls_to_corner' is defined in the documentation as:

    large_ampls_to_corners:  If this is set to True then when the
        constellation is making decisions, points that are far outside
        the constellation are mapped to the closest corner rather than
        the closet constellation point.  This can help with phase
        locking.

Similarly, gr-digital/python/digital/qam_constellations.py defines a of QAM constellation functions that return a tuple containing the constellation points and the symbol mappings. The naming scheme is defined in depth in the module itself and is similar to the equivalent set of PSK functions.

Currently, only a subset of 16QAM symbol mappings are defined, but we can use of the constellation_map_generator function described in the previous section to define more mapping rotations for and square QAM modulation.

The Generic Modulator/Demodulator

Hierarchical Blocks

Since digital modulation and demodulation are complex functions, the different parts can be done by different existing GNU Radio blocks. We have combined these into a generic modulator and generic demodulator hierarchical blocks to make access and use much easier. This file can be found as gr-digital/python/digital/generic_mod_demod.py.

Generic Modulator

The modulator constructor looks like:

    digital.generic_mod(constellation, differential, samples_per_symbol,
                        pre_diff_code, excess_bw, verbose, log)

The 'constellation' arg is a constellation object as defined above in Constellation Objects and can represent any constellation mapping. The 'differential' arg is a bool to turn differential coding on/off. The block also performs pulse shaping and interpolates the pulse-shaped filter to some number of 'samples_per_symbol'. The pulse shaping is a root raised cosine filter defined by the excess bandwidth (or alpha) parameter called 'excess_bw.'

We can also turn on a verbose mode to output information to the user. The 'log' parameter toggles logging data on/off. When logging is turned on, it stores every stage of the modulation to a different file so that each stage can be independently analyzed.

Generic Demodulator

The demodulator looks like:

    digital.generic_demod(constellation, differential, samples_per_symbol,
                          pre_diff_code, excess_bw, freq_bw, timing_bw,
                          phase_bw, verbose, log)

The additional parameters to the demodulator are the loop bandwidths for the different signal recovery loops used internally. There are separate loops for frequency acquisition, timing acquisition, and fine frequency / phase acquisition, controlled in tern by each of the three 'X_bw' arguments. Otherwise, the arguments are the same as the modulator.

Guts of the Modulator and Demodulator

The generic modulator looks like the following:

    blocks.packed_to_unpacked_bb: takes in packed bytes
    digital.map_bb: maps baseband symbols to the pre-differential encoding
    digital.diff_encoder_bb: differentially encode symbols
    digital.chunks_to_symbols_bc: convert symbols to complex samples
    filter.pfb_arb_resampler_ccf: perform upsampling to samps/symbol and pulse shape

The mapping and chunks-to-symbols stages are done using the information provided by the constellation object.

Note that the modulator takes in packed bytes, which means that all 8 bits per byte are used and unpacked into k bits per symbol.

The generic demodulator looks like the following:

    digital.fll_band_edge_cc: Performs coarse frequency correction
    digital.pfb_clock_sync_ccf: Matched filtering and timing recovery
    digital.constellation_receiver_cb: Phase tracking and decision making (hard bits)
    digital.diff_decoder_bb: Differential decoding
    digital.map_bb: Map to pre-differential symbols
    blocks.unpack_k_bits_bb: Unpack k bits/symbol to a stream of bits

This block outputs unpacked bits, so each output item represents a single bit of data. A block like 'pack_k_bits' can be used following this to convert the data back into bytes.

Support for Soft Decisions

To support soft decisions of the receivers instead of the current hard decisions, the constellation objects also accept a soft decision look-up table (LUT) or can be told to generate a LUT based on the constellation points and symbol map.

All constellation objects can accept a new LUT using the gr::digital::constellation::set_soft_dec_lut function. This function takes in a LUT, which is a vector of floating point tuples (in C++ it is just a vector<vector<float>>) and a precision value that specifies how accurate the LUT is to a given number of bits.

The constellation objects also have two functions to calculate the soft decisions from their constellation and symbol map. The gr::digital::constellation::calc_soft_dec takes a complex number (and optional noise power) and returns the soft decisions as a list of floats. This function is used internally in the gr::digital::constellation::gen_soft_dec_lut, which takes in the LUT's precision (as a number of bits) and an optional noise power estimate, if known. This function calculates the soft decisions itself. These functions are very expensive because each constellation point is taken into account during the calculation. We provide the gr::digital::constellation::set_soft_dec_lut in order to allow users to use one of the many known approximations to more quickly generate the soft decision LUT.

The gr::digital::constellation::calc_soft_dec function could be used instead of drawing directly from a LUT, which is probably only important if the noise floor or channel estimates are likely to change and we want to account for this in the decisions. The basic implementation of the soft decision calculation is the full calculation based on the distance between the sample and all points in the constellation space. If using this function for real-time decisions, a new object should inherit from the gr::digital::constellation class (or whichever child class is being used) and redefine this function with a faster approximation calculation.

Note: If no soft decision LUT is defined but gr::digital::constellation::soft_decision_maker is called then the full calculation from gr::digital::constellation::calc_soft_dec is used by default.

The LUT is a list of tuples, where each index of the list is some quantized (to some number of bits of precision) point in the constellation space. At each index, there is a tuple of k soft bit values for a constellation with k bits/symbol.

To help with this, the file gr-digital/python/digital/soft_dec_lut_gen.py can be used to create these tables. The function digital.soft_dec_table_generator(generator, precision) function generates a LUT based on some generator function and the number of bits of precision required. This file contains documentation explaining the system better. Or the digital.soft_dec_table(constel, symbols, prec, npwr=1) can be used which takes in the constellation map and symbols to do the full raw calculation of the softbits as opposed to a generator function.

To further aid the LUT creation, the digital module also defines a number of functions that can be used as soft decision generators for the soft_dec_table function. These functions are found in psk_constellations.py and qam_constellations.py. These files were already mentioned as they contain a set of functions that return tuples of constellation points and Gray-mapped symbols for different modulations. But these files contain a second set of functions prefixed by 'sd_' which are soft decision LUT generator functions Each LUT generator takes in a complex value and returns the tuple of soft decisions for that point in complex space. To aid with this, soft_dec_lut_gen.py defines a 'calc_from_table' function that takes in a complex sample, the precision of the table, and the LUT itself and returns the tuple of soft decisions in the LUT that is closest to the given symbol. Each of these functions can be found directly from the 'digital' Python module.

The LUTs are defined from min to max constellation points in both the real and imaginary axes. That means that signals coming in outside of these bounds are clipped to 1. So there is no added certainty for values beyond these bounds.

The gr::digital::constellation_soft_decoder_cf block takes in a constellation object where a soft decision LUT is defined. It takes in complex samples and produces a stream of floats of soft decisions. The soft decision outputs are not grouped together, it is just a stream of floats. So this block acts as an interpolator that takes in 1 complex sample and return k float for k bits per symbol.

Review of the Soft Decision API/Functions

Files of interest:

  • psk_constellations.py: PSK constellations and soft decision generators
  • qam_constellations.py: QAM constellations and soft decision generators
  • soft_dec_lut_gen.py: Functions to build soft decision LUTs and test them
  • test_soft_decisions.py: A script that generates a random complex sample and calculates the soft decisions using various methods. Plots the sample against the full constellation. Requires matplotlib installed.

Functions:

  • digital.sd_psk_2_*: Returns (constellation, symbol_map) lists for different rotations for BPSK.
  • digital.sd_psk_4_*: Returns (constellation, symbol_map) lists for different rotations for QPSK.
  • digital.sd_qam_16_*: Returns (constellation, symbol_map) lists for different rotations for 16QAM.
  • digital.soft_dec_table_generator: Takes in a generator function (like the digital.sd_XXX above) and creates a LUT to a specific precision.
  • digital.soft_dec_table: Takes in a constellation/symbol map and uses digital.calc_soft_dec to generate a LUT to a specific precision.
  • digital.calc_soft_dec: Takes a complex sample and calculates the soft decisions for a given constellation/symbol mapping.
  • digital.calc_soft_dec_from_table: Given a sample and a LUT, returns the soft decisions of the LUT for the nearest point to the sample.

C++ Interface: