The implementation of the Mueller and Muller clock recovery that I am seeing adds a large amount of 'noise' or degradation to the Error Vector Magnitude (EVM) to the symbols on the constellation. This is done independently in GNU Radio and a manual script in Python - giving similar results. Is this normal?
I understand that I need to adjust the parameters in this algorithm but I cant get anywhere near an optimisation that is adequate, its too noisy.
In both implementations GNU radio and python , the only impairment to the original data signal is the performance of the RRC pulse shaping filters. You can see that's its an almost perfect received constellation with an EVM < -60 dB using a filter taps of 300 and roll off factor of 0.35. See below image.
However, once I add the M&M algorithm I have no luck getting anywhere close to this and it generates a lot of noise. See below image.
It somewhat makes it difficult to assess the true extent of impairments I may later add in the model, like AWGN. Because if I add AWGN to this then the EVM or SNR just gets worse when the M&M is being used.
Here is the GNU radio flow.
EDIT Note that in my python code the Rx RRC filter is performed before the M&M, but in GNU radio I do it before an RRC because having an RRC before the MM block doesn't work give any result in the flow below.
Below is the M&M python algorithm, its from pySDR.com
samples = input_signal
interp = 16
samples_interpolated = signal.resample_poly(samples,interp,1)
mu = 2 #2 # initial estimate of phase of sample
out = np.zeros(len(samples) + 10, dtype=np.complex)
out_rail = np.zeros(len(samples) + 10, dtype=np.complex) # stores values, each iteration we need the previous 2 values plus current value
i_in = 0 # input samples index
i_out = 2 # output index (let first two outputs be 0)
while i_out < len(samples) and i_in < len(samples):
out[i_out] = samples_interpolated[i_in*interp + int(mu*interp)] # grab what we think is the "best" sample
out_rail[i_out] = int(np.real(out[i_out]) > 0) + 1j*int(np.imag(out[i_out]) > 0)
z = (out_rail[i_out] - out_rail[i_out-2]) * np.conj(out[i_out-1])
zz = (out[i_out] - out[i_out-2]) * np.conj(out_rail[i_out-1])
mm_val = np.real(zz - z)
mu += sps + 0.3*mm_val
i_in += int(np.floor(mu)) # round down to nearest int since we are using it as an index
mu = mu - np.floor(mu) # remove the integer part of mu
i_out += 1 # increment output index
out = out[2:i_out-1] # remove the first two, and anything after i_out (that was never filled out)