├── README.md ├── calcSCurve.m ├── calcTedKp.m ├── derivativeMf.m ├── docs └── receiver_diagram_timing_sync.png ├── experiments.md ├── genTestVector.m ├── main.m ├── piLoopConstants.m ├── plotTedGain.m ├── polyDecomp.m ├── polyInterpFilt.m ├── sCurveDemo.mlx ├── simSCurve.m └── symbolTimingSync.m /README.md: -------------------------------------------------------------------------------- 1 | # Symbol Timing Recovery 2 | 3 | ## Overview 4 | This repository contains MATLAB scripts focusing on symbol timing recovery 5 | algorithms. The implementation is based on the material from the book *"Digital 6 | Communications: A Discrete-Time Approach"* by Michael Rice. If you are new to 7 | the topic, you can check out chapter 8 from this book or access the introductory 8 | tutorial in my 9 | [blog](https://igorfreire.com.br/2016/10/15/symbol-timing-synchronization-tutorial/). 10 | 11 | The main script of this repository (file `main.m`) is a simulator of symbol 12 | timing recovery applied to a pulse-shaped PAM/QAM signal under additive white 13 | Gaussian noise (AWGN). This script generates the pulse-shaped Tx sequence and 14 | feeds it into a receiver with the following blocks: 15 | 16 | ![Symbol Timing Synchronization Loop](docs/receiver_diagram_timing_sync.png) 17 | 18 | The symbol timing recovery loop is implemented by the `symbolTimingSync` 19 | function and combines the timing error detector (TED), interpolator, controller, 20 | and loop filter blocks. The adopted loop filter is a 21 | proportional-plus-integrator (PI) controller, while the interpolator controller 22 | is a modulo-1 counter. Meanwhile, you can choose the TED and interpolator 23 | implementation from various alternative methods. 24 | 25 | When running the simulation, ensure to tune the parameters of interest at the 26 | top of the `main.m` file. For instance, choose the TED scheme and the 27 | interpolation method from the supported options summarized in the table below. 28 | Alternatively, experiment with filter parameters such as the loop bandwidth and 29 | damping factor, or play with plotting and debugging options by enabling the 30 | `debug_tl_static` and `debug_tl_runtime` flags at the top of `main.m`. 31 | 32 | | Supported TEDs | Supported Interpolators | 33 | | -------------------------------|-----------------------------------| 34 | | Maximum-likelihood TED (MLTED) | Polyphase filterbank interpolator | 35 | | Early-late TED (ELTED) | Linear polynomial interpolator | 36 | | Zero-crossing TED (ZCTED) | Quadratic polynomial interpolator | 37 | | Gardner TED (GTED) | Cubic polynomial interpolator | 38 | | Mueller and Müller TED (MMTED) | | 39 | 40 | ## Code Organization 41 | 42 | | File | Description | 43 | | --------------------------- |:------------------------------------------------------| 44 | | `calcSCurve.m` | Function to compute the TED's S-curve analytically. | 45 | | `calcTedKp.m` | Function to compute the timing error detector gain. | 46 | | `derivativeMf.m` | Function to compute the derivative matched filter. | 47 | | `genTestVector.m` | Function to generate input/output test vectors. | 48 | | `main.m` | Main simulation. | 49 | | `piLoopConstants.m` | Function to compute the PI controller constants. | 50 | | `plotTedGain.m` | Function to plot the TED gain vs. the rolloff factor. | 51 | | `polyDecomp.m` | Function to decompose a filter into a polyphase bank. | 52 | | `polyInterpFilt.m` | Function to design a polyphase interpolator. | 53 | | `sCurveDemo.mlx` | A demonstration of TED S-curve and gain evaluations. | 54 | | `simSCurve.m` | Function to simulate the TED's S-curve. | 55 | | `symbolTimingSync.m` | Function implementing the symbol timing recovery loop.| 56 | 57 | ## Experiments 58 | 59 | Please refer to the [documentation page](experiments.md) covering relevant 60 | experiments with the symbol timing recovery simulator. 61 | 62 | ## Contact 63 | 64 | If you have any questions or comments, please feel free to e-mail me or open an 65 | issue. -------------------------------------------------------------------------------- /calcSCurve.m: -------------------------------------------------------------------------------- 1 | function [ normTauE, g ] = calcSCurve(TED, rollOff, rcDelay) 2 | %% Compute the S-Curve of a given TED assuming data-aided operation 3 | % 4 | % [ normTauE, g ] = calcSCurve(TED, rollOff, rcDelay) returns the S-curve 5 | % g(normTauE) of the chosen TED computed analytically based on closed-form 6 | % expressions. It also returns the vector normTauE with the normalized time 7 | % offset errors (within -0.5 to 0.5) where the S-curve is evaluated. 8 | % TED -> TED choice. 9 | % rollOff -> Rolloff factor. 10 | % rcDelay -> Raised cosine pulse delay (default: 10). 11 | % 12 | % Note: this function assumes constant channel gain (e.g., after automatic 13 | % gain control) and unitary average symbol energy. 14 | 15 | if nargin < 3 16 | rcDelay = 10; 17 | end 18 | 19 | %% Parameters and assumptions 20 | % Oversampling factor 21 | % 22 | % Note the oversampling factor that is effectively used on a symbol timing 23 | % recovery loop has nothing to do with the factor adopted here. Here, the 24 | % only goal is to observe the S curve, and the S curve (a continuous time 25 | % metric) should be independent of the oversampling factor. Hence, we use a 26 | % large value to observe the S-curve with enough resolution. 27 | L = 1e3; 28 | 29 | % Assume constant gain and unitary average symbol energy 30 | K = 1; 31 | Ex = 1; 32 | 33 | %% Raised cosine pulse 34 | 35 | % Raised cosine (Tx filter convolved with MF), equivalent to the 36 | % autocorrelation function of the pulse shaping filter 37 | r_p = rcosine(1, L, 'normal', rollOff, rcDelay); 38 | 39 | %% Derivative of the raised cosine pulse 40 | % NOTE: normally a receiver uses the dMF, which is the derivative matched 41 | % filter (dMF) computed by differentiating the MF. However, the goal here 42 | % is to compute the analytical S-curve, not to simulate an actual receiver. 43 | % This computation requires the derivative of the entire raised cosine 44 | % pulse (or pulse shape autocorrelation), as seen in Eq. (8.30). Hence, for 45 | % convenience, we compute the latter directly, and not the dMF. 46 | % 47 | % For further understanding regarding the differentiation implemented 48 | % below, refer to the implementation and comments in derivativeMf.m. 49 | h = L * [0.5 0 -0.5]; 50 | r_p_diff = conv(h, r_p); 51 | r_p_diff = r_p_diff(2:end-1); 52 | 53 | %% TED Gain 54 | 55 | % Zero-centered indices corresponding to one symbol interval 56 | tau_e = -L/2 : L/2; % timing error in units of sample periods 57 | normTauE = tau_e / L; % normalized timing error 58 | idx = L * rcDelay + 1 + fliplr(tau_e); % Central indexes 59 | % Note: reverse the order using fliplr because r_p_diff in "g" is a 60 | % function of "-tau_e". 61 | 62 | %% S-Curve 63 | switch (TED) 64 | case 'MLTED' % Maximum Likelihood - See Eq. (8.30) 65 | g = K * Ex * r_p_diff(idx); 66 | 67 | case 'ELTED' % Early-late - See Eq. (8.35) 68 | g = K * Ex * (r_p(idx + L/2) - r_p(idx - L/2)); 69 | 70 | case 'ZCTED' % Zero-crossing - See Eq. (8.42) 71 | g = K * Ex * (r_p(idx + L/2) - r_p(idx - L/2)); 72 | % NOTE: the the ZCTED and ELTED have identical S-curves 73 | 74 | case 'GTED' % Gardner - See Eq. (8.45) 75 | C = sin(pi * rollOff / 2) / (4 * pi * (1 - (rollOff^2 / 4))); 76 | g = (4 * K^2 * Ex) * C * sin(2 * pi * tau_e / L); 77 | % NOTE: 78 | % - Eq. (8.45) normalizes tau_e by Ts inside the sine argument. 79 | % Here, in contrast, we normalize it by L (oversampling factor). 80 | % - In Eq. (8.45), the scaling constant (4 * K^2 * Ex) is also 81 | % divided by Ts. Here, we don't divide it, not even by L. This 82 | % has been confirmed empirically using the "simSCurve" script. 83 | 84 | case 'MMTED' % Mueller and Müller - See Eq. (8.50) 85 | g = K * Ex * (r_p(idx + L) - r_p(idx - L)); 86 | end 87 | 88 | end -------------------------------------------------------------------------------- /calcTedKp.m: -------------------------------------------------------------------------------- 1 | function [ Kp ] = calcTedKp(TED, rollOff, method, plotSCurve, rcDelay) 2 | % Returns the gain Kp corresponding to the chosen timing error detector 3 | % (TED). The gain is computed as the slope of the TED's S-curve evaluated 4 | % for tau_e (timing error) around 0. The returned value assumes constant 5 | % channel gain (e.g., after using automatic gain control) and unitary 6 | % average symbol. When this assumption does not hold, the Kp returned by 7 | % this function must be scaled appropriately. 8 | % 9 | % [ Kp ] = calcTedKp(TED, rollOff, method, plotSCurve, rcDelay) 10 | % TED -> TED choice (MLTED, ELTED, ZCTED, GTED, or MMTED). 11 | % rollOff -> Rolloff factor. 12 | % method -> 'analytic' or 'simulated' (default: 'analytic'). 13 | % plotSCurve -> Whether to plot the S-Curve (default: false). 14 | % rcDelay -> Raised cosine pulse delay (default: 10). 15 | 16 | if nargin < 3 17 | method = 'analytic'; 18 | end 19 | 20 | if nargin < 4 21 | plotSCurve = false; 22 | end 23 | 24 | if nargin < 5 25 | rcDelay = 10; 26 | end 27 | 28 | if (strcmp(method, 'simulated')) 29 | [ normTauE, g ] = simSCurve(TED, rollOff, rcDelay); 30 | else 31 | [ normTauE, g ] = calcSCurve(TED, rollOff, rcDelay); 32 | end 33 | 34 | % Oversampling factor adopted for the evaluation. Assume the evaluation is 35 | % over "-L/2:L/2", namely over L+1 timing offset values. 36 | L = length(g) - 1; 37 | 38 | % TED Gain: slope around the origin (tau_e=0), estimated as follows: 39 | assert(normTauE(L/2 + 1) == 0) 40 | delta_y = g(L/2 + 2) - g(L/2); 41 | delta_x = normTauE(L/2 + 2) - normTauE(L/2); 42 | Kp = delta_y / delta_x; 43 | 44 | if (plotSCurve) 45 | method(1) = upper(method(1)); 46 | figure 47 | plot(normTauE, g) 48 | xlabel("Normalized timing error $(\tau_e / T_s)$", 'interpreter', 'latex') 49 | ylabel("$g(\tau_e)$", 'interpreter', 'latex') 50 | title(sprintf("%s S-Curve for the %s (rolloff=%.2f)", ... 51 | method, TED, rollOff), 'interpreter', 'latex') 52 | grid on 53 | end -------------------------------------------------------------------------------- /derivativeMf.m: -------------------------------------------------------------------------------- 1 | function [dmf] = derivativeMf(mf, L) 2 | %% Compute the derivative matched filter (dMF) 3 | % 4 | % [dmf] = derivativeMf(mf) returns the derivative matched filter (dMF) 5 | % corresponding to a given matched filter. 6 | % 7 | % Args: 8 | % mf -> Matched filter taps. 9 | % L -> Oversampling factor. 10 | 11 | % First central difference based on Eq.(3.61) and using T=1/L 12 | % 13 | % Eq. (3.61) divides by "2*T", where T is the sampling interval. Here, we 14 | % don't have "T", but we assume "T = 1/L", such that the denominator of 15 | % (3.61) becomes "(2/L)" instead. 16 | h = L * [0.5 0 -0.5]; 17 | central_diff_mf = conv(h, mf); 18 | 19 | % Skip the tail and head so that the dMF length matches the MF length 20 | dmf = central_diff_mf(2:end-1); 21 | 22 | end -------------------------------------------------------------------------------- /docs/receiver_diagram_timing_sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igorauad/symbol_timing_sync/f78a5658c9a5b4c279b47f9b8e68deff09505095/docs/receiver_diagram_timing_sync.png -------------------------------------------------------------------------------- /experiments.md: -------------------------------------------------------------------------------- 1 | # Experiments 2 | 3 | This page shall document relevant experiments using the symbol timing recovery loop simulator. The goal is to highlight some considerations in terms of parameters and the expected performance. 4 | 5 | ## Performance under Sampling Clock Frequency Offset 6 | 7 | The interpolator choice can be critical when applying the symbol timing recovery scheme under significant sampling clock frequency offset with low oversampling ratios such as $L=2$. In this scenario, due to the clock frequency offset, the fractional timing offset mu keeps increasing or decreasing, depending on the sign of the offset. Hence, $\mu$ changes rapidly and frequently wraps around from 0 to 1 or vice-versa. In the end, the interpolator often has to compute interpolants far from the basepoint index. 8 | 9 | For instance, consider a waveform sampled at a rate of 2 samples/symbol and that the fractional timing offset estimate reaches $\mu(k) = 1.0$. In this case, the interpolator would need to estimate the interpolant located after an interval of $0.5 T_s$ from the basepoint index, where $T_s$ is the symbol period, i.e., half a symbol period (or one sample period). This could be a challenging task for a linear interpolator, for example. In contrast, this problem does not arise when the oversampling ratio is high. For instance, with 8 samples/symbol, the interval resulting from $\mu(k) = 1.0$ is of only $0.125T_s$, which seems doable for a linear interpolator. 10 | 11 | To evaluate this, configure `main.m` as follows: 12 | - `L = 2` 13 | - `timeOffset = 1` 14 | - `fsOffsetPpm = 100` 15 | - `EsN0 = 50` 16 | - `intpl = 1` 17 | 18 | > Note: The high $E_s/N_0$ is useful to focus on the degradation due to the 19 | > symbol timing recovery scheme alone (instead of noise). 20 | 21 | With this configuration, you can observe that: 22 | - The performance achieved with this project's timing recovery loop is poor compared to the performance achieved by MATLAB's `comm.SymbolSynchronizer`. That is only because the latter does not offer a configurable interpolator. So, while this project's recovery loop uses a linear interpolator (`intpl = 1`), the MATLAB implementation uses its default quadratic interpolator. This performance discrepancy is already a hint about the importance of using a higher-order interpolator in this scenario. 23 | - If you enable the real-time debugging option `debug_tl_runtime = 1`, you can see the constellation periodically loses accuracy. The rationale is that the linear interpolator can only perform well when the target interpolant falls on a line between the basepoint and the succeeding samples. Thus, the interpolation becomes poor periodically as the fractional timing offset ramps up from 0 to 1. In other words, the raised cosine pulse is a "rounded" waveform and not a triangular wave, so the interpolation over long intervals is not always accurate. 24 | 25 | Next, observe what happens when a quadratic interpolator is adopted instead. Change the following interpolator parameter on `main.m`: 26 | - `intpl = 2` 27 | 28 | Now, the performance is significantly superior and similar to the performance achieved by the MATLAB implementation. After all, now both implementations are using quadratic interpolators. Nevertheless, note there is a remaining artifact of the imperfect interpolation. If you observe the real-time constellation scope (with option `debug_tl_runtime = 1`), you can see that the constellation points are periodically contracting (getting closer together) and expanding, as if they were sliding on diagonal axes around each of the four QPSK constellation points. This phenomenon is also visible in the static constellation plot obtained with `debug_tl_static = 1` on `main.m`. In contrast, with a cubic interpolator, the referred problem disappears. To verify that, set the following parameter on `main.m`: 29 | - `intpl = 3` 30 | 31 | In fact, you can observe the measured MER (printed on the console) achieved with the cubic interpolator is superior to the MER achieved with MATLAB's implementation using a quadratic interpolator. 32 | 33 | The performance is even better when using a polyphase interpolator. To verify that, set the following parameter on `main.m`: 34 | - `intpl = 0` 35 | 36 | The rationale is that the polyphase interpolator has an interpolation factor of its own. The receiver may be running with an oversampling of two (`L=2`). Meanwhile, the polyphase interpolator can still rely on 32 polyphase branches, which is equivalent to using an interpolating factor of 32. Because of this property, the polyphase interpolator achieves the best performance with the parameters in this experiment. Its measured MER is nearly 10 dB better than the one achieved with the cubic interpolator, and 20 dB better than the linear interpolator. 37 | 38 | Lastly, observe that these effects arise only because the oversampling ratio is low. For example, set the linear interpolator again but increase the oversampling ratio to $L=8$. That is, on `main.m`, set: 39 | - `L = 8` 40 | - `intpl = 1` 41 | 42 | Note the constellation is clean now, even though the interpolator is a linear one. 43 | 44 | -------------------------------------------------------------------------------- /genTestVector.m: -------------------------------------------------------------------------------- 1 | function [x, y] = genTestVector(nSymbols, sps, rolloff, rcDelay, ... 2 | tedChoice, interpChoice, loopBw, dampingFactor) 3 | % Generate input/output vectors for testing 4 | % 5 | % [x, y] = genTestVector(nSymbols, sps, rolloff, rcDelay, tedChoice, ... 6 | % interpChoice, loopBw, dampingFactor) 7 | % 8 | % Generates input and output test vectors to support the comparison of 9 | % other symbol timing recovery implementations (e.g., in RTL or C/C++) 10 | % against this project's implementation from function symbolTimingSync. The 11 | % input test vector consists of the sample-spaced matched filter output due 12 | % to a sequence of random unit-energy QPSK symbols. The output test vector 13 | % is the corresponding symbol-spaced interpolated symbol sequence output by 14 | % the symbol timing recovery loop. 15 | % 16 | % Args: 17 | % nSymbols -> Number of QPSK symbols to generate. 18 | % sps -> Oversampling ratio (samples per symbol). 19 | % rolloff -> Raised cosine pulse rolloff factor. 20 | % rcDelay -> Raised cosine pulse delay in symbols. 21 | % tedChoice -> Timing error detector to be used by symbolTimingSync. 22 | % interpChoice -> Interpolator method to be used by symbolTimingSync. 23 | % loopBw -> Normalized loop bandwidth. 24 | % dampingFactor -> Loop damping factor. 25 | % 26 | % Example with nSymbols=10, sps=2, rolloff=0.2, rcDelay=10, 27 | % tedChoice='GTED' (Gardner TED), interpChoice=1 (linear interpolator), 28 | % loopBw = 0.01, and dampingFactor=1: 29 | % 30 | % genTestVector(10, 2, 0.2, 10, 'GTED', 1, 0.01, 1); 31 | % 32 | % NOTE: The input test vector comprises the "central part" of the 33 | % convolution between the upsampled symbol sequence and the raised cosine 34 | % pulse, after the pulse shaping filter's transitory. It is the result of 35 | % calling "conv()" with "SHAPE=same". 36 | 37 | % Test Constellation 38 | M = 4; % Constellation size 39 | Ex = 1; % Average symbol energy 40 | const = qammod(0:(M-1), M); 41 | Ksym = modnorm(const, 'avpow', Ex); 42 | const = Ksym * const; 43 | 44 | % Loop constants 45 | Kp = calcTedKp(tedChoice, rolloff); 46 | K0 = -1; 47 | [ K1, K2 ] = piLoopConstants(Kp, K0, dampingFactor, loopBw, sps); 48 | 49 | % Root raised cosine filters (Tx filter and Rx matched filter) 50 | htx = rcosdesign(rolloff, rcDelay, sps); 51 | hrx = htx; 52 | 53 | % Test symbols 54 | data = randi([0 M-1], nSymbols, 1); 55 | test_syms = Ksym * qammod(data, M); 56 | 57 | % Matched filter (MF) input 58 | test_syms_up = upsample(test_syms, sps); 59 | x_mf = conv(test_syms_up, htx, 'same'); 60 | 61 | % MF output 62 | y_mf = conv(x_mf, hrx, 'same'); 63 | 64 | % Symbol timing recovery 65 | y_sync = symbolTimingSync(tedChoice, interpChoice, sps, x_mf, y_mf, ... 66 | K1, K2, const, Ksym, rolloff, rcDelay); 67 | 68 | % The input to the symbol synchronizer is the MF input if using a polyphase 69 | % interpolator. Otherwise, it is the output of a dedicated MF block. 70 | if (interpChoice == 0) 71 | printComplexVec(x_mf, "in"); 72 | else 73 | printComplexVec(y_mf, "in"); 74 | end 75 | printComplexVec(y_sync, "out"); 76 | end 77 | 78 | function [] = printComplexVec(x, label) 79 | fprintf("%s = [", label); 80 | for i = 1:(length(x) - 1) 81 | fprintf('(%f%+fj), ', real(x(i)), imag(x(i))); 82 | end 83 | i = length(x); 84 | fprintf('(%f%+fj)]\n', real(x(i)), imag(x(i))); 85 | end -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | clearvars, clc, close all 2 | 3 | %% Debug Configuration 4 | 5 | debug_tl_static = 0; % Show static debug plots after sync processing 6 | debug_tl_runtime = 0; % Open scope for debugging of sync loop iterations 7 | 8 | %% Parameters 9 | L = 32; % Oversampling factor 10 | M = 4; % Constellation order 11 | N = 2; % Dimensions per symbol (1 for PAM, 2 for QAM) 12 | nSymbols = 1e5; % Number of transmit symbols 13 | Bn_Ts = 0.01; % Loop noise bandwidth (Bn) times symbol period (Ts) 14 | eta = 1; % Loop damping factor 15 | rollOff = 0.2; % Pulse shaping roll-off factor 16 | timeOffset = 25; % Simulated channel delay in samples 17 | fsOffsetPpm = 0; % Sampling clock frequency offset in ppm 18 | rcDelay = 10; % Raised cosine (combined Tx/Rx) delay 19 | EsN0 = 20; % Target Es/N0 20 | Ex = 1; % Average symbol energy 21 | TED = 'ZCTED'; % TED (MLTED, ELTED, ZCTED, GTED, or MMTED) 22 | intpl = 2; % 0) Polyphase; 1) Linear; 2) Quadratic; 3) Cubic 23 | forceZc = 0; % Use to force zero-crossings and debug self-noise 24 | 25 | %% System Objects 26 | % Tx Filter 27 | TXFILT = comm.RaisedCosineTransmitFilter( ... 28 | 'OutputSamplesPerSymbol', L, ... 29 | 'RolloffFactor', rollOff, ... 30 | 'FilterSpanInSymbols', rcDelay); 31 | 32 | % Rx Matched Filter (MF) 33 | % 34 | % NOTE: in most simulations, the decimation factor would be L below. 35 | % However, here we want to process the filtered sequence as-is, without the 36 | % downsampling stage. The symbol timing recovery loop processes the 37 | % fractionally-spaced sequence and handles the downsampling process. 38 | RXFILT = comm.RaisedCosineReceiveFilter( ... 39 | 'InputSamplesPerSymbol', L, ... 40 | 'DecimationFactor', 1, ... 41 | 'RolloffFactor', rollOff, ... 42 | 'FilterSpanInSymbols', rcDelay); 43 | mf = RXFILT.coeffs.Numerator; % same as "rcosdesign(rollOff, rcDelay, L)" 44 | 45 | % Digital Delay 46 | DELAY = dsp.Delay(timeOffset); 47 | 48 | % Reference constellation for MER measurement 49 | if (N==2) 50 | const = qammod(0:M-1,M); 51 | else 52 | const = pammod(0:M-1,M); 53 | end 54 | Ksym = modnorm(const, 'avpow', Ex); 55 | const = Ksym * const; 56 | 57 | % MER Meter 58 | mer = comm.MER; 59 | mer.ReferenceSignalSource = 'Estimated from reference constellation'; 60 | mer.ReferenceConstellation = const; 61 | 62 | %% Loop Constants 63 | % Time-error Detector Gain (TED Gain) 64 | Kp = calcTedKp(TED, rollOff); 65 | 66 | % Scale Kp based on the average symbol energy (at the receiver) 67 | K = 1; % Assume channel gain is unitary (or that an AGC is used) 68 | Kp = K * Ex * Kp; 69 | % NOTE: if using the GTED when K is not 1, note the scaling is K^2 not K. 70 | 71 | % Counter Gain 72 | K0 = -1; 73 | % Note: this is analogous to VCO or DDS gain, but in the context of timing 74 | % recovery loop. 75 | 76 | % PI Controller Gains: 77 | [ K1, K2 ] = piLoopConstants(Kp, K0, eta, Bn_Ts, L); 78 | 79 | fprintf("Loop constants:\n"); 80 | fprintf("K1 = %g; K2 = %g; Kp = %g\n", K1, K2, Kp); 81 | 82 | % MATLAB's symbol synchronizer for comparison 83 | tedMap = containers.Map({'ELTED', 'ZCTED', 'GTED', 'MMTED'}, ... 84 | {'Early-Late (non-data-aided)', ... 85 | 'Zero-Crossing (decision-directed)', ... 86 | 'Gardner (non-data-aided)', ... 87 | 'Mueller-Muller (decision-directed)' 88 | }); 89 | if strcmp(TED, "MLTED") 90 | warning("MLTED not supported by MATLAB's synchronizer - using ZCTED"); 91 | matlabTed = "ZCTED"; 92 | else 93 | matlabTed = TED; 94 | end 95 | SYMSYNC = comm.SymbolSynchronizer(... 96 | 'TimingErrorDetector', tedMap(matlabTed), ... 97 | 'SamplesPerSymbol', L, ... 98 | 'NormalizedLoopBandwidth', Bn_Ts, ... 99 | 'DampingFactor', eta, ... 100 | 'DetectorGain', Kp); 101 | 102 | %% Random Transmit Symbols 103 | if (forceZc) 104 | % Force zero-crossings within the transmit symbols. Use to eliminate 105 | % the problem of self-noise and debug the operation of the loop 106 | data = zeros(nSymbols, 1); 107 | data(1:2:end) = M-1; 108 | else 109 | data = randi([0 M-1], nSymbols, 1); 110 | end 111 | 112 | if (N==2) 113 | modSig = Ksym * qammod(data, M); 114 | else 115 | modSig = real(Ksym * pammod(data, M)); 116 | end 117 | % Ensure the average symbol energy is unitary, otherwise the loop constants 118 | % must be altered (because Kp, the TED gain, must scale accordingly). 119 | 120 | %% Simulation: Tx -> Channel -> Rx Matched Filtering -> Symbol Synchronizer 121 | % Tx Filter 122 | txSig = step(TXFILT, modSig); 123 | 124 | % Sampling clock frequency offset 125 | % 126 | % The frequencies produced by the Tx and Rx sampling clocks are often 127 | % significantly distinct, unless both sides adopt high-accuracy oscillators 128 | % (e.g., atomic clocks) or clock disciplining mechanisms, such as with 129 | % GPSDOs. Simulate this relative frequency offset by resampling the signal. 130 | fsRatio = 1 + (fsOffsetPpm * 1e-6); % Rx/Tx clock frequency ratio 131 | tol = 1e-9; 132 | [P, Q] = rat(fsRatio, tol); % express the ratio as a fraction P/Q 133 | txResamp = resample(txSig, P, Q); 134 | 135 | % Channel 136 | delaySig = step(DELAY, txResamp); 137 | txSigPower = 1 / sqrt(L); 138 | rxSeq = awgn(delaySig, EsN0, txSigPower); 139 | 140 | % Rx matched filter (MF) 141 | mfOut = step(RXFILT, rxSeq); 142 | 143 | %% Symbol Timing Recovery 144 | % Downsampled symbols without symbol timing recovery 145 | rxNoSync = downsample(mfOut, L); 146 | 147 | % Downsampled symbols with perfect symbol timing recovery 148 | rxPerfectSync = downsample(mfOut, L, timeOffset); 149 | 150 | % Our symbol timing recovery implementation 151 | [ rxSync1 ] = symbolTimingSync(TED, intpl, L, rxSeq, mfOut, K1, K2, ... 152 | const, Ksym, rollOff, rcDelay, debug_tl_static, debug_tl_runtime); 153 | % MATLAB's implementation 154 | rxSync2 = step(SYMSYNC, mfOut); 155 | 156 | %% MER Measurement and Constellation Plots 157 | skip = 0.2 * nSymbols; % skip the initial transitory when plotting 158 | 159 | fprintf("\nMeasured MER:\n") 160 | fprintf("No Timing Correction: %.2f dB\n", mer(rxNoSync(skip:end))) 161 | fprintf("Ideal Timing Correction: %.2f dB\n", mer(rxPerfectSync(skip:end))) 162 | fprintf("Our %s Timing Recovery: %.2f dB\n", TED, mer(rxSync1(skip:end))) 163 | fprintf("MATLAB's %s Timing Recovery: %.2f dB\n", ... 164 | matlabTed, mer(rxSync2(skip:end))) 165 | 166 | if (debug_tl_static) 167 | scatterplot(rxNoSync(skip:end)) 168 | title('No Timing Correction'); 169 | 170 | scatterplot(rxPerfectSync(skip:end)) 171 | title('Ideal Timing Correction'); 172 | 173 | scatterplot(rxSync1(skip:end)) 174 | title(sprintf('Our %s Timing Recovery', TED)); 175 | 176 | scatterplot(rxSync2(skip:end)) 177 | title(sprintf('MATLAB''s %s', matlabTed)); 178 | end -------------------------------------------------------------------------------- /piLoopConstants.m: -------------------------------------------------------------------------------- 1 | function [ K1, K2 ] = piLoopConstants(Kp, K0, eta, Bn_Ts, L) 2 | % Compute the constants for the symbol timing loop PI controller 3 | % 4 | % Inputs: 5 | % Kp -> TEQ Gain. 6 | % K0 -> Counter (interpolator controller) gain. 7 | % eta -> Damping factor. 8 | % Bn_Ts -> PLL noise bandwidth normalized by the symbol rate 9 | % (multiplied by the symbol period Ts). 10 | % L -> Oversampling factor. 11 | % 12 | % Outputs: 13 | % K1 -> Proportional gain 14 | % K2 -> Integrator gain 15 | % 16 | % 17 | % Note: 18 | % Assuming L = Ts/T and that Bn*Ts is given, we can obtain Bn*T by 19 | % considering that: 20 | % 21 | % Bn * T = Bn * (Ts/L) = (Bn * Ts)/L 22 | 23 | %% Main 24 | 25 | % Convert (Bn*Ts), i.e. multiplied by symbol period, to (Bn*T), i.e. 26 | % multiplied by the sampling period. 27 | Bn_times_T = Bn_Ts / L; 28 | 29 | % Theta_n (a definition in terms of T and Bn) 30 | Theta_n = (Bn_times_T)/(eta + (1 / (4 * eta))); % See Eq. C.57 31 | 32 | % Constants obtained by analogy to the continuous time transfer function 33 | % (see Eq. C.56): 34 | Kp_K0_K1 = (4 * eta * Theta_n) / (1 + 2*eta*Theta_n + Theta_n^2); 35 | Kp_K0_K2 = (4 * Theta_n^2) / (1 + 2*eta*Theta_n + Theta_n^2); 36 | 37 | % K1 (proportional) and K2 (integrator) constants: 38 | K1 = Kp_K0_K1 / (Kp * K0); 39 | K2 = Kp_K0_K2 / (Kp * K0); 40 | 41 | end 42 | 43 | -------------------------------------------------------------------------------- /plotTedGain.m: -------------------------------------------------------------------------------- 1 | function [] = plotTedGain(TED) 2 | % [] = plotTedGain(TED) plots the TED gain (Kp) evaluated for varying 3 | % roll-off factor values. 4 | % TED -> MLTED, ELTED, ZCTED, GTED, or MMTED. 5 | 6 | rollOff = 0:0.001:1; 7 | Kp = zeros(length(rollOff),1); 8 | for i = 1:length(rollOff) 9 | Kp(i) = calcTedKp(TED, rollOff(i)); 10 | end 11 | 12 | figure 13 | plot(rollOff, Kp) 14 | grid on 15 | xlabel('Roll-off factor', 'Interpreter', 'latex') 16 | ylabel('$K_p$', 'Interpreter', 'latex') 17 | title(sprintf("%s gain vs. roll-off", TED), 'interpreter', 'latex') 18 | 19 | end -------------------------------------------------------------------------------- /polyDecomp.m: -------------------------------------------------------------------------------- 1 | function [ polyFiltBank ] = polyDecomp(filt, L) 2 | % Polyphase Decomposition 3 | % 4 | % Decompose a given FIR filter into L polyphase subfilters. For symbol 5 | % timing recovery, the given filter is expected to be an interpolating 6 | % filter, such as the one produced by function "intfilt()". Namely, "filt" 7 | % is expected to be an L-band filter with zero-crossings after every L 8 | % samples, except for the central value. This function decomposes the 9 | % L-band filter into L subfilters, and each subfilter can be used 10 | % independently to obtain a particular phase of the output sequence 11 | % according to the estimated symbol timing offset. 12 | % 13 | % Input Arguments: 14 | % filt -> Interpolating filter to be decomposed into polyphase branches. 15 | % L -> Interpolating filter's intrinsic upsampling factor. 16 | % 17 | % Output 18 | % polyFiltBank -> Polyphase interpolation filter bank 19 | 20 | % First zero-pad the FIR filter to an integer multiple of L if necessary: 21 | if (mod(length(filt), L) == 0) 22 | paddedFilt = filt; 23 | else 24 | nZerosToPad = L - mod(length(filt), L); 25 | paddedFilt = [filt zeros(1, nZerosToPad)]; 26 | end 27 | 28 | % Next, split the padded sequence into "L" branches/subfilters: 29 | lenSubfilt = length(paddedFilt) / L; 30 | polyFiltBank = reshape(paddedFilt, L, lenSubfilt); 31 | end -------------------------------------------------------------------------------- /polyInterpFilt.m: -------------------------------------------------------------------------------- 1 | function [ polyFiltBank ] = polyInterpFilt(I, P, Blim) 2 | % Polyphase Interpolator Filter Bank 3 | % 4 | % polyInterpFilt(I, P, Blim) returns a matrix containing the subfilters 5 | % (polyphase branches) of a polyphase interpolator. 6 | % 7 | % Input Arguments: 8 | % I -> Interpolation/upsampling factor. 9 | % P -> Neighbor samples weighted by the interpolation filter. 10 | % Blim -> Bandlimitedness of the interpolated sequence. 11 | % 12 | % Output" 13 | % polyFiltBank -> Polyphase interpolation filter bank. 14 | % 15 | % Example: 16 | % 17 | % Design a polyphase interpolator to upsample by a factor of two, while 18 | % weighting 2*P=4 neighbor samples for each interpolant (P on each side): 19 | % 20 | % polyFiltBank = polyInterpFilt(2, 2, 0.5) 21 | % 22 | % --------- 23 | % 24 | % Interpolation is a process that combines upsampling with a subsequent 25 | % low-pass filter to remove spectral images (anti-imaging filter). 26 | % Typically, the adopted filter is the so-called L-band filter (or L-th 27 | % Band Filter, or Nyquist Filter), namely a filter whose zero-crossings are 28 | % located at integer multiples of L (the interpolation factor). Since the 29 | % zero-interpolation applied to a given sequence (i.e., the upsampling 30 | % operation) introduces L-1 zeros in between every sample of the original 31 | % sequence, the subsequent filtering by an Lth-band filter (with 32 | % zero-crossings spaced by L) preserves the input samples in the output and 33 | % fills in the zeros based neighbor samples of the original sequence. 34 | % 35 | % Consider the following example sequence: 36 | % 37 | % x = [1 2 3], 38 | % 39 | % and an Lth-band filter for L=2: 40 | % 41 | % h = [-0.1, 0, 0.6, 1, 0.6, 0, -0.1]. 42 | % 43 | % The upsampled-by-2 sequence is: 44 | % 45 | % x_up = [ 1 0 2 0 3 0 4 0 ]. 46 | % 47 | % Now, consider the result when the peak of the flipped h (center value 48 | % equal to 1) is aligned with the fifth sample in x_up (equal to 3) through 49 | % the convolution. In this case, the inner product becomes: 50 | % 51 | % [ -0.1, 0, 0.6, 1, 0.6, 0, -0.1 ] --> (h) 52 | % .*[ 1, 0, 2, 0, 3, 0, 4, 0 ] --> (x_up) 53 | % ------------------------------------------------ 54 | % = 3 55 | % 56 | % Namely, the input sample is preserved in the output since the 57 | % zero-crossings in h coincide with the non-zero samples of x_up other than 58 | % the one of interest (the one aligned with the peak of h). 59 | % 60 | % In contrast, consider the result when the peak of h is aligned with the 61 | % fourth sample in the upsampled sequence (a zero that must be filled in): 62 | % 63 | % [ -0.1, 0, 0.6, 1, 0.6, 0, -0.1 ] --> (h) 64 | % .*[ 1, 0, 2, 0, 3, 0, 4, 0 ] --> (x_up) 65 | % ------------------------------------------------ 66 | % = (-0.1 * 1) + (0.6 * 2) + (0.6 * 3) + (-0.1 * 4) 67 | % = 2.5 68 | % 69 | % In this case, the result consists of a weighted sum based primarily on 70 | % the two adjacent neighbor non-zero samples of x_up on each side. Namely, 71 | % based on 2*P samples in total, P on each side of the target index. 72 | % 73 | % That is, the number of non-zero samples to be weighted in the 74 | % interpolation is determined by the parameter "P". From another viewpoint, 75 | % P can be used to tune the filter length, which is "2*I*P - 1". In the 76 | % given example, I=2 and P=2, so the filter length is 7. Besides, "P" 77 | % determines the filter delay. The interpolation filter has a peak at index 78 | % P*I (considering MATLAB indexing), which implies a delay of "P*I - 1". 79 | % However, when the interpolated sequence is immediately fully downsampled 80 | % by the same factor I, the delay becomes approximately P, aside from a 81 | % fractional term. Another interpretation is that delay becomes the delay 82 | % in each polyphase branch of the filter's polyphase decomposition. 83 | % 84 | % Another parameter of interest is the so-called bandlimitedness factor 85 | % ("Blim"), which specifies the bandwidth occupied by the signal to be 86 | % interpolated. More specifically, Blim indicates the signal is mostly 87 | % contained within the bandwidth from "-Blim*pi" to "Blim*pi" (in two-sided 88 | % representation). In case the signal occupies the full normalized spectrum 89 | % from -pi to pi, then Blim=1. Since the sequence to be interpolated by the 90 | % interpolator of the timing recovery scheme is actually the 91 | % fractionally-spaced sequence at the receiver, we can safely assume its 92 | % spectrum does not occupy the full bandwidth, so Blim can be less than 1 93 | % to allow for a smooth filter transition bandwidth. 94 | % 95 | % This function returns specifically the polyphase realization of the 96 | % Lth-band filter. The goal of a polyphase realization is to perform 97 | % filtering before upsampling, at a lower rate, rather than after 98 | % upsampling. The rationale is that the upsampling step I-1 zeros between 99 | % each sample of the input sequence, but the filtering of such zero values 100 | % is unnecessary and irrelevant. It is only necessary to filter the 101 | % original sequence and rearrange the outputs to produce the sample result 102 | % that would be obtained through regular upsampling plus filtering. 103 | % 104 | % A polyphase interpolator consists essentially of "I" parallel filters 105 | % (also called subfilters or polyphase branches), which operate in the same 106 | % original low-rate input sequence to be interpolated (say, at rate Fs). 107 | % The result of each subfilter can be serialized in sequence on the final 108 | % output. This serialization is carried out by a "commutator", which 109 | % operates at the higher rate (say, I*Fs) by picking the output of the 110 | % independent subfilters sequentially. In many occasions, such as silicon 111 | % implementations, it is significantly easier to implement a commutator at 112 | % rate "I*Fs" while filtering at rate "Fs", than to implement a full filter 113 | % at rate "I*Fs". Hence, the polyphase realization becomes handy. 114 | % 115 | % Nevertheless, for symbol timing recovery, the polyphase interpolation 116 | % filter is used differently. It is not used to increase the rate of an 117 | % input signal. Instead, it is used to compute the interpolated symbol 118 | % between each selection of input samples. Namely, instead of a 119 | % rate-increasing use case, the interpolator is used for rate-decreasing 120 | % (downsampling) case. In this context, there is no commutator block 121 | % following the polyphase interpolator. The polyphase interpolator takes an 122 | % oversampled sequence on its input and filters I distinct phases of this 123 | % sequence in parallel. Meanwhile, a symbol timing recovery loop selects 124 | % the output of a single polyphase subfilter at a time whenever it decides 125 | % it is time to obtain a new interpolated symbol. Each subfilter processes 126 | % a particular phase of the oversampled signal, and the timing recovery 127 | % loop can pick the appropriate phase according to its estimate of the 128 | % symbol timing offset. Besides, note the input sequence can be oversampled 129 | % by any arbitrary factor, not necessary by a factor of I. The polyphase 130 | % interpolation factor I only determines how many phases of the input 131 | % sequence are observed in parallel. 132 | 133 | % Anti-imaging interpolation filter: 134 | interpFilt = intfilt(I, P, Blim); 135 | % Polyphase decomposition 136 | polyFiltBank = polyDecomp(interpFilt, I); 137 | 138 | end 139 | 140 | -------------------------------------------------------------------------------- /sCurveDemo.mlx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igorauad/symbol_timing_sync/f78a5658c9a5b4c279b47f9b8e68deff09505095/sCurveDemo.mlx -------------------------------------------------------------------------------- /simSCurve.m: -------------------------------------------------------------------------------- 1 | function [ normTauE, g ] = simSCurve(TED, rollOff, rcDelay, nSymbols) 2 | %% Compute the S-Curve for the data-aided TED operation 3 | % 4 | % Simulates the average error detected by a TED of choice for varying 5 | % receiver timing offsets. The simulation sends a number of random 2-PAM 6 | % symbols over the cascaded combination of the Tx and matched filters. Each 7 | % transmitted symbol produces a timing error detection out of the TED, and 8 | % the average of these detected errors becomes the S-curve value for that 9 | % timing offset. This process repeats for all simulated timing offsets. 10 | % 11 | % The simulation assumes the Tx 2-PAM symbols are known to the detector 12 | % (e.g., pilot symbols), so it computes the S-curve for data-aided TED 13 | % operation, as opposed to decision-directed operation. 14 | % 15 | % [ normTauE, g ] = simSCurve(TED, rollOff, rcDelay, nSymbols) returns the 16 | % S-curve g(normTauE) of the chosen TED obtained through simulation. It 17 | % also returns the vector normTauE with the normalized time offset errors 18 | % (within -0.5 to 0.5) where the S-curve is evaluated. 19 | % TED -> TED choice. 20 | % rollOff -> Rolloff factor. 21 | % rcDelay -> Raised cosine pulse delay (default: 10). 22 | % nSymbols -> Number of random 2-PAM symbols to simulate (default: 1e4). 23 | 24 | if nargin < 3 25 | rcDelay = 10; 26 | end 27 | 28 | if nargin < 4 29 | nSymbols = 1e4; 30 | end 31 | 32 | % Oversampling (use a large value to observe the S-curve with enough 33 | % resolution). Note the oversampling factor that is effectively used on a 34 | % symbol timing recovery loop has nothing to do with the factor adopted 35 | % here. Here, the only goal is to observe the S curve, and the S curve (a 36 | % continuous time metric) should be independent of the oversampling factor. 37 | L = 1e3; 38 | 39 | % Root raised cosine Tx and Rx filters 40 | htx = rcosdesign(rollOff, rcDelay, L); 41 | mf = conj(fliplr(htx)); % Matched filter (MF) 42 | 43 | % Corresponding raised cosine pulse shape 44 | r_p = conv(htx, mf); 45 | 46 | % Derivative matched filter (dMF) 47 | dmf = derivativeMf(mf, L); 48 | 49 | % Random data (note it must be random) 50 | data = randi(2, 1, nSymbols) - 1; 51 | 52 | % 2-PAM Tx symbols 53 | txSym = real(pammod(data, 2)); 54 | 55 | % Tx upsampled sequence 56 | tau = 0; % true time delay/offset 57 | txUpSequence = upsample(txSym, L, tau); 58 | 59 | % MF output sequence 60 | mfOutput = conv(r_p, txUpSequence); 61 | 62 | % dMF output sequence 63 | dmfOutput = conv(conv(htx, txUpSequence), dmf); 64 | 65 | % Vectors with the time offset estimates (tauEst) and the corresponding 66 | % estimate errors (tauErr). Note "tauErr = tau - tauEst", as defined after 67 | % Eq. 8.25. However, "tau = 0" here, so "tauErr = -tauEst". The intuition 68 | % is as follows: suppose we are simulating the receiver signalling the 69 | % strobe L/2 sample intervals earlier (before the ideal strobe location). 70 | % In that case, the tauEst is "-L/2" (negative). However, the difference 71 | % "tau - tauEst" is "+L/2" (positive). 72 | tauEstVec = -(L/2):(L/2); % rx time delay/offset estimate 73 | tauErrVec = tau - tauEstVec; % time offset estimate error 74 | normTauE = tauErrVec / L; % normalized time offset estimate error 75 | % NOTE: our tau metric (estimate or true) is given in terms of sample 76 | % periods, not in seconds. Hence, the normalization is accordingly by "L", 77 | % in units of sample periods, not by "Ts" as used in the book. 78 | 79 | % S-Curve simulation 80 | g = zeros(1, length(tauErrVec)); 81 | for i = 1:length(tauEstVec) 82 | tauEst = tauEstVec(i); 83 | tauErr = tau - tauEst; 84 | idealStrobeIdx = (rcDelay * L) + (1:L:(nSymbols * L)); 85 | strobeIdx = idealStrobeIdx + tauEst; 86 | rxSym = mfOutput(strobeIdx); 87 | 88 | switch (TED) 89 | case 'MLTED' % Maximum Likelihood TED 90 | e = dmfOutput(strobeIdx) .* txSym; 91 | case 'ELTED' % Early-late - See Eq. (8.35) 92 | earlyIdx = strobeIdx + L/2; 93 | lateIdx = strobeIdx - L/2; 94 | e = txSym .* (mfOutput(earlyIdx) - mfOutput(lateIdx)); 95 | case 'ZCTED' % Zero-crossing TED 96 | zcStrobeIdx = strobeIdx + L/2; 97 | zcSamples = mfOutput(zcStrobeIdx); 98 | e = zcSamples(1:end-1) .* (txSym(1:end-1) - txSym(2:end)); 99 | case 'GTED' % Gardner TED 100 | zcStrobeIdx = strobeIdx(2:end) - L/2; % Zero-crossings 101 | prevStrobeIdx = strobeIdx(1:end-1); % Previous symbols 102 | currStrobeIdx = strobeIdx(2:end); % Current symbols 103 | e = mfOutput(zcStrobeIdx) .* ... 104 | (mfOutput(prevStrobeIdx) - mfOutput(currStrobeIdx)); 105 | % NOTE: the GTED is the only purely non-data-aided TED. 106 | case 'MMTED' % Mueller and Müller 107 | prevStrobeIdx = strobeIdx(1:end-1); % Previous symbols 108 | currStrobeIdx = strobeIdx(2:end); % Current symbols 109 | e = txSym(1:end-1) .* mfOutput(currStrobeIdx) - ... 110 | txSym(2:end) .* mfOutput(prevStrobeIdx); 111 | end 112 | 113 | % S-Curve value for this time offset estimation error 114 | idx = tauErrVec == tauErr; 115 | g(idx) = mean(e); 116 | end 117 | 118 | end -------------------------------------------------------------------------------- /symbolTimingSync.m: -------------------------------------------------------------------------------- 1 | function [ xI ] = symbolTimingSync(TED, intpl, L, mfIn, mfOut, K1, K2, ... 2 | const, Ksym, rollOff, rcDelay, debug_s, debug_r) 3 | % Symbol Timing Loop 4 | % --------------------- 5 | % 6 | % Implements closed-loop symbol timing recovery with a configurable timing 7 | % error detector (TED) and a configurable interpolator. The feedback 8 | % control loop uses a proportional-plus-integrator (PI) controller and a 9 | % modulo-1 counter to control the interpolator. Meanwhile, the TED can be 10 | % configured from five alternative implementations: 11 | % - Maximum-likelihood TED (MLTED); 12 | % - Early-late TED (ELTED); 13 | % - Zero-crossing TED (ZCTED); 14 | % - Gardner TED (GTED); 15 | % - Mueller-Muller TED (MMTED). 16 | % The interpolator can be chosen from four implementations: 17 | % - Polyphase; 18 | % - Linear; 19 | % - Quadratic; 20 | % - Cubic. 21 | % 22 | % When using a polyphase interpolator, the loop simultaneously synchronizes 23 | % the symbol timing and implements the matched filter (MF). In this case, 24 | % the loop processes the MF input sequence directly, and there is no need 25 | % for an external MF block. In contrast, when using any other interpolation 26 | % method (linear, quadratic, or cubic), the loop processes the MF output 27 | % and produces interpolated values based on groups of MF output samples. 28 | % For instance, the linear interpolator projects a line between a pair of 29 | % MF output samples and produces an interpolant along this line. Thus, when 30 | % using the linear, quadratic, or cubic interpolators, this symbol 31 | % synchronizer loop must be preceded by a dedicated MF block. 32 | % 33 | % In any case, to support both pre-MF and post-MF interpolation approaches, 34 | % this function takes both the MF input and MF output sequences as input 35 | % arguments (i.e., the mfIn and mfOut arguments). The mfOut argument can be 36 | % empty when using the polyphase interpolator, while the mfIn argument can 37 | % be empty when using the other interpolation methods. The only exception 38 | % is if using the MLTED. The MLTED uses the so-called derivative matched 39 | % filter (dMF), which takes the MF input sequence and produces the 40 | % differentiated MF output. Thus, when using the MLTED, even with the 41 | % linear, quadratic, or cubic interpolators, the MF input sequence must be 42 | % provided on the "mfIn" input argument. 43 | % 44 | % Note the reason why the matched filtering is executed jointly with symbol 45 | % synchronization when using the polyphase interpolator is merely because 46 | % it is possible to do so. The cascaded combination of the Tx root raised 47 | % cosine (RRC) filter and the matched RRC filter results in a raised cosine 48 | % filter, which, in turn, is an Lth-band filter (also known as Nyquist 49 | % filter) that is adequate for interpolation (see [2]). Hence, the two 50 | % tasks (interpolation and matched filtering) can be achieved in one go, 51 | % which saves the need and computational cost of an extra dedicated MF 52 | % block. If it were not for this approach, the computational cost of the 53 | % polyphase interpolator would typically be higher than the other 54 | % interpolation methods. In contrast, by using the polyphase interpolator 55 | % jointly as the MF, its computational cost becomes nearly zero, since it 56 | % only implements the indispensable MF computations. 57 | % 58 | % Furthermore, note all TED schemes except the GTED compute the symbol 59 | % timing error using symbol decisions. Hence, the reference constellation 60 | % and scaling factor must be provided through input arguments 'const' and 61 | % 'Ksym'. Such TED schemes could also leverage prior knowledge and compute 62 | % the timing error using known symbols instead of decisions. However, this 63 | % function does not offer the data-aided alternative. Instead, it only 64 | % implements the decision-directed flavor of each TED. Meanwhile, the 65 | % GTED is the only scheme purely based on the raw input samples, which is 66 | % not decision-directed nor data-aided. Hence, the 'const' and 'Ksym' 67 | % arguments are irrelevant when using the GTED. 68 | % 69 | % Input Arguments: 70 | % TED -> TED scheme ('MLTED', 'ELTED', 'ZCTED', 'GTED', or 'MMTED'). 71 | % intpl -> Interpolator: 0) Polyphase; 1) Linear; 2) Quadratic; 3) Cubic. 72 | % L -> Oversampling factor. 73 | % mfIn -> MF input sequence sampled at L samples/symbol. 74 | % mfOut -> MF output sequence sampled at L samples/symbol. 75 | % K1 -> PI controller's proportional gain. 76 | % K2 -> PI controller's integrator gain. 77 | % const -> Symbol constellation. 78 | % Ksym -> Symbol scaling factor to be undone prior to slicing. 79 | % rollOff -> Matched filter's rolloff factor. 80 | % rcDelay -> Raised cosine filter delay (double the MF RRC delay). 81 | % debug_s -> Show static debug plots after the loop processing. 82 | % debug_r -> Open scopes for real-time monitoring over the loop iterations. 83 | % 84 | % References: 85 | % [1] Michael Rice, Digital Communications - A Discrete-Time Approach. 86 | % New York: Prentice Hall, 2008. 87 | % [2] Milić, Ljiljana. Multirate Filtering for Digital Signal Processing: 88 | % MATLAB Applications. Information Science Reference, 2009. 89 | 90 | if (nargin < 12) 91 | debug_s = 0; 92 | end 93 | 94 | if (nargin < 13) 95 | debug_r = 0; 96 | end 97 | 98 | % Midpoint between consecutive symbols 99 | % 100 | % Some of the TED schemes (ELTED, ZCTED, and GTED) rely on the interpolants 101 | % located halfway between two consecutive output interpolants (or output 102 | % symbols). For instance, the ZCTED computes the timing error using the 103 | % zero-crossing value obtained from interpolation, referred to as the 104 | % "zero-crossing interpolant". When processing the k-th strobe, the loop 105 | % estimates the zero-crossing interpolant by applying the offset "mu(k)" on 106 | % the sample located at the basepoint index "m(k) - L/2". 107 | % 108 | % However, the problem is that the midpoint offset at "m(k) - L/2" only 109 | % works if L is even. If L is odd, we can take "ceil(L/2)" and compensate 110 | % for the discrepancy using the fractional timing offset mu. For instance, 111 | % for L=3, the basepoint index for the zero-crossing interpolant would be 112 | % located at "m(k) - 2", and the fractional timing offset can be adjusted 113 | % to "mu(k) + 0.5". Similarly, the ELTED's "early" value would be computed 114 | % using the sample at "m(k) + 2" as the basepoint index and "mu(k) - 0.5" 115 | % as the fractional symbol timing offset. 116 | % 117 | % Finally, note that "mu(k) +-0.5" may not fall within the [0, 1) range. 118 | % For instance, if mu(k)=0, then "mu(k) - 0.5" would be negative. In this 119 | % case, we can move the basepoint index and readjust mu(k). For example, if 120 | % "mu(k) - 0.5" is negative, we can move the the basepoint index to the 121 | % preceding sample and use "mu(k) - 0.5 + 1" instead. See the adjustment on 122 | % the "interpolate()" function. 123 | midpointOffset = ceil(L / 2); 124 | muOffset = midpointOffset - L/2; % 0.5 if L is odd, 0 if L is even 125 | 126 | % Modulation order 127 | M = numel(const); 128 | 129 | % Make sure the input vectors are column vectors 130 | if (size(mfIn, 1) == 1) 131 | mfIn = mfIn(:); 132 | end 133 | if (size(mfOut, 1) == 1) 134 | mfOut = mfOut(:); 135 | end 136 | 137 | % Create an alias for the input vector to be used. As explained above, use 138 | % the MF input with the polyphase interpolator and the MF output otherwise. 139 | if (intpl == 0) 140 | inVec = mfIn; 141 | else 142 | inVec = mfOut; 143 | end 144 | 145 | %% Optional System Objects for Step-by-step Debugging of the Loop 146 | 147 | % Constellation Diagram 148 | if (debug_r) 149 | hScope = comm.ConstellationDiagram(... 150 | 'SymbolsToDisplaySource', 'Property',... 151 | 'SamplesPerSymbol', 1, ... 152 | 'MeasurementInterval', 256, ... 153 | 'ReferenceConstellation', ... 154 | Ksym * const); 155 | hScope.XLimits = [-1.5 1.5]*max(real(const)); 156 | hScope.YLimits = [-1.5 1.5]*max(imag(const)); 157 | end 158 | 159 | % Time scope used to debug the fractional error 160 | if (debug_r) 161 | hTScopeCounter = dsp.TimeScope(... 162 | 'Title', 'Fractional Inverval', ... 163 | 'NumInputPorts', 1, ... 164 | 'ShowGrid', 1, ... 165 | 'ShowLegend', 1, ... 166 | 'BufferLength', 1e5, ... 167 | 'TimeSpanOverrunAction', 'Scroll', ... 168 | 'TimeSpan', 1e4, ... 169 | 'TimeUnits', 'None', ... 170 | 'YLimits', [-1 1]); 171 | end 172 | 173 | %% Derivative Matched Filter (dMF) - used with the MLTED only 174 | % As explained above, the polyphase interpolator implements the matched 175 | % filtering concurrently with interpolation, so there is no need for a 176 | % dedicated MF block. When using the polyphase interpolator with an MLTED, 177 | % the same applies for the dMF part. Namely, an additional polyphase filter 178 | % is adopted just for the differential matched filtering. The polyphase dMF 179 | % obtains the interpolants jointly with dMF filtering, so there is no need 180 | % to use a dedicated dMF block. In contrast, the other interpolators 181 | % require a dedicated dMF block and process the dMF output directly. For 182 | % such interpolators, implement the dedicated dMF block right here: 183 | if (intpl ~= 0 && strcmp(TED, 'MLTED')) 184 | mf = rcosdesign(rollOff, rcDelay, L); 185 | dmf = derivativeMf(mf, L); 186 | dMfOut = filter(dmf, 1, mfIn); 187 | end 188 | 189 | %% Interpolator Design 190 | 191 | % Polyphase filter bank 192 | if (intpl == 0) 193 | % Interpolation factor 194 | % 195 | % Note the polyphase filter's interpolation factor is completely 196 | % independent from the receiver's oversampling factor L. For instance, 197 | % the receiver may be running with L=2 and the polyphase filter may 198 | % still apply a significantly higher interpolation factor. Furthermore, 199 | % note that there is no performance penalty in using a high 200 | % interpolation factor, aside from using more memory to store the 201 | % polyphase filter bank. In the end, a single subfilter is used per 202 | % strobe anyway. A high interpolation factor (e.g., 128) is preferable 203 | % when the receiver oversampling is low (e.g., L=2). 204 | polyInterpFactor = 128; 205 | 206 | % Polyphase MF realization 207 | % 208 | % Note the RRC filter is not an Lth-band filter, so it is not strictly 209 | % adequate for a polyphase interpolation filter. However, its cascaded 210 | % combination with the RRC filter used on the Tx side for pulse shaping 211 | % yields a raised cosine filter, which is a proper Lth-band filter. 212 | % 213 | % The RRC filter is normally designed based on the receiver's 214 | % oversampling factor L. However, the following RRC is also an 215 | % interpolator, and the interpolator aims to "divide" each sampling 216 | % period into polyInterpFactor instants (or phases). Hence, design the 217 | % filter with a combined oversampling factor of "L * polyInterpFactor". 218 | % Later on, after applying the polyphase decomposition, each resulting 219 | % subfilter will end up having an oversampling of only L. 220 | interpMf = sqrt(polyInterpFactor) * ... 221 | rcosdesign(rollOff, rcDelay, L * polyInterpFactor); 222 | polyMf = polyDecomp(interpMf, polyInterpFactor); 223 | assert(size(polyMf, 1) == polyInterpFactor); 224 | 225 | % Polyphase dMF 226 | % 227 | % Each subfilter of the polyphase MF is a phase-offset RRC on its own, 228 | % equivalent to a phase-offset version of the filter produced by 229 | % "rcosdesign(rollOff, rcDelay, L)". Correspondingly, to polyphase dMF 230 | % shall contain the differentiated rows of the polyphase MF. 231 | polyDMf = zeros(size(polyMf)); 232 | for i = 1:polyInterpFactor 233 | polyDMf(i, :) = derivativeMf(polyMf(i, :), L); 234 | end 235 | 236 | % To facilitate the convolution computation using inner products, flip 237 | % all subfilters (rows of polyMf and polyDMf) from left to right. 238 | polyMf = fliplr(polyMf); 239 | polyDMf = fliplr(polyDMf); 240 | else 241 | polyMf = []; % dummy variable 242 | end 243 | 244 | % Quadratic and cubic interpolators 245 | % 246 | % Define the matrix bl_i with the Farrow coefficients to multiply the 247 | % samples surrounding the desired interpolant. Each column of b_mtx holds 248 | % b_l(i) for a fixed l (exponent of mu(k) in 8.76) and for i (neighbor 249 | % sample index) from -2 to 1. After the fliplr operations, the first column 250 | % becomes the one associated with l=0 and the last with l=2. Each of those 251 | % columns are filters to process the samples from x(mk-1) to x(mk+2), i.e., 252 | % the sample before the basepoint index (x(mk-1)), the sample at the 253 | % basepoint index (x(mk)), and two samples ahead (x(mk+1) and x(mk+2)). 254 | % Before the flipud, the first row of bl_i would have the taps for i=-2, 255 | % which would multiply x(mk+2). For convenience, however, the flipping 256 | % ensures the first row has the taps for i=+1, which multiply x(mk-1). This 257 | % order facilitates the dot product used later. 258 | if (intpl == 2) 259 | % Farrow coefficients for alpha=0.5 (see Table 8.4.1) 260 | alpha = 0.5; 261 | b_mtx = flipud(fliplr(... 262 | [+alpha, -alpha, 0; ... 263 | -alpha, (1 + alpha), 0; ... 264 | -alpha, (alpha - 1), 1; ... 265 | +alpha, -alpha, 0])); 266 | elseif (intpl == 3) 267 | % Table 8.4.2 268 | b_mtx = flipud(fliplr(... 269 | [+1/6, 0, -1/6, 0; ... 270 | -1/2, +1/2, +1, 0; ... 271 | +1/2, -1, -1/2, 1; ... 272 | -1/6, +1/2, -1/3, 0])); 273 | else 274 | b_mtx = []; % dummy variable 275 | end 276 | 277 | %% Timing Recovery Loop 278 | 279 | % Constants 280 | nSamples = length(inVec); 281 | nSymbols = ceil(nSamples / L); 282 | 283 | % Preallocate 284 | xI = zeros(nSymbols, 1); % Output interpolants 285 | mu = zeros(nSymbols, 1); % Fractional symbol timing offset estimate 286 | v = zeros(nSamples, 1); % PI output 287 | e = zeros(nSamples, 1); % Error detected by the TED 288 | 289 | % Initialize 290 | k = 0; % interpolant/symbol index 291 | strobe = 0; % strobe signal 292 | cnt = 1; % modulo-1 counter 293 | vi = 0; % PI filter integrator 294 | 295 | % NOTE: by starting cnt with value 1, the first strobe is asserted when 296 | % "n=L+1" and takes effect on iteration "n=L+2", while setting the 297 | % basepoint index to "m_k=L+1". Furthermore, because the counter step is 298 | % "W=1/L" before the first strobe and "cnt=0" when the first strobe is 299 | % asserted, the first fractional interval estimate from Eq. (8.89) is 300 | % "mu=0". Consequently, the first interpolant tends to be closer (or equal 301 | % to) the sample at the basepoint index m_k, namely to "x(L+1)". This is 302 | % not strictly necessary, but is important to understand, e.g., when 303 | % evaluating the loop in unit tests. Also, this strategy is useful to 304 | % ensure there is enough "memory" when the time comes to compute the first 305 | % interpolant, as the interpolation equations use samples from the past. 306 | % 307 | % Furthermore, note that some TED schemes (ZCTED, GTED, and MMTED) use the 308 | % previous output interpolant or its decision in the error computation. 309 | % Since the first strobe takes effect on iteration "n=L+2", with a 310 | % basepoint at "n=L+1", let the raw input sample at "n=1" be considered as 311 | % the "last output interpolant" when computing the timing error on the 312 | % first strobe, even though "inVec(1)" is not strictly an output 313 | % interpolant. Again, this is an arbitrary choice, which is noteworthy when 314 | % testing the loop. By picking inVec(1) as the starting "last_xI", we can 315 | % compute the timing error right from the first strobe (k=1). Otherwise, we 316 | % would need to compute the error conditionally on "k > 1". 317 | % 318 | % Lastly, note the above "cnt=1" initialization applies only to the first 319 | % iteration. In all other iterations, the counter is always within [0, 1). 320 | last_xI = inVec(1); 321 | 322 | % End the loop with enough margin for the computations 323 | % 324 | % Do not process the last L samples when using the ELTED due to the 325 | % look-ahead scheme used to compute the "early" interpolant. When using the 326 | % quadratic or cubic interpolators (which use "x(m_k + 2)"), leave one 327 | % extra sample in the end. In all other cases, process all samples. 328 | if (strcmp(TED, 'ELTED')) 329 | n_end = nSamples - L; 330 | elseif (intpl > 1) 331 | n_end = nSamples - 1; 332 | else 333 | n_end = nSamples; 334 | end 335 | 336 | % Start with enough history samples for the interpolator. 337 | % 338 | % As mentioned earlier, the first strobe only takes effect on iteration 339 | % "n=L+2", and the first basepoint index is "m_k=L+1". Hence, the first 340 | % interpolation can access up to L samples from the past. This amount is 341 | % generally sufficient for the linear, quadratic, and cubic interpolators, 342 | % which at maximum access the sample preceding the basepoint index (in this 343 | % case, index "n = m_k - 1 = L"). Thus, the loop can be started right from 344 | % "n=1". Furthermore, this approach works even with the ELTED, ZCTED and 345 | % GTED schemes, which need to compute the zero-crossing (or late) 346 | % interpolants. The zero-crossing interpolant is computed based on the 347 | % basepoint index at "m_k - L/2", so the interpolator only uses up to index 348 | % "m_k - L/2 - 1", which is guaranteed to be available for L >= 2. For 349 | % instance, if L=2, the first basepoint index is "m_k=3", the zero-crossing 350 | % basepoint index is 2, and the interpolator accesses index 1 to compute 351 | % the zero-crossing interpolant. 352 | % 353 | % The only scenario where there may not be enough history samples for the 354 | % interpolation is if using the polyphase interpolator. The sample history 355 | % required by the polyphase interpolator depends on the filter length 356 | % adopted on each polyphase branch. Say, if the polyphase branch filter has 357 | % length N, it processes the samples from index "m_k - N + 1" to m_k. This 358 | % range only works if "m_k - N + 1 >= 1", namely if "m_k >= N". And since 359 | % the first basepoint index occurs after L iterations from the start, the 360 | % loop must start at index "N - L". Furthermore, when using the ELTED, 361 | % ZCTED, or GTED, all of which compute the zero-crossing interpolant using 362 | % basepoint index "m_k - ceil(L/2)", the starting index must be offset by 363 | % another "ceil(L/2)" samples. 364 | if (intpl == 0) 365 | poly_branch_len = size(polyMf, 2); 366 | n_start = max(1, poly_branch_len - L); 367 | if (strcmp(TED, 'ELTED') || strcmp(TED, 'ZCTED') || ... 368 | strcmp(TED, 'GTED')) 369 | n_start = n_start + ceil(L/2); 370 | end 371 | else 372 | n_start = 1; 373 | end 374 | 375 | for n = n_start:n_end 376 | if strobe == 1 377 | % Interpolation 378 | xI(k) = interpolate(intpl, inVec, m_k, mu(k), b_mtx, polyMf); 379 | 380 | % Timing Error Detector: 381 | a_hat_k = Ksym * slice(xI(k) / Ksym, M); % Data Symbol Estimate 382 | switch (TED) 383 | case 'MLTED' % Maximum Likelihood TED 384 | % dMF interpolant 385 | if (intpl == 0) 386 | xdotI = interpolate(intpl, mfIn, m_k, mu(k), ... 387 | b_mtx, polyDMf); 388 | else 389 | xdotI = interpolate(intpl, dMfOut, m_k, mu(k), b_mtx); 390 | end 391 | % Decision-directed version of Eq. (8.98), i.e., Eq. (8.27) 392 | % adapted to complex symbols: 393 | e(n) = real(a_hat_k) * real(xdotI) + ... 394 | imag(a_hat_k) * imag(xdotI); 395 | case 'ELTED' % Early-late TED 396 | % Early and late interpolants 397 | early_idx = m_k + midpointOffset; 398 | late_idx = m_k - midpointOffset; 399 | early_mu = mu(k) - muOffset; 400 | late_mu = mu(k) + muOffset; 401 | x_early = interpolate(intpl, inVec, early_idx, ... 402 | early_mu, b_mtx, polyMf); 403 | x_late = interpolate(intpl, inVec, late_idx, ... 404 | late_mu, b_mtx, polyMf); 405 | % Decision-directed version of (8.99), i.e., (8.34) 406 | % adapted to complex symbols: 407 | e(n) = real(a_hat_k) * (real(x_early) - real(x_late)) + ... 408 | imag(a_hat_k) * (imag(x_early) - imag(x_late)); 409 | case 'ZCTED' % Zero-crossing TED 410 | % Estimate of the previous data symbol 411 | a_hat_prev = Ksym * slice(last_xI / Ksym, M); 412 | 413 | % Zero-crossing interpolant 414 | zc_idx = m_k - midpointOffset; 415 | zc_mu = mu(k) + muOffset; 416 | x_zc = interpolate(intpl, inVec, zc_idx, zc_mu, ... 417 | b_mtx, polyMf); 418 | 419 | % Decision-directed version of (8.100), i.e., (8.37) 420 | % adapted to complex symbols: 421 | e(n) = real(x_zc) * ... 422 | (real(a_hat_prev) - real(a_hat_k)) + ... 423 | imag(x_zc) * (imag(a_hat_prev) - imag(a_hat_k)); 424 | case 'GTED' % Gardner TED 425 | % Zero-crossing interpolant, same as used by the ZCTED 426 | zc_idx = m_k - midpointOffset; 427 | zc_mu = mu(k) + muOffset; 428 | x_zc = interpolate(intpl, inVec, zc_idx, zc_mu, ... 429 | b_mtx, polyMf); 430 | 431 | % Equation (8.101): 432 | e(n) = real(x_zc) * (real(last_xI) - real(xI(k))) ... 433 | + imag(x_zc) * (imag(last_xI) - imag(xI(k))); 434 | case 'MMTED' % Mueller and Müller TED 435 | % Estimate of the previous data symbol 436 | a_hat_prev = Ksym * slice(last_xI / Ksym, M); 437 | 438 | % Decision-directed version of (8.102), i.e., (8.49) 439 | % adapted to complex symbols: 440 | e(n) = real(a_hat_prev) * real(xI(k)) - ... 441 | real(a_hat_k) * real(last_xI) + ... 442 | imag(a_hat_prev) * imag(xI(k)) - ... 443 | imag(a_hat_k) * imag(last_xI); 444 | end 445 | 446 | % Update the "last output interpolant" for the next strobe 447 | last_xI = xI(k); 448 | 449 | % Real-time debugging scopes 450 | if (debug_r) 451 | step(hScope, xI(k)) 452 | step(hTScopeCounter, mu(k)); 453 | end 454 | else 455 | % Make the error null on the iterations without a strobe. This is 456 | % equivalent to upsampling the TED output. 457 | e(n) = 0; 458 | end 459 | 460 | % Loop Filter 461 | vp = K1 * e(n); % Proportional 462 | vi = vi + (K2 * e(n)); % Integral 463 | v(n) = vp + vi; % PI Output 464 | % NOTE: since e(n)=0 when strobe=0, the PI output can be simplified to 465 | % "v(n) = vi" on iterations without a strobe. It is only when strobe=1 466 | % that "vp != 0" and that vi (integrator output) changes. Importantly, 467 | % note the counter step W below changes briefly to "1/L + vp + vi" when 468 | % strobe=1 and, then, changes back to "1/L + vi" when strobe=0. In the 469 | % meantime, "vi" remains constant until the next strobe. 470 | 471 | % Adjust the step used by the modulo-1 counter (see below Eq. 8.86) 472 | W = 1/L + v(n); 473 | 474 | % Check whether the counter will underflow on the next cycle, i.e., 475 | % whenever "cnt < W". When that happens, the strobe signal must 476 | % indicate the underflow occurrence and trigger updates on: 477 | % 478 | % - The basepoint index: set to the index right **before** the 479 | % underflow. When strobe=1, it means an underflow will occur on the 480 | % **next** cycle. Hence, the index before the underflow is exactly 481 | % the current index. 482 | % - The estimate of the fractional symbol timing offset: the estimate 483 | % is based on the counter value **before** the underflow (i.e., on 484 | % the current cycle) and the current counter step, according to 485 | % equation (8.89). 486 | strobe = cnt < W; 487 | if (strobe) 488 | k = k + 1; % Update the interpolant Index 489 | m_k = n; % Basepoint index (the index **before** the underflow) 490 | mu(k) = cnt / W; % Equation (8.89) 491 | end 492 | 493 | % Next modulo-1 counter value: 494 | cnt = mod(cnt - W, 1); 495 | end 496 | 497 | % Trim the output vector 498 | if (strobe) % ended on a strobe (before filling the k-th interpolant) 499 | xI = xI(1:k-1); 500 | else 501 | xI = xI(1:k); 502 | end 503 | 504 | %% Static Debug Plots 505 | if (debug_s) 506 | figure 507 | plot(e) 508 | ylabel('Timing Error $e(t)$', 'Interpreter', 'latex') 509 | xlabel('Symbol $k$', 'Interpreter', 'latex') 510 | 511 | figure 512 | plot(v) 513 | title('PI Controller Output') 514 | ylabel('$v(n)$', 'Interpreter', 'latex') 515 | xlabel('Sample $n$', 'Interpreter', 'latex') 516 | 517 | figure 518 | plot(mu, '.') 519 | title('Fractional Error') 520 | ylabel('$\mu(k)$', 'Interpreter', 'latex') 521 | xlabel('Symbol $k$', 'Interpreter', 'latex') 522 | end 523 | 524 | end 525 | 526 | %% Interpolation 527 | function [xI] = interpolate(method, x, m_k, mu, b_mtx, poly_f) 528 | % [xI] = interpolate(method, x, m_k, mu, b_mtx, poly_h) returns the 529 | % interpolant xI obtained from the vector of samples x. 530 | % 531 | % Args: 532 | % method -> Interpolation method: polyphase (0), linear (1), quadratic 533 | % (2), or cubic (3). 534 | % x -> Vector of samples based on which the interpolant shall be 535 | % computed, including the basepoint and surrounding samples. 536 | % m_k -> Basepoint index, the index preceding the interpolant. 537 | % mu -> Estimated fractional interval between the basepoint index 538 | % and the desired interpolant instant. 539 | % b_mtx -> Matrix with the coefficients for the polynomial 540 | % interpolator used with method=2 or method=3. 541 | % poly_f -> Polyphase filter bank that should process the input samples 542 | % when using the polyphase interpolator (method=0). 543 | 544 | % Adjust the basepoint if mu falls out of the nominal [0,1) range. This 545 | % step is necessary only to support odd oversampling ratios, when a 546 | % +-0.5 offset is added to the original mu estimate. In contrast, with 547 | % an even oversampling ratio, mu is within [0,1) by definition. 548 | if (mu < 0) 549 | m_k = m_k - 1; 550 | mu = mu + 1; 551 | elseif (mu >= 1) 552 | m_k = m_k + 1; 553 | mu = mu - 1; 554 | end 555 | assert(mu >= 0 && mu < 1); 556 | 557 | switch (method) 558 | case 0 % Polyphase interpolator 559 | % Choose the polyphase subfilter using mu. Use the floor operator 560 | % to make sure the resulting branch (polyBranch) is always within 561 | % the acceptable range [1, polyInterpFactor], given that mu is 562 | % within [0, 1). Also, note it is perfectly feasible to use "round" 563 | % instead of "floor". In this case, it is only necessary to add an 564 | % extra subfilter to the filter bank. More specifically, to add a 565 | % shifted-by-one version of the first subfilter, namely the same 566 | % subfilter as the first but with a delay shorter by one sampling 567 | % period (see commit 0537d70). However, this extra complexity is 568 | % unnecessary, especially if the polyInterpFactor is high enough, 569 | % when the subfilter phases are already very close to each other. 570 | polyInterpFactor = size(poly_f, 1); 571 | polyBranch = floor(polyInterpFactor * mu) + 1; 572 | polySubfilt = poly_f(polyBranch, :); 573 | N = length(polySubfilt); 574 | xI = polySubfilt * x((m_k - N + 1) : m_k); 575 | case 1 % Linear Interpolator (See Eq. 8.61) 576 | xI = mu * x(m_k + 1) + (1 - mu) * x(m_k); 577 | case 2 % Quadratic Interpolator 578 | % Recursive computation based on Eq. 8.77 579 | v_l = x(m_k - 1 : m_k + 2).' * b_mtx; 580 | xI = (v_l(3) * mu + v_l(2)) * mu + v_l(1); 581 | case 3 % Cubic Interpolator 582 | % Recursive computation based on Eq. 8.78 583 | v_l = x(m_k - 1 : m_k + 2).' * b_mtx; 584 | xI = ((v_l(4) * mu + v_l(3)) * ... 585 | mu + v_l(2)) * mu + v_l(1); 586 | end 587 | end 588 | 589 | %% Function to map Rx symbols into constellation points 590 | function [z] = slice(y, M) 591 | if (isreal(y)) 592 | % Move the real part of input signal; scale appropriately and round the 593 | % values to get ideal constellation index 594 | z_index = round( ((real(y) + (M-1)) ./ 2) ); 595 | % clip the values that are outside the valid range 596 | z_index(z_index <= -1) = 0; 597 | z_index(z_index > (M-1)) = M-1; 598 | % Regenerate Symbol (slice) 599 | z = z_index*2 - (M-1); 600 | else 601 | M_bar = sqrt(M); 602 | % Move the real part of input signal; scale appropriately and round the 603 | % values to get ideal constellation index 604 | z_index_re = round( ((real(y) + (M_bar - 1)) ./ 2) ); 605 | % Move the imaginary part of input signal; scale appropriately and 606 | % round the values to get ideal constellation index 607 | z_index_im = round( ((imag(y) + (M_bar - 1)) ./ 2) ); 608 | 609 | % clip the values that are outside the valid range 610 | z_index_re(z_index_re <= -1) = 0; 611 | z_index_re(z_index_re > (M_bar-1)) = M_bar-1; 612 | z_index_im(z_index_im <= -1) = 0; 613 | z_index_im(z_index_im > (M_bar-1)) = M_bar-1; 614 | 615 | % Regenerate Symbol (slice) 616 | z = (z_index_re*2 - (M_bar-1)) + 1j*(z_index_im*2 - (M_bar-1)); 617 | end 618 | end 619 | --------------------------------------------------------------------------------