kvak - pi/4-DQPSK demodulator documentation
Note that I am by no means a DSP expert, the approaches used in this software could easily be misleading, suboptimal or just plain wrong.
This code also served as semestral project for my college intro to programming class.
Notice that subtracting every symbol effectively makes this into DQPSK. This will be automagically caught by the frequency recovery loop described later.
Phase and frequency recovery
The local oscillators of the receiver and the transmitter are independent. Therefore, the receiver is going to observe a phase and frequency shift from the transmitter.
A mechanism for correcting this error is required.
To figure out our phase offset, we need something to tell us how far and in which direction are we off.
Naively, we can take the angle of our input symbol and compare it with the corresponding (closest) diagonal.
This is somewhat cumbersome, as it requires calculating for each sample, which would be better to avoid.
There is a trick to make the calculation computationally less demanding:
There is a minor issue here — the value is no longer amplitude independent. Notice that the atan-based error function has equal value on every line passing through the origin. This is no longer the case for the approximation.
This means that we need to ensure our input channels have more or less comparable amplitude.
Error calculated by the error function is fed to a feedback loop, which corrects the incoming sample stream, as per the following pseudocode:
Here, is effectively a long term “average” of the input signal amplitude with the “weight” dictating how quickly are amplitude changes tracked.
This part really isn’t all that critical, as we are receiving from a bunch of stationary base stations, which means there really shouldn’t be any fast amplitude transitions.
We have another issue coming up - what do we do if the transmitter and receiver sample clocks do not match exactly? Realistically, this will almost always be the case! We could be receiving 2.01 or 1.98 samples per symbol.
As the input rate from our channelizer is nominally 2 samples per symbol, I picked the Gardner method.
(note that this is done for the I and Q components separately and the results are just summed afterwards)
means we are lagging behind and , indicates we are running ahead.
I am somewhat unsure at this point about how does this algorithm actually work — it seems “obvious” for the signal-shaped-as-triangles case, however Tetra (and most other PSK systems) shape the symbols using the raised-cosine filter, which does not have zero crossings at the midpoint between the samples.
Notice that the symbols are sampled perfectly, yet there is non-zero error!
Presumably, we could argue by symmetry and proclaim that these errors will average out over time (given slow enough feedback loop).
Timing recovery “pseudocode”
The timing recovery process is illustrated by the almost-python code below:
Output from detector is fed to a PI loop, which attempts to track the timing error.
On fractional resampling
Our timing recovery is able to tell us whether we are sampling too fast or too slow. However, we need to close the feedback loop somehow. To do this, let’s implement something called fractional resampler.
As our input, we have a series of samples, now we need to figure out what is a value of an “in-between” sample. A powerful fact at our disposal — the input samples were sampled without violating the Nyquist criterion.
Naively, this means that we can interpolate our signal by some relatively high interpolation factor and then just pick the intermediate sample that we need. This would of course be unnecessary, as we don’t really need all the in-between samples.
To interpolate a discrete-time signal, we have something called the Whittaker-Shannon interpolation formula. This is effectively convolving a time-axis shifted sinc function with our input samples in order to calculate a sample at any in-between point of the signal.
There are several technicalities to be solved now — calculating the formula from definition would demand convolution with infinite-length filter and it also requires us to compute the sinc value for any of the in-between points we obtain.
First, let’s split the intra sample space to, say, 128 discrete points and just round to the closest one when interpolating. This will effectively introduce sample clock jitter to our signal, but it’s something we can live with. Second, let’s limit the length of the filters to some reasonable amount of taps, say, 8. These parameters are used by GNURadio, so they are probably a good enough starting point.
At this point, we need to get N filters, shifting the signal in the 0-1 sample period interval. This PDF introduces several methods to get these filters — I went with the easy one — that is taking the sinc function, shifting and windowing.
And is some window function. My implementation uses the hamming window, but there is no specific reason for this choice.
There are methods which get “better” filters, using iterative methods, such as the one implemented in GNURadio. I am not sure how big of a difference would these improved filters make.
Figuring out coefficients for the PI loops
At this point, we have 2 PI loops, requiring 4 coefficients. PI loop coefficients are somewhat of a dark magic though.
Anyway, I hacked together a genetic algorithm script which attempts to explore the coefficient space maximizing the amount of valid frames received as reported by OsmoTETRA (pro tip — build tetra-rx with -O3 to get a 6x speedup!).
The demodulator exposes a capnproto-based RPC interface.
The repository includes a command line interface to interactively control the demodulators.