Viterbi soft symbol decoding for 4FSK modem

By Adrian, Sun 12 August 2018, in category Amateur radio

Codec2, digital voice

Viterbi soft symbol decoding for 4FSK modem

Here I will introduce a brief description of a slightly unconventional 4FSK decoder with Viterbi soft symbol decoding for convolutional code. Stepping away for a moment from existing GNU radio 4FSK demodulators, I wanted to see whether I could create a 4FSK decoder using mostly the default GNU radio blocks and writing as few lines of code as possible, but still preserve some weak signal performance.
There are a number of GNU radio 4FSK demodulators, out of which the OP25 project stands out for its ability to demodulate DMR signals efficiently. I did not set myself a goal to try to use the same parameters as the DMR ETSI standard, instead I created my own FSK transmitter which uses a simple frequency modulator block with a sensitivity of 2 * PI / samples_per_symbol. This results in a carrier separation of Rs/2, which for a Codec2 digital voice payload of 2000 bits / second (1300 plus framing and data) would give us a 4 kHz wide signal. To improve weak signal performace, a convolutional encoder with K=7, R=1/2 was added, resulting in a 4000 bits /second or 2000 symbols / second transmission.

The 4FSK demodulator is partly based on theory and partly improvised to resemble a QPSK demodulator. The first block in the GNU radio flowgraph is a rational resampler. This block will bring down the sample rate from 1 Msps to a more manageable value of 20 ksps.
Next, a frequency lock loop was added to track the 4FSK signal and possibly correct any Doppler effects. This block is not really suited for such a signal, so it can be ignored, but it works in practice with stronger signals.
The four FSK levels are recovered through a series of parallel filters, each one being responsible for getting the samples of one of the four possible symbols. The filters need to be adjusted so that they do not overlap each other significantly, thus causing inter-symbol interference, but they are not too long either, causing symbol delay. Therefore, the transition width was chosen to be Rs * 2.
At this point, our flowgraph starts to diverge slightly from conventional wisdom. Instead of a four level slicer that maps the (multi sample) symbols into the real domain, with conventional values of (-3, -1, 1, 3), a new block was made which maps each symbol into one of the four points of a QPSK constellation in complex domain. The result is a square wave (in the best case scenario) which is phase modulated just like a quadrature phase shift keying signal before RRC filtering. Theoretically, we could proceed with demodulation from here just like in a QPSK demodulator.
Practically, I chose to use the MM clock recovery which is cheap in CPU resources and easy to configure, but has the disadvantage that it doesn't work with a square wave well (newer clock syncronization blocks might work even better but this was not tested). To mitigate this, we need to place a filter before it, which removes high frequency components and smoothes out the square wave. A simple low pass filter was chosen in this case, with a bandwidth of sample_rate / samples_per_symbol. A RRC filter might work as well. The clock synchronizer can now lock onto our symbols and output a complex value representing one of the (-3, -1, 1, 3) symbols, but in complex space instead. At this point, we should pass this on to the Viterbi decoder, however it expects (through the cc_decoder block ) soft symbols in the [-1, 1] range which are mapped as [1, 256] unsigned char values. To achieve this, we take advantage of the fact that our QPSK constellation can be decoded as two orthogonal BPSK signals, so we split the complex values into real and imaginary and then interleave the two floating point streams. Our symbols will now be located in the real space of (-1, 1) which the convolutional decoder with Viterbi algorithm accepts.

So, here is what the signal at 20 dB looks like on the FFT before decoding:

One can clearly see the four level FSK signal and discern the symbol spacing.
And below, this is what it looks like during processing, on the constellation display:

When the signal becomes weaker, noise bursts introduce inter-symbol interference, which our low-pass filter cannot eliminate:

Despite this, the convolutional encoded FEC manages to recover almost all of the information that was transmitted, and the Viterbi soft-symbol decoder plays a large part. All blocks are present by default in GNU radio, except for the 4FSK level slicer which was hand made. This makes it easy to implement and test. It is probably not the most efficient way of doing things, but it is fun to abuse the 4FSK -> QPSK transition.
The main drawback of this method though is that the four level slicer is a hard slicer. A long-ish noise burst taking most of the symbol period will cause the symbol to be mapped as one of the other points, having a larger Euclidean distance to the actual value. The low-pass filter will somewhat take care of shorter noise bursts as it can be seen in the image above, but the performance will still be affected. There is after all a trade-off when you want to write as little code as possible.

All code used for the GNU radio 4FSK modulator can be found here
The code for the 4FSK demodulator upon which this article is based can be found here.
The GNU radio flowgraphs, although written in C++, can be easily translated into Python. This article is not meant to teach you how to demodulate 4FSK signals properly, instead it points out a less common way it can be done. Thanks go to Marcus Mueller who gave me an idea how to do this in one of his posts on the GNU radio list.