# Symbol timing recovery in Python

My objective is to demodulate QPSK signal. At the receiver I apply RRC filter and interpolate the signal to get the values of the signal (approximately) at the sampling instances. Then I am concerned with the symbol timing recovery to get the correct sampling timing. The filtered and interpolated signal is represented in the figure below.

Red vertical lines indicate the symbol intervals. Obviously, the symbol timing recovery is needed. My idea is to apply PLL in order to track the phase error to zero and then sample the corrected signal at correct time instances.

So I have the following python code:

import pdb

class SimPLL(object):
def __init__(self, lf_bandwidth):
self.phase_out = 0.0
self.freq_out = 20*pow(10, 6)  # Arbitrarily set, because I expect to get the input signal in the range of 20 MHz. Thus with this frequency I think the PLL can be locked faster
self.vco = np.exp(1j*self.phase_out)
self.phase_difference = 0.0
self.bw = lf_bandwidth
self.beta = np.sqrt(lf_bandwidth)

def update_phase_estimate(self):
self.vco = np.exp(1j*self.phase_out)

def update_phase_difference(self, in_sig):
self.phase_difference = np.angle(in_sig*np.conj(self.vco))

def step(self, in_sig):
# Takes an instantaneous sample of a signal and updates the PLL's inner state
self.update_phase_difference(in_sig)
self.freq_out += self.bw * self.phase_difference
self.phase_out += self.beta * self.phase_difference + self.freq_out
self.update_phase_estimate()

pll = SimPLL(f_symb//20) # I have read in one post, that with the bandwidth of the loop filter between f_symb/100...f_symb/20 PLL performs well
num_samples = 100
phi = 0
ref = []
out = []
diff = []
for i in range(0, num_samples - 1):
in_sig = sig_interpolated[i]
phi = np.angle(sig_interpolated[i])
pll.step(sig_interpolated[i])
ref.append(sig_interpolated[i])
out.append(pll.vco)
diff.append(pll.phase_difference)
#plt.plot(ref)
#plt.plot(ref)
#plt.plot(out)
#plt.plot(diff)

ref, = plt.plot(ref, label='ref')
out, = plt.plot(out, label='out')
diff, = plt.plot(diff, label='diff')

plt.legend(handles = [ref, out, diff], loc = 'upper right')

plt.show()


And the result I get is represented in the figure below:

So it seems like the PLL cannot lock the loop and the signal cannot be corrected.

Does anybody know, how to fix my code or can point me to the relevant example?

Thanks!

• Either you forgot to apply RRC at the transmitter, or your choice of RRC was inadequate: The drawing you show in your first figure is definitely not one of QPSK that went through a Nyquis Criterion-fulfilling filter, and RC is such a filter. You see that the signal takes more than two values at sample instants! That means you've got ISI, and that means your timing recovery will be much harder. So, fix your filtering first. May 13 at 16:04
• In addition to Marcus' comment: A PLL is commonly used for carrier recovery, not symbol synchronization. Take a look at Gardner's or Mueller's algorithms (there are several related questions on this site).
– MBaz
May 13 at 16:10
• @MarcusMüller Thank you for the answer! My filtering looks in the following way rrc_impulse_response = rrcosfilter(10, 0.22, 1/f_symb, Fs)[1], sig_filtered = np.convolve(sig, rrc_impulse_response) I know that roll off factor = 0.22 at transmitter, so I set the same one at the receiver. The length of the filter - I do not understand quite well how to select, so I try different values and in this case it was 10. So do you think there is still problem with filtering or I can already work on symbol timing recovery right now? May 13 at 16:36
• well, you can certainly work on this and consider it as something subject to an ISI channel, but as said, it just makes things harder, and less robust. May 13 at 16:56
• @Python - is the PLL coded by you or is it a reuse? Which timing error detector do you use? Which resampler do you use? May 14 at 11:55

Phase offset and timing offset are two different things with two independent tracking loops to resolve. A Phase Lock loop eliminates carrier phase offset (and a frequency offset is a carrier offset changing with time). We can have phase offset with no timing offset, and we can have timing offset with no phase offset.

Once carrier offset is removed, I recommend creating an upsampled eye diagram to evaluate the signal quality and see if further timing recovery is all that is needed. The diagrams below specific to 16 QAM illustrate the difference between carrier offset and timing offset:

Here is the eye diagram of the real portion of the 16 QAM signal, where we see the four levels and we see the signal is sampled at 4 samples per symbol. The signal quality is excellent as given by the wide eye openings and small variation at the ideal sampling locations. However given the present sampling locations there is a clear timing offset where further timing recovery is needed. There is no phase offset in this example.

For comparison, after timing recovery the eye diagram looks like the following:

If we plot only the samples at the correct sampling locations (one sample per symbol) on an an IQ diagram to get the constellation, the above case including both real and imaginary values would be at the orange dots in the following graphic:

If there was a phase error in contrast to timing offset, the entire constellation would be rotated by the phase error, demonstrating phase error with no time error:

Note that if there is phase error even with no timing error the eye diagram will appear very corrupted still (often there is a phase rotation due to frequency offset which makes the eye unrecognizable, these must be corrected first before evaluating signal fidelity with an eye diagram). Below shows the same eye as was done above for 16 QAM with no timing error, but with the phase offset as shown in the previous constellation:

A Gardner Loop is my recommended go-to for timing offset correction in that it can converge and remove timing errors even in the presence of very large carrier offsets. Then the carrier offset can be determined by simply measuring phase rotation from symbol to symbol using the correct sampling points as determined by the Gardner timing recovery loop.

More details on the Gardner Loop are provided here:

Gardner Timing Recovery for Repeated Symbols

And more details on Carrier Recovery are provided here:

High modulation index PSK - carrier recovery

• What if our symbol rate is not a multiple of sampling rate? E.g. symbol rate is 1.5 MHz while sampling rate is 10 MHz. Do we need to interpolate the signal? If yes, then is interpolator used before or after matched filers? Because I have encountered both scenarios. Thank you very much for the explanations! Things are much clearer now! May 16 at 16:25
• There is no hard requirement that the symbol and sampling rate need to be related, but the algorithms for timing recovery are simpler when they are. May 16 at 17:29