Troubleshooting 64-QAM DFE-NLMS Equalizer( High BER and Issues with Learning)

Hi all,
I’m working on a 64-QAM receiver simulation in Simulink (aiming for HDL Coder) and I’m having a lot of trouble getting the DFE-NLMS equalizer to converge. The BER is stuck high and the constellation looks terrible no matter what parameters I tweak. I suspect there is a logical bug in how I’m handling the feedback/update loop.
I want to restructure the equalizer block to do this:
  1. Use the first 14 symbols of the incoming Tx stream as a training sequence to get the initial coefficients.
  2. After these 14 symbols, I want to check the error magnitude. If it falls below a specific threshold, it should switch to decision-directed mode and pass the cleaned data to the demodulator.
My questions are:
  • What is the best way to write this state/threshold logic in a MATLAB Function block so it stays HDL-compatible?
  • How should I handle the packet/frame length? Do I need a dedicated counter to track the 14-symbol training window and the data payload, or is there a better way to do this in a streaming HDL design?
If anyone has a template or code snippet of a working DFE with this kind of training/decision logic, I’d really appreciate the help.
Thanks!
function [eq_out, err, w_ff_out] = dfe_nlms_hdl(rx_sample, tx_ref, train_enable)
%#codegen
% HDL-friendly DFE-NLMS equalizer for normalized 64-QAM
%
% Inputs:
% rx_sample : complex received sample
% tx_ref : complex reference symbol (properly delayed)
% train_enable : 1 => training mode, 0 => decision-directed mode
%
% Outputs:
% eq_out : equalizer output
% err : error signal
% w_ff_out : feed-forward weights (for monitoring)
% ------------------------------------------------------------
% Parameters
% ------------------------------------------------------------
N_ff = 10;
N_fb = 15;
mu_train = fi(0.002, 0, 16, 15);
mu_dd = fi(0.0007, 0, 16, 15);
delta = fi(0.05, 0, 16, 15);
% ------------------------------------------------------------
% Numeric type templates
% ------------------------------------------------------------
zero_r = cast(0, 'like', real(rx_sample));
one_r = cast(1, 'like', real(rx_sample));
zero_c = complex(zero_r, zero_r);
one_c = complex(one_r, zero_r);
% ------------------------------------------------------------
% Wider type for power accumulation (prevents overflow)
% ------------------------------------------------------------
if isa(zero_r, 'embedded.fi')
T_power = fi(0, 1, 24, 12); % signed, 24-bit, 12 fractional bits
zero_p = cast(0, 'like', T_power);
else
zero_p = zero_r;
end
% ------------------------------------------------------------
% Force inputs into the same complex type template
% ------------------------------------------------------------
rx_c = complex(cast(real(rx_sample), 'like', zero_r), ...
cast(imag(rx_sample), 'like', zero_r));
rx_c = cast(rx_c, 'like', zero_c);
tx_c = complex(cast(real(tx_ref), 'like', zero_r), ...
cast(imag(tx_ref), 'like', zero_r));
tx_c = cast(tx_c, 'like', zero_c);
% ------------------------------------------------------------
% Persistent states
% ------------------------------------------------------------
persistent ff_delay_line fb_delay_line w_ff w_fb
if isempty(ff_delay_line)
ff_delay_line = repmat(zero_c, N_ff, 1);
fb_delay_line = repmat(zero_c, N_fb, 1);
w_ff = repmat(zero_c, N_ff, 1);
w_fb = repmat(zero_c, N_fb, 1);
% Initialize center tap
w_ff(8) = one_c;
end
% ------------------------------------------------------------
% Update feed-forward delay line
% ------------------------------------------------------------
for i = N_ff:-1:2
ff_delay_line(i) = ff_delay_line(i - 1);
end
ff_delay_line(1) = rx_c;
% ------------------------------------------------------------
% Feed-forward FIR output
% ------------------------------------------------------------
y_ff = zero_c;
for i = 1:N_ff
prod_ff = cast(w_ff(i) * ff_delay_line(i), 'like', zero_c);
y_ff = cast(y_ff + prod_ff, 'like', zero_c);
end
% ------------------------------------------------------------
% Feedback FIR output
% ------------------------------------------------------------
y_fb = zero_c;
for i = 1:N_fb
prod_fb = cast(w_fb(i) * fb_delay_line(i), 'like', zero_c);
y_fb = cast(y_fb + prod_fb, 'like', zero_c);
end
% ------------------------------------------------------------
% DFE output
% ------------------------------------------------------------
eq_out = cast(y_ff - y_fb, 'like', zero_c);
% ------------------------------------------------------------
% Desired symbol selection
% ------------------------------------------------------------
if train_enable
desired = cast(tx_c, 'like', zero_c);
mu_curr = mu_train;
else
re_dec = slicer_64qam_axis(real(eq_out), zero_r);
im_dec = slicer_64qam_axis(imag(eq_out), zero_r);
desired = complex(cast(re_dec, 'like', zero_r), ...
cast(im_dec, 'like', zero_r));
desired = cast(desired, 'like', zero_c);
mu_curr = mu_dd;
end
% ------------------------------------------------------------
% Error
% ------------------------------------------------------------
err = cast(desired - eq_out, 'like', zero_c);
% ------------------------------------------------------------
% NLMS normalization power
% p_ff = sum(|x|^2)
% Use wider type to avoid overflow
% ------------------------------------------------------------
p_ff = zero_p;
for i = 1:N_ff
re_i = cast(real(ff_delay_line(i)), 'like', zero_p);
im_i = cast(imag(ff_delay_line(i)), 'like', zero_p);
re2 = cast(re_i * re_i, 'like', zero_p);
im2 = cast(im_i * im_i, 'like', zero_p);
mag2 = cast(re2 + im2, 'like', zero_p);
p_ff = cast(p_ff + mag2, 'like', zero_p);
end
norm_factor = cast(p_ff + cast(delta, 'like', zero_p), 'like', zero_p);
% ------------------------------------------------------------
% Adaptation step
% step = (mu / (||x||^2 + delta)) * err
% ------------------------------------------------------------
step_gain = cast(cast(mu_curr, 'like', zero_p) / norm_factor, ...
'like', zero_r);
step = cast(step_gain * err, 'like', zero_c);
% ------------------------------------------------------------
% Update feed-forward weights
% w_ff = w_ff + step * conj(ff_delay_line)
% ------------------------------------------------------------
for i = 1:N_ff
dw_ff = cast(step * conj(ff_delay_line(i)), 'like', zero_c);
w_ff(i) = cast(w_ff(i) + dw_ff, 'like', zero_c);
end
% ------------------------------------------------------------
% Update feedback weights
% y = y_ff - y_fb => gradient sign is negative for FB path
% w_fb = w_fb - step * conj(fb_delay_line)
% ------------------------------------------------------------
for i = 1:N_fb
dw_fb = cast(step * conj(fb_delay_line(i)), 'like', zero_c);
w_fb(i) = cast(w_fb(i) - dw_fb, 'like', zero_c);
end
% ------------------------------------------------------------
% Update feedback delay line using desired symbol
% ------------------------------------------------------------
for i = N_fb:-1:2
fb_delay_line(i) = fb_delay_line(i - 1);
end
fb_delay_line(1) = desired;
% ------------------------------------------------------------
% Optional output
% ------------------------------------------------------------
w_ff_out = w_ff;
end
function y = slicer_64qam_axis(x, zero_r)
%#codegen
% Axis slicer for normalized square 64-QAM
% Levels: ±1/7, ±3/7, ±5/7, ±7/7
x_r = cast(x, 'like', zero_r);
lvl1 = cast(1/7, 'like', zero_r);
lvl3 = cast(3/7, 'like', zero_r);
lvl5 = cast(5/7, 'like', zero_r);
lvl7 = cast(1, 'like', zero_r);
th0 = cast(0, 'like', zero_r);
th2 = cast(2/7, 'like', zero_r);
th4 = cast(4/7, 'like', zero_r);
th6 = cast(6/7, 'like', zero_r);
thm2 = cast(-2/7, 'like', zero_r);
thm4 = cast(-4/7, 'like', zero_r);
thm6 = cast(-6/7, 'like', zero_r);
if x_r < thm6
y = -lvl7;
elseif x_r < thm4
y = -lvl5;
elseif x_r < thm2
y = -lvl3;
elseif x_r < th0
y = -lvl1;
elseif x_r < th2
y = lvl1;
elseif x_r < th4
y = lvl3;
elseif x_r < th6
y = lvl5;
else
y = lvl7;
end
y = cast(y, 'like', zero_r);
end

Answers (0)

Asked:

on 1 Jul 2026

Commented:

on 6 Jul 2026

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!