- Class folder 'Timing Recovery' with different timing recoveries
- Minimal example for the timing recovery on the FSO data
- New evaluation scripts in the FSO project folder
This commit is contained in:
magf
2026-02-02 10:56:12 +01:00
parent 798a0ca3b3
commit 005e821131
28 changed files with 3685 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
classdef Godard_Timing_Recovery < handle
properties(Access=public)
mode
num_blocks
fft_length
sps
rolloff
mu
Ki
end
methods(Access=public)
function obj = Godard_Timing_Recovery(options)
arguments(Input)
options.mode = 0;
options.num_blocks = 1;
options.fft_length = 1024;
options.sps = 2;
options.rolloff = 0;
options.mu = 0;
options.Ki = 1e-3;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
%obj-Initialization here%
end
function [data_out, tau_hat] = process(obj, data_in)
data_out = data_in;
output_vector = zeros(length(data_in),1);
block_length = length(data_in)/obj.num_blocks;
for i = 1:obj.num_blocks
i_start = ((i-1)*block_length)+1;
i_end = i*block_length;
x = data_in.signal(i_start:i_end);
beta = obj.rolloff;
eta = obj.sps;
R = fft(x,obj.fft_length);
R_full = fft(x);
if obj.mode == 0 % Classic Godard
% Calculate time shift
k0 = (1:obj.fft_length/2).';
idx1 = k0;
idx2 = k0+(obj.fft_length/2);
tau_hat = sum(imag(R(idx1) .* conj(R(idx2))));
elseif obj.mode == 1 || obj.mode == 2 % Modified Godard 1
% Calculate Shift for the received signal
shiftBins = (1 - 1/eta) * obj.fft_length;
if abs(shiftBins - round(shiftBins)) > 1e-12
disp('Warning: shiftBins=(1-1/eta)*fft_length is non-integer. Choose compatible values for fft_length and eta.');
end
shiftBins = round(shiftBins);
% Calculate upper and lower bounds
kStart = ((1-beta)/(2*eta)) * obj.fft_length;
kEnd = ((1+beta)/(2*eta)) * obj.fft_length - 1;
if abs(kStart - round(kStart)) > 1e-12 || abs(kEnd - round(kEnd)) > 1e-12
disp('Warning: kStart/kEnd are non-integer. Choose compatible values for fft_length, eta, and beta.');
end
kStart = round(kStart);
kEnd = round(kEnd);
k0 = (kStart:kEnd).';
idx1 = k0 + 1;
idx2 = mod(k0 + shiftBins, obj.fft_length) + 1;
% Calculate time shift
if obj.mode == 1
tau_hat = sum(imag(R(idx1) .* conj(R(idx2))));
elseif obj.mode == 2
tau_hat = sum(angle(R(idx1))-angle(R(idx2)));
end
end
% % Normalization
% denom = floor(log10(abs(tau_hat)));
% tau_hat = tau_hat / 10^denom;
% Calculate mu using a first-order loop filter
mu_block = obj.mu + obj.Ki * tau_hat;
% % Shifting the signal in time domain using interpolation
% x_original = linspace(0,length(x)-1,length(x)).';
% x_new = x_original + mu_block;
% output_vector(i_start:i_end) = interp1(x_original,x,x_new,'linear','extrap');
% Shifting the signal in frequency domain
k = (0:block_length-1).';
phaseRamp = exp(-1j * 2*pi * (k/block_length) * mu_block);
R_shifted = R_full .* phaseRamp;
output_vector(i_start:i_end) = real(ifft(R_shifted));
end
data_out.signal = output_vector;
end
end
end

View File

@@ -0,0 +1,111 @@
classdef MaxVar_Timing_Recovery < handle
properties(Access=public)
mode
sps
fsym
fadc
num_tau
comp_signal
comp_mode
end
methods(Access=public)
function obj = MaxVar_Timing_Recovery(options)
arguments(Input)
options.mode = 0;
options.sps = 2;
options.fsym = 32e9;
options.fadc = 80e9;
options.num_tau = 64;
options.comp_signal = 0;
options.comp_mode = 0;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
%obj-Initialization here%
end
function [data_out] = process(obj, data_in)
data_out = data_in;
x = data_in.signal;
if obj.comp_mode
our_signal = data_in;
their_signal = obj.comp_signal;
end
if obj.mode == 0
vars = zeros(obj.sps,1);
for phi = 1:obj.sps
% Test variance for different start samples
y_phi = x(phi:obj.sps:end);
vars(phi) = var(y_phi);
% Comparison to reference signal
if obj.comp_mode
our_signal.signal = x(phi:obj.sps:end);
our_signal.fs = 6e9;
our_signal.normalize("mode","rms").plot("displayname",['Our signal, ' num2str(vars(phi))],'fignum',1231+phi);
their_signal.normalize("mode","rms").plot("displayname",'Their signal','fignum',1231+phi);
end
end
% Choose signal configuration with the maximum variance
[~,phi_opt] = max(vars);
y = x(phi_opt:obj.sps:end).';
elseif obj.mode == 1
% Create a grid with different (sub)sample starting points
T = 1/obj.fsym;
t = (0:length(x)-1)/obj.fadc;
tauGrid = linspace(0, T, obj.num_tau);
% Interpolate the signal starting from every defined point
% and calculate the MMSE/variance
vars = zeros(size(tauGrid));
for i = 1:numel(tauGrid)
tau = tauGrid(i);
% tk = linspace(tau, t(end), length(x)/obj.sps);
tk = tau : T : t(end)+tau;
xk = interp1(t, x, tk, 'linear', 'extrap');
if obj.comp_mode % MMSE
e = xk.' - obj.comp_signal.signal;
vars(i) = mean(abs(e).^2);
else % Variance
vars(i) = var(xk, 1);
end
end
if obj.comp_mode % MMSE
[~,idx] = min(vars);
else % Variance
[~,idx] = max(vars);
end
tau_opt = tauGrid(idx);
% Choosing the signal at optimum
% tk = linspace(tau_opt, t(end), length(x)/obj.sps);
tk = tau_opt : T : t(end)+tau_opt;
y = interp1(t, x, tk, 'linear', 'extrap');
end
% if length(y) ~= length(x)/obj.sps
% y = [y 0];
% end
data_out.signal = y.';
end
end
end

View File

@@ -0,0 +1,39 @@
classdef Time_Shifter < handle
properties(Access=public)
value
end
methods(Access=public)
function obj = Time_Shifter(options)
arguments(Input)
options.value = 0;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
%obj-Initialization here%
end
function [data_out, tau_hat] = process(obj, data_in)
data_out = data_in;
x = data_in.signal;
R = fft(x);
k = (0:length(R)-1).';
phaseRamp = exp(-1j * 2*pi * (k/length(R)) * obj.value);
R_shifted = R .* phaseRamp;
data_out.signal = real(ifft(R_shifted));
end
end
end

View File

@@ -0,0 +1,49 @@
classdef Timing_Recovery < handle
properties(Access=public)
modulation
timing_error_detector
sps
damping_factor
normalized_loop_bandwidth
detector_gain
end
methods(Access=public)
function obj = Timing_Recovery(options)
arguments(Input)
options.modulation = 'PAM/PSK/QAM';
options.timing_error_detector = 'Gardner (non-data-aided)';
options.sps = 2;
options.damping_factor = 1.0;
options.normalized_loop_bandwidth = 0.01;
options.detector_gain = 2.7;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
%obj-Initialization here%
end
function [data_out, timing_error] = process(obj, data_in)
timing_synchronization = comm.SymbolSynchronizer( ...
"Modulation", obj.modulation,...
"TimingErrorDetector", obj.timing_error_detector, ...
"SamplesPerSymbol", obj.sps, ...
"DampingFactor", obj.damping_factor, ...
"NormalizedLoopBandwidth", obj.normalized_loop_bandwidth, ...
"DetectorGain", obj.detector_gain);
data_out = data_in;
[data_out.signal, timing_error] = timing_synchronization(data_in.signal);
end
end
end

View File

@@ -0,0 +1,77 @@
classdef Timing_Recovery_GPT < handle
properties(Access=public)
sps
muGrid
end
methods(Access=public)
function obj = Timing_Recovery_GPT(options)
arguments(Input)
options.sps = 2;
options.muGrid = 0;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
%obj-Initialization here%
end
function [data_out, mu_best, score] = process(obj, data_in)
%MAXVARTIMINGSYNC Choose sampling phase mu that maximizes variance of downsampled symbols.
%
% data_in : matched-filtered samples (complex or real), length N
% sps : samples per symbol (here typically 2)
% muGrid : candidate fractional offsets in [0,1)
%
% data_out : symbol-rate samples (length floor(N/sps))
% mu_best: chosen fractional offset
% score : variance score for each mu in muGrid
data_out = data_in;
x = data_in.signal(:);
N = length(x);
Ns = floor(N/obj.sps);
if nargin < 3 || isempty(obj.muGrid)
obj.muGrid = linspace(0, 0.99, 101); % 0..0.99 in ~0.01 steps
end
% Symbol indices (1-based sample positions)
n0 = 1; % start sample index
k = (0:Ns-1).';
tBase = n0 + k*obj.sps; % integer times (1, 1+sps, ...)
score = zeros(numel(obj.muGrid),1);
for m = 1:numel(obj.muGrid)
mu = obj.muGrid(m);
t = tBase + mu;
% Linear fractional sampling
y = interp1(1:N, x, t, 'linear', 'extrap');
% For PAM, maximize variance of real part (or abs if you prefer)
yr = real(y);
score(m) = var(yr, 1); % use population variance (normalization doesn't matter for argmax)
end
% Pick best mu
[~, idx] = max(score);
mu_best = obj.muGrid(idx);
% Resample with best mu
t = tBase + mu_best;
data_out.signal = interp1(1:N, x, t, 'linear', 'extrap');
end
end
end

View File

@@ -0,0 +1,77 @@
classdef Timing_Recovery_Move_It < handle
properties(Access=public)
f_sim
gamma
end
methods(Access=public)
function obj = Timing_Recovery_Move_It(options)
arguments(Input)
options.f_sim = 14e9;
options.gamma = 0.1;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
%obj-Initialization here%
end
function data_out = process(obj, data_in)
e = NaN(size(data_in.signal));
T = 1/obj.f_sim;
t = zeros(size(data_in.signal));
t(2) = T/2;
mu = zeros(size(data_in.signal));
% n = 2;
for k = 3:2:length(data_in.signal)
data_in.signal(k-1) = data_in.signal(k-1)*(1-mu(k-2)) + data_in.signal(k)*mu(k-2);
data_in.signal(k) = data_in.signal(k)*(1-mu(k-1)) + data_in.signal(k+1)*mu(k-1);
% TED
e(k) = (data_in.signal(k-2)-data_in.signal(k))*data_in.signal(k-1);
e(k+1) = e(k);
% TED Mueller Mueller
% e(k) = ref_in(n-1)*data_in.signal(k) - ref_in(n)*data_in.signal(k-2);
% e(k+1) = e(k);
% n = n+1;
%
% interpolator control
% t(k) = t(k-1) + T/2 + obj.gamma/2*e(k);
% t(k+1) = t(k) + T/2 + obj.gamma/2*e(k+1);
t(k) = t(k-1) + T/2 + obj.gamma*e(k)*T/2;
t(k+1) = t(k) + T/2 + obj.gamma*e(k+1)*T/2;
% t(k) = k*T/2 + obj.gamma*e(k)*T/2;
% t(k+1) = k*T/2 + obj.gamma*e(k+1)*T/2;
% interpolator
mu(k) = t(k)/(T/2) - round(t(k)/(T/2));
mu(k+1) = t(k+1)/(T/2) - round(t(k+1)/(T/2));
thres = 0.7;
if mu(k)-mu(k-1) > thres
mu(k) = mu(k) - 1;
elseif mu(k) - mu(k-1) < -thres
mu(k) = mu(k) + 1;
end
if mu(k+1)-mu(k) > thres
mu(k+1) = mu(k+1) - 1;
elseif mu(k+1) - mu(k) < -thres
mu(k+1) = mu(k+1) + 1;
end
end
data_out = data_in;
end
end
end