classdef FFE_DCremoval_adaptive_mu < 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 ffe_buffer_len smoothing_buffer_length smoothing_buffer_update constellation decide end methods function obj = FFE_DCremoval_adaptive_mu(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.ffe_buffer_len = 1; options.smoothing_buffer_length = 0; options.smoothing_buffer_update = 0; 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); % if obj.smoothing_buffer_length > 0 % % Apply A1 filter smoothing % % Calculate the moving sum with the window size N1 % moving_sum = movsum(X.signal, [obj.smoothing_buffer_length,0]); % % % Initialize the output smoothed signal % X.signal = X.signal - (1 / obj.smoothing_buffer_length) * moving_sum; % end % 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] = equalize(obj, x, d, mu_lms, epochs, N, training) % Equalize with adaptive DC-removal, VSS, and parallel-buffered DC updates % Added: FFE gradient buffering in DD mode (error buffer) with update every obj.dc_buffer_len symbols arguments obj x d mu_lms % LMS step-size (or 0 for NLMS) epochs % number of training/DD epochs N % number of samples to process training % boolean flag: true->training mode, false->DD mode end % Zero-padding for filter memory x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)]; % Initialize storage numSymbols = ceil(N/obj.sps); y = zeros(numSymbols,1); d_hat = zeros(numSymbols,1); err = NaN(numSymbols,numel(obj.constellation)); e_dc_save= zeros(numSymbols,1); % DC-adaptation parameters P_err = 0; % running error power alpha = 0.98; % forgetting factor for error power err_prev = 0; % previous error sample for VSS correlation gamma_dc = 1e-6; % meta step-size for DC VSS mu_min = 1e-6; % lower bound for mu_dc mu_max = 1e-1; % upper bound for mu_dc % DC removal buffer L = obj.dc_buffer_len; % buffer length e_dc_buf = NaN(L,1); e_dc_est = 0; % FFE gradient buffer (DD mode only) L_grad = obj.ffe_buffer_len; % buffer length if ~training % each column holds one past gradient of length obj.order grad_buf = NaN(obj.order, L_grad); end smth_buffer = zeros(1, obj.smoothing_buffer_length); smth_mean = 0; % Main loop for epoch = 1:epochs s = 0; for sample = 1:obj.sps:N s = s + 1; if obj.smoothing_buffer_length > 0 smth_buffer = circshift(smth_buffer,1,2); smth_buffer(1) = x(sample); if mod(s, obj.smoothing_buffer_update) == 0 smth_mean = mean(smth_buffer); end x(sample:sample+obj.sps-1) = x(sample:sample+obj.sps-1)-smth_mean; end U = x(obj.order+sample-1:-1:sample); %-- 1) filter output with DC correction y(s) = e_dc_est + obj.e.'*U; %-- 2) decision if training [~, idx] = min(abs(d(s) - obj.constellation)); else [~, idx] = min(abs(y(s) - obj.constellation)); end d_hat(s) = obj.constellation(idx); %-- 3) error e_val = y(s) - d_hat(s); err(s,idx) = e_val; %-- 4) tap-weight update: training immediate, DD buffered if training % immediate update (LMS or NLMS) if mu_lms ~= 0 obj.e = obj.e - mu_lms * e_val * U; else normU = (U.'*U) + eps; obj.e = obj.e - e_val * U / normU; end else if 1 % buffer gradient if mu_lms ~= 0 grad = e_val * U; else normU = (U.'*U) + eps; grad = e_val * U / normU; end % shift and insert grad_buf = circshift(grad_buf, 1, 2); grad_buf(:,1) = grad; % update once every L symbols if mod(s, L_grad) == 0 avg_grad = mean(grad_buf, 2, 'omitnan'); if mu_lms ~= 0 obj.e = obj.e - mu_lms * avg_grad; else obj.e = obj.e - avg_grad; end end end end %-- 5) DC adaptation if obj.mu_dc ~= 0 % VSS for mu_dc delta_mu = gamma_dc * e_val * err_prev * (U.'*U); obj.mu_dc = min(max(obj.mu_dc + delta_mu, mu_min), mu_max); err_prev = e_val; % DC buffer update & periodic estimate P_err = alpha*P_err + (1-alpha)*e_val^2; mu_dc_norm = obj.mu_dc / (P_err + eps); e_dc_buf = circshift(e_dc_buf, 1); e_dc_buf(1) = e_dc_est - mu_dc_norm * e_val; if mod(s, L) == 0 e_dc_est = median(e_dc_buf, 'omitnan'); end e_dc_save(s) = e_dc_est; end % store instantaneous squared error obj.error(epoch, s) = e_val^2; end end % Optional plotting in DD mode (uncomment if needed) if ~training figure(342);clf hold on scatter(1:numSymbols, err + obj.constellation', '.', 'SizeData', 1); yline(obj.constellation); end end function mu = update_mu() end end end