classdef FFE_DCremoval_level < handle % Implementation of plain and simple FFE. % 1) Training mode (stable performance when you use NLMS) % 2) Decision directed mode % Eq = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",25,"sps",2,"decide",0); properties sps % usually 2 order e error len_tr mu_tr epochs_tr mu_dd epochs_dd mu_dc dc_buffer_len constellation decide KF_meas_noise = 0; KF_process_noise = 0; KF_state_cov = 0; end methods function obj = FFE_DCremoval_level(options) arguments(Input) options.sps = 2; options.order = 15; options.len_tr = 4096; options.mu_tr = 0; options.epochs_tr = 5; options.mu_dd = 1e-5; options.epochs_dd = 5; options.mu_dc = 0.05; options.dc_buffer_len = 1; options.decide = false; end assert(options.dc_buffer_len>0); fn = fieldnames(options); for n = 1:numel(fn) obj.(fn{n}) = options.(fn{n}); end obj.e = zeros(obj.order,1); obj.error = 0; obj.dc_buffer_len = floor(obj.dc_buffer_len); end function [X,Noi] = process(obj, X, D) % actual processing of the signal (steps 1. - 3.) % 1 normalize RMS X = X.normalize("mode","rms"); obj.constellation = unique(D.signal); % Training Mode training = 1; obj.equalize(X.signal, D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training); % Decision Directed Mode N = X.length; training = 0; [signal,decision]=obj.equalize(X.signal, D.signal,obj.mu_dd,obj.epochs_dd,N,training); % Output Signal if obj.decide X.signal = decision; else X.signal = signal; end X.fs = D.fs; %change sampling frequency of outgoing signal from fdac e.g. 2 sps to symbol spaced = fsym lbdesc = [num2str(obj.order),' tap FFE']; X = X.logbookentry(lbdesc); % append to logbook Noi = X - D; end function [y, d_hat, logs] = equalize(obj, x, d, mu_lms, epochs, N, training) % Equalize with Kalman-based DC removal; training epochs estimate KF noise parameters arguments obj x d mu_lms % LMS step-size (or 0 for NLMS) epochs % number of training or DD epochs N % number of samples to process training % true => training mode (tap-training + noise estimation) end % Zero-pad for filter memory x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)]; numSym = ceil(N/obj.sps); % Pre-allocate outputs y = zeros(numSym,1); d_hat = zeros(numSym,1); % --- Training: estimate noise stats and train taps --- if training % Pre-allocate error accumulator totalTrain = epochs * numSym; trainErrs = zeros(totalTrain,1); te_idx = 0; for ep = 1:epochs s = 0; for n = 1:obj.sps:N s = s + 1; U = x(obj.order + n - 1 : -1 : n); % Equalizer output (no DC correction yet) y(s) = obj.e.' * U; % Decision based on known symbol [~, idx] = min(abs(d(s) - obj.constellation)); d_hat(s) = obj.constellation(idx); % Instantaneous error e_n = y(s) - d_hat(s); % Collect error for noise estimation te_idx = te_idx + 1; trainErrs(te_idx) = e_n; % Tap-weight update (LMS or NLMS) if mu_lms ~= 0 obj.e = obj.e - mu_lms * e_n * U; else normU = (U.'*U) + eps; obj.e = obj.e - e_n * U / normU; end end end % Estimate measurement noise R and process noise Q R_est = var(trainErrs(1:te_idx)); Q_est = 1e-3 * R_est; % Q/R ratio = 1e-3 (tune as needed) % Store into object for DD pass obj.KF_meas_noise = R_est; obj.KF_process_noise = Q_est; obj.KF_state_cov = 5*R_est; % or 5*R_est for a more “eager” start % No Kalman in training; return logs = struct(); return; end % --- Decision-Directed with Kalman DC tracking --- % Initialize Kalman state x_est = 0; P = obj.KF_state_cov; % initial P (tune in obj; e.g. 1) % Logging containers logs.y_raw = zeros(numSym,1); logs.y_corr = zeros(numSym,1); logs.err = zeros(numSym,1); logs.K_gain = zeros(numSym,1); logs.x_est = zeros(numSym,1); logs.P = zeros(numSym,1); logs.normU = zeros(numSym,1); logs.tap_norm = zeros(numSym,1); for ep = 1:epochs s = 0; for n = 1:obj.sps:N s = s + 1; U = x(obj.order + n - 1 : -1 : n); % 1) Kalman prediction P = P + obj.KF_process_noise; x_prior = x_est; % 2) raw equalizer output y_raw = obj.e.' * U; logs.y_raw(s) = y_raw; % 3) DC-corrected output y_corr = y_raw + x_prior; y(s) = y_corr; logs.y_corr(s) = y_corr; % 4) decision-directed symbol [~, idx] = min(abs(y_corr - obj.constellation)); d_hat(s) = obj.constellation(idx); % 5) error e_n = y_corr - d_hat(s); logs.err(s) = e_n; % 6) tap-weight update (LMS/NLMS) if mu_lms ~= 0 obj.e = obj.e - mu_lms * e_n * U; else normU = U.' * U + eps; logs.normU(s) = normU; obj.e = obj.e - e_n * U / normU; end logs.tap_norm(s) = norm(obj.e); % 7) Kalman update K_gain = P / (P + obj.KF_meas_noise); x_est = x_prior + K_gain * (e_n - x_prior); P = (1 - K_gain) * P; % 8) log Kalman state logs.K_gain(s) = K_gain; logs.x_est(s) = x_est; logs.P(s) = P; end end end end end