Source code for sk_dsp_comm.synchronization

"""
A Digital Communications Synchronization 
and PLLs Function Module

A collection of useful functions when studying PLLs
and synchronization and digital comm

Copyright (c) March 2017, Mark Wickert
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of the FreeBSD Project.
"""

import numpy as np
from logging import getLogger
log = getLogger(__name__)
import warnings


[docs] def NDA_symb_sync(z,Ns,L,BnTs,zeta=0.707,I_ord=3): """ zz,e_tau = NDA_symb_sync(z,Ns,L,BnTs,zeta=0.707,I_ord=3) z = complex baseband input signal at nominally Ns samples per symbol Ns = Nominal number of samples per symbol (Ts/T) in the symbol tracking loop, often 4 BnTs = time bandwidth product of loop bandwidth and the symbol period, thus the loop bandwidth as a fraction of the symbol rate. zeta = loop damping factor I_ord = interpolator order, 1, 2, or 3 e_tau = the timing error e(k) input to the loop filter Kp = The phase detector gain in the symbol tracking loop; for the NDA algoithm used here always 1 Mark Wickert July 2014 Motivated by code found in M. Rice, Digital Communications A Discrete-Time Approach, Prentice Hall, New Jersey, 2009. (ISBN 978-0-13-030497-1). """ # Loop filter parameters K0 = -1.0 # The modulo 1 counter counts down so a sign change in loop Kp = 1.0 K1 = 4*zeta/(zeta + 1/(4*zeta))*BnTs/Ns/Kp/K0 K2 = 4/(zeta + 1/(4*zeta))**2*(BnTs/Ns)**2/Kp/K0 zz = np.zeros(len(z),dtype=np.complex128) #zz = np.zeros(int(np.floor(len(z)/float(Ns))),dtype=np.complex128) e_tau = np.zeros(len(z)) #e_tau = np.zeros(int(np.floor(len(z)/float(Ns)))) #z_TED_buff = np.zeros(Ns) c1_buff = np.zeros(2*L+1) vi = 0 CNT_next = 0 mu_next = 0 underflow = 0 epsilon = 0 mm = 1 z = np.hstack(([0], z)) for nn in range(1,Ns*int(np.floor(len(z)/float(Ns)-(Ns-1)))): # Define variables used in linear interpolator control CNT = CNT_next mu = mu_next if underflow == 1: if I_ord == 1: # Decimated interpolator output (piecewise linear) z_interp = mu*z[nn] + (1 - mu)*z[nn-1] elif I_ord == 2: # Decimated interpolator output (piecewise parabolic) # in Farrow form with alpha = 1/2 v2 = 1/2.*np.sum(z[nn+2:nn-1-1:-1]*[1, -1, -1, 1]) v1 = 1/2.*np.sum(z[nn+2:nn-1-1:-1]*[-1, 3, -1, -1]) v0 = z[nn] z_interp = (mu*v2 + v1)*mu + v0 elif I_ord == 3: # Decimated interpolator output (piecewise cubic) # in Farrow form v3 = np.sum(z[nn+2:nn-1-1:-1]*[1/6., -1/2., 1/2., -1/6.]) v2 = np.sum(z[nn+2:nn-1-1:-1]*[0, 1/2., -1, 1/2.]) v1 = np.sum(z[nn+2:nn-1-1:-1]*[-1/6., 1, -1/2., -1/3.]) v0 = z[nn] z_interp = ((mu*v3 + v2)*mu + v1)*mu + v0 else: log.error('I_ord must 1, 2, or 3') # Form TED output that is smoothed using 2*L+1 samples # We need Ns interpolants for this TED: 0:Ns-1 c1 = 0 for kk in range(Ns): if I_ord == 1: # piecewise linear interp over Ns samples for TED z_TED_interp = mu*z[nn+kk] + (1 - mu)*z[nn-1+kk] elif I_ord == 2: # piecewise parabolic in Farrow form with alpha = 1/2 v2 = 1/2.*np.sum(z[nn+kk+2:nn+kk-1-1:-1]*[1, -1, -1, 1]) v1 = 1/2.*np.sum(z[nn+kk+2:nn+kk-1-1:-1]*[-1, 3, -1, -1]) v0 = z[nn+kk] z_TED_interp = (mu*v2 + v1)*mu + v0 elif I_ord == 3: # piecewise cubic in Farrow form v3 = np.sum(z[nn+kk+2:nn+kk-1-1:-1]*[1/6., -1/2., 1/2., -1/6.]) v2 = np.sum(z[nn+kk+2:nn+kk-1-1:-1]*[0, 1/2., -1, 1/2.]) v1 = np.sum(z[nn+kk+2:nn+kk-1-1:-1]*[-1/6., 1, -1/2., -1/3.]) v0 = z[nn+kk] z_TED_interp = ((mu*v3 + v2)*mu + v1)*mu + v0 else: log.error('Error: I_ord must 1, 2, or 3') c1 = c1 + np.abs(z_TED_interp)**2 * np.exp(-1j*2*np.pi/Ns*kk) c1 = c1/Ns # Update 2*L+1 length buffer for TED output smoothing c1_buff = np.hstack(([c1], c1_buff[:-1])) # Form the smoothed TED output epsilon = -1/(2*np.pi)*np.angle(np.sum(c1_buff)/(2*L+1)) # Save symbol spaced (decimated to symbol rate) interpolants in zz zz[mm] = z_interp e_tau[mm] = epsilon # log the error to the output vector e mm += 1 else: # Simple zezo-order hold interpolation between symbol samples # we just coast using the old value #epsilon = 0 pass vp = K1*epsilon # proportional component of loop filter vi = vi + K2*epsilon # integrator component of loop filter v = vp + vi # loop filter output W = 1/float(Ns) + v # counter control word # update registers CNT_next = CNT - W # Update counter value for next cycle if CNT_next < 0: # Test to see if underflow has occured CNT_next = 1 + CNT_next # Reduce counter value modulo-1 if underflow underflow = 1 # Set the underflow flag mu_next = CNT/W # update mu else: underflow = 0 mu_next = mu # Remove zero samples at end zz = zz[:-(len(zz)-mm+1)] # Normalize so symbol values have a unity magnitude zz /=np.std(zz) e_tau = e_tau[:-(len(e_tau)-mm+1)] return zz, e_tau
[docs] def DD_carrier_sync(z, M, BnTs, zeta=0.707, mod_type = 'MPSK', type = 0, open_loop = False): """ z_prime,a_hat,e_phi = DD_carrier_sync(z,M,BnTs,zeta=0.707,type=0) Decision directed carrier phase tracking z = complex baseband PSK signal at one sample per symbol M = The PSK modulation order, i.e., 2, 8, or 8. BnTs = time bandwidth product of loop bandwidth and the symbol period, thus the loop bandwidth as a fraction of the symbol rate. zeta = loop damping factor type = Phase error detector type: 0 <> ML, 1 <> heuristic z_prime = phase rotation output (like soft symbol values) a_hat = the hard decision symbol values landing at the constellation values e_phi = the phase error e(k) into the loop filter Ns = Nominal number of samples per symbol (Ts/T) in the carrier phase tracking loop, almost always 1 Kp = The phase detector gain in the carrier phase tracking loop; This value depends upon the algorithm type. For the ML scheme described at the end of notes Chapter 9, A = 1, K 1/sqrt(2), so Kp = sqrt(2). Mark Wickert July 2014 Updated for improved MPSK performance April 2020 Added experimental MQAM capability April 2020 Motivated by code found in M. Rice, Digital Communications A Discrete-Time Approach, Prentice Hall, New Jersey, 2009. (ISBN 978-0-13-030497-1). """ Ns = 1 z_prime = np.zeros_like(z) a_hat = np.zeros_like(z) e_phi = np.zeros(len(z)) theta_h = np.zeros(len(z)) theta_hat = 0 # Tracking loop constants Kp = 1 # What is it for the different schemes and modes? K0 = 1 K1 = 4*zeta/(zeta + 1/(4*zeta))*BnTs/Ns/Kp/K0; K2 = 4/(zeta + 1/(4*zeta))**2*(BnTs/Ns)**2/Kp/K0; # Initial condition vi = 0 # Scaling for MQAM using signal power # and known relationship for QAM. if mod_type == 'MQAM': z_scale = np.std(z) * np.sqrt(3/(2*(M-1))) z = z/z_scale for nn in range(len(z)): # Multiply by the phase estimate exp(-j*theta_hat[n]) z_prime[nn] = z[nn]*np.exp(-1j*theta_hat) if mod_type == 'MPSK': if M == 2: a_hat[nn] = np.sign(z_prime[nn].real) + 1j*0 elif M == 4: a_hat[nn] = (np.sign(z_prime[nn].real) + \ 1j*np.sign(z_prime[nn].imag))/np.sqrt(2) elif M > 4: # round to the nearest integer and fold to nonnegative # integers; detection into M-levels with thresholds at mid points. a_hat[nn] = np.mod((np.rint(np.angle(z_prime[nn])*M/2/np.pi)).astype(np.int),M) a_hat[nn] = np.exp(1j*2*np.pi*a_hat[nn]/M) else: print('M must be 2, 4, 8, etc.') elif mod_type == 'MQAM': # Scale adaptively assuming var(x_hat) is proportional to if M ==2 or M == 4 or M == 16 or M == 64 or M == 256: x_m = np.sqrt(M)-1 if M == 2: x_m = 1 # Shift to quadrant one for hard decisions a_hat_shift = (z_prime[nn] + x_m*(1+1j))/2 # Soft IQ symbol values are converted to hard symbol decisions a_hat_shiftI = np.int16(np.clip(np.rint(a_hat_shift.real),0,x_m)) a_hat_shiftQ = np.int16(np.clip(np.rint(a_hat_shift.imag),0,x_m)) # Shift back to antipodal QAM a_hat[nn] = 2*(a_hat_shiftI + 1j*a_hat_shiftQ) - x_m*(1+1j) else: print('M must be 2, 4, 16, 64, or 256'); if type == 0: # Maximum likelihood (ML) Rice e_phi[nn] = z_prime[nn].imag * a_hat[nn].real - \ z_prime[nn].real * a_hat[nn].imag elif type == 1: # Heuristic Rice e_phi[nn] = np.angle(z_prime[nn]) - np.angle(a_hat[nn]) # Wrap the phase to [-pi,pi] e_phi[nn] = np.angle(np.exp(1j*e_phi[nn])) elif type == 2: # Ouyang and Wang 2002 MQAM paper e_phi[nn] = imag(z_prime[nn]/a_hat[nn]) else: print('Type must be 0 or 1') vp = K1*e_phi[nn] # proportional component of loop filter vi = vi + K2*e_phi[nn] # integrator component of loop filter v = vp + vi # loop filter output theta_hat = np.mod(theta_hat + v,2*np.pi) theta_h[nn] = theta_hat # phase track output array if open_loop: theta_hat = 0 # for open-loop testing # Normalize MQAM outputs if mod_type == 'MQAM': z_prime *= z_scale return z_prime, a_hat, e_phi, theta_h
[docs] def time_step(z, ns, t_step, n_step): """ Create a one sample per symbol signal containing a phase rotation step Nsymb into the waveform. :param z: complex baseband signal after matched filter :param ns: number of sample per symbol :param t_step: in samples relative to Ns :param n_step: symbol sample location where the step turns on :return: the one sample per symbol signal containing the phase step Mark Wickert July 2014 """ z_step = np.hstack((z[:ns * n_step], z[(ns * n_step + t_step):], np.zeros(t_step))) return z_step
[docs] def phase_step(z, ns, p_step, n_step): """ Create a one sample per symbol signal containing a phase rotation step Nsymb into the waveform. :param z: complex baseband signal after matched filter :param ns: number of sample per symbol :param p_step: size in radians of the phase step :param n_step: symbol sample location where the step turns on :return: the one sample symbol signal containing the phase step Mark Wickert July 2014 """ nn = np.arange(0, len(z[::ns])) theta = np.zeros(len(nn)) idx = np.where(nn >= n_step) theta[idx] = p_step*np.ones(len(idx)) z_rot = z[::ns] * np.exp(1j * theta) return z_rot
[docs] def PLL1(theta,fs,loop_type,Kv,fn,zeta,non_lin): """ Baseband Analog PLL Simulation Model :param theta: input phase deviation in radians :param fs: sampling rate in sample per second or Hz :param loop_type: 1, first-order loop filter F(s)=K_LF; 2, integrator with lead compensation F(s) = (1 + s tau2)/(s tau1), i.e., a type II, or 3, lowpass with lead compensation F(s) = (1 + s tau2)/(1 + s tau1) :param Kv: VCO gain in Hz/v; note presently assume Kp = 1v/rad and K_LF = 1; the user can easily change this :param fn: Loop natural frequency (loops 2 & 3) or cutoff frquency (loop 1) :param zeta: Damping factor for loops 2 & 3 :param non_lin: 0, linear phase detector; 1, sinusoidal phase detector :return: theta_hat = Output phase estimate of the input theta in radians, ev = VCO control voltage, phi = phase error = theta - theta_hat Notes ----- Alternate input in place of natural frequency, fn, in Hz is the noise equivalent bandwidth Bn in Hz. Mark Wickert, April 2007 for ECE 5625/4625 Modified February 2008 and July 2014 for ECE 5675/4675 Python version August 2014 """ T = 1/float(fs) Kv = 2*np.pi*Kv # convert Kv in Hz/v to rad/s/v if loop_type == 1: # First-order loop parameters # Note Bn = K/4 Hz but K has units of rad/s #fn = 4*Bn/(2*pi); K = 2*np.pi*fn # loop natural frequency in rad/s elif loop_type == 2: # Second-order loop parameters #fn = 1/(2*pi) * 2*Bn/(zeta + 1/(4*zeta)); K = 4 *np.pi*zeta*fn # loop natural frequency in rad/s tau2 = zeta/(np.pi*fn) elif loop_type == 3: # Second-order loop parameters for one-pole lowpass with # phase lead correction. #fn = 1/(2*pi) * 2*Bn/(zeta + 1/(4*zeta)); K = Kv # Essentially the VCO gain sets the single-sided # hold-in range in Hz, as it is assumed that Kp = 1 # and KLF = 1. tau1 = K/((2*np.pi*fn)**2) tau2 = 2*zeta/(2*np.pi*fn)*(1 - 2*np.pi*fn/K*1/(2*zeta)) else: warnings.warn('Loop type must be 1, 2, or 3') # Initialize integration approximation filters filt_in_last = 0; filt_out_last = 0; vco_in_last = 0; vco_out = 0; vco_out_last = 0; # Initialize working and final output vectors n = np.arange(len(theta)) theta_hat = np.zeros_like(theta) ev = np.zeros_like(theta) phi = np.zeros_like(theta) # Begin the simulation loop for k in range(len(n)): phi[k] = theta[k] - vco_out if non_lin == 1: # sinusoidal phase detector pd_out = np.sin(phi[k]) else: # Linear phase detector pd_out = phi[k] # Loop gain gain_out = K/Kv*pd_out # apply VCO gain at VCO # Loop filter if loop_type == 2: filt_in = (1/tau2)*gain_out filt_out = filt_out_last + T/2*(filt_in + filt_in_last) filt_in_last = filt_in filt_out_last = filt_out filt_out = filt_out + gain_out elif loop_type == 3: filt_in = (tau2/tau1)*gain_out - (1/tau1)*filt_out_last u3 = filt_in + (1/tau2)*filt_out_last filt_out = filt_out_last + T/2*(filt_in + filt_in_last) filt_in_last = filt_in filt_out_last = filt_out else: filt_out = gain_out; # VCO vco_in = filt_out if loop_type == 3: vco_in = u3 vco_out = vco_out_last + T/2*(vco_in + vco_in_last) vco_in_last = vco_in vco_out_last = vco_out vco_out = Kv*vco_out # apply Kv # Measured loop signals ev[k] = vco_in theta_hat[k] = vco_out return theta_hat, ev, phi
[docs] def PLL_cbb(x,fs,loop_type,Kv,fn,zeta): """ Baseband Analog PLL Simulation Model :param x: input phase deviation in radians :param fs: sampling rate in sample per second or Hz :param loop_type: 1, first-order loop filter F(s)=K_LF; 2, integrator with lead compensation F(s) = (1 + s tau2)/(s tau1), i.e., a type II, or 3, lowpass with lead compensation F(s) = (1 + s tau2)/(1 + s tau1) :param Kv: VCO gain in Hz/v; note presently assume Kp = 1v/rad and K_LF = 1; the user can easily change this :param fn: Loop natural frequency (loops 2 & 3) or cutoff frequency (loop 1) :param zeta: Damping factor for loops 2 & 3 :return: theta_hat = Output phase estimate of the input theta in radians, ev = VCO control voltage, phi = phase error = theta - theta_hat Mark Wickert, April 2007 for ECE 5625/4625 Modified February 2008 and July 2014 for ECE 5675/4675 Python version August 2014 """ T = 1/float(fs) Kv = 2*np.pi*Kv # convert Kv in Hz/v to rad/s/v if loop_type == 1: # First-order loop parameters # Note Bn = K/4 Hz but K has units of rad/s #fn = 4*Bn/(2*pi); K = 2*np.pi*fn # loop natural frequency in rad/s elif loop_type == 2: # Second-order loop parameters #fn = 1/(2*pi) * 2*Bn/(zeta + 1/(4*zeta)); K = 4 *np.pi*zeta*fn # loop natural frequency in rad/s tau2 = zeta/(np.pi*fn) elif loop_type == 3: # Second-order loop parameters for one-pole lowpass with # phase lead correction. #fn = 1/(2*pi) * 2*Bn/(zeta + 1/(4*zeta)); K = Kv # Essentially the VCO gain sets the single-sided # hold-in range in Hz, as it is assumed that Kp = 1 # and KLF = 1. tau1 = K/((2*np.pi*fn)^2); tau2 = 2*zeta/(2*np.pi*fn)*(1 - 2*np.pi*fn/K*1/(2*zeta)) else: warnings.warn('Loop type must be 1, 2, or 3') # Initialize integration approximation filters filt_in_last = 0; filt_out_last = 0; vco_in_last = 0; vco_out = 0; vco_out_last = 0; vco_out_cbb = 0 # Initialize working and final output vectors n = np.arange(len(x)) theta_hat = np.zeros(len(x)) ev = np.zeros(len(x)) phi = np.zeros(len(x)) # Begin the simulation loop for k in range(len(n)): #phi[k] = theta[k] - vco_out phi[k] = np.imag(x[k] * np.conj(vco_out_cbb)) pd_out = phi[k] # Loop gain gain_out = K/Kv*pd_out # apply VCO gain at VCO # Loop filter if loop_type == 2: filt_in = (1/tau2)*gain_out filt_out = filt_out_last + T/2*(filt_in + filt_in_last) filt_in_last = filt_in filt_out_last = filt_out filt_out = filt_out + gain_out elif loop_type == 3: filt_in = (tau2/tau1)*gain_out - (1/tau1)*filt_out_last u3 = filt_in + (1/tau2)*filt_out_last filt_out = filt_out_last + T/2*(filt_in + filt_in_last) filt_in_last = filt_in filt_out_last = filt_out else: filt_out = gain_out; # VCO vco_in = filt_out if loop_type == 3: vco_in = u3 vco_out = vco_out_last + T/2*(vco_in + vco_in_last) vco_in_last = vco_in vco_out_last = vco_out vco_out = Kv*vco_out # apply Kv vco_out_cbb = np.exp(1j*vco_out) # Measured loop signals ev[k] = vco_in theta_hat[k] = vco_out return theta_hat, ev, phi