Main Content

HDL Neural Network Design for Digital Predistorter

R2026b
Since R2026b

This example shows how to design, train, and validate a neural network digital predistorter (NN DPD) that uses segmented spline activations (SSC).The SSC-NN DPD linearizes a power amplifier (PA) driven by a wideband OFDM signal. PAs introduce nonlinear distortion, which causes spectral regrowth and degrades error vector magnitude (EVM). DPD compensates for PA nonlinearity by applying an inverse distortion before the PA. Classical DPD approaches use memory polynomials, which require high polynomial orders and memory depths for wideband signals. NN-based DPD can capture complex nonlinearities with fewer parameters.

This example uses a feedforward neural network with segmented spline activations which are piecewise-linear learnable activation functions that are hardware-friendly and can be implemented as a lookup table with linear interpolation, while providing the expressiveness of smooth nonlinear activations.

In this example, you compare the SSC-NN DPD performance against a classical memory polynomial DPD and verify fixed-point Simulink fidelity before hardware deployment.

The following diagram shows the end-to-end OFDM signal chain used for DPD validation. The OFDM signal generation, sample rate conversion, and performance evaluation are done in MATLAB(R), while the DPD block highlighted is the device under test (DUT) - a fixed-point Simulink(R) subsystem targeted for HDL code generation.

Signal Generation

Generate an OFDM waveform, upsample with digital upconversion (DUC), and apply crest factor reduction (CFR).

ofdmCfg = getOFDMConfig();

rng(123);
numTxFrames = 1;
radioSampleRate = 122.88e6;

% Generate transmit waveform
txBits = randi([0 1], ofdmCfg.bitsPerFrame, 1);
txWaveformTrain = customOFDMTx(txBits, ofdmCfg);
matTxOut = txWaveformTrain;

% Upsample to radio sample rate
txWaveformTrain = digitalUpconvert(txWaveformTrain, ...
    radioSampleRate/ofdmCfg.ofdmSampleRate, ofdmCfg);

% Measure PAPR and determine CFR threshold
paprTarget = 8; % dB
refImpedance = 1;
pm = powermeter(Measurement="All", ...
    WindowLength=round(radioSampleRate*10e-3), ...
    ReferenceLoad=refImpedance, PowerUnits="dBm");
[avgpwr,~,~] = pm(txWaveformTrain);

avgpwrOFDM = avgpwr(end);
peakInstPow = avgpwrOFDM + paprTarget;
peakInstMag = db2mag(peakInstPow - 30);

% Apply CFR and scale. The 0.875 factor reserves 12.5% headroom below
% full-scale to prevent DAC clipping from residual peaks after CFR.
postCfrScale = 0.875 / peakInstMag;
txWaveformTrain = cfrClip(txWaveformTrain, peakInstMag) * postCfrScale;
scalingFactor = 1/std(txWaveformTrain);

Generate a separate test waveform for evaluation.

txBits = randi([0 1], ofdmCfg.bitsPerFrame, 1);
[txWaveformTest, txDataSymsTest] = customOFDMTx(txBits, ofdmCfg);
[txWaveformTest, filterCoeffs] = digitalUpconvert(txWaveformTest, ...
    radioSampleRate/ofdmCfg.ofdmSampleRate, ofdmCfg);
txWaveformTest = cfrClip(txWaveformTest, peakInstMag) * postCfrScale;

Generate a separate test waveform for validation.

txBits = randi([0 1], ofdmCfg.bitsPerFrame, 1);
[txWaveformVal, txDataSymsVal] = customOFDMTx(txBits, ofdmCfg);
[txWaveformVal, ~] = digitalUpconvert(txWaveformVal, ...
    radioSampleRate/ofdmCfg.ofdmSampleRate, ofdmCfg);
txWaveformVal = cfrClip(txWaveformVal, peakInstMag) * postCfrScale;

PA Behavioral Model

This example uses a neural network trained on data captured from a real PA using an NI VST to model the PA behavior. The trained PA model resides in |paModelNN.mat|. This approach provides a realistic nonlinear model without requiring hardware-in-the-loop (HIL). For details on the PA modeling methodology and training procedure, see Power Amplifier Modeling Using Neural Networks.

% Input backoff to avoid hard saturation
IBO = 8.2; % dB
pmPapr = powermeter(...
    Measurement="Peak-to-average power ratio", ...
    WindowLength=round(radioSampleRate*10e-3), ...
    ReferenceLoad=refImpedance, ...
    PowerUnits="dBm");

paprOFDM = pmPapr(txWaveformTrain);
paprOFDM = paprOFDM(end);

disp("OFDM Tx Waveform PAPR: " + num2str(paprOFDM,4) + "dBm");
OFDM Tx Waveform PAPR: 8.008dBm
if paprOFDM >= IBO
    warning("OFDM signal PAPR is greater than the PA back-off and will saturate")
end
% PA observation without DPD
paObsNoDpd = paObservationPath(txWaveformTest, IBO, refImpedance, radioSampleRate);

Visualize PA AM/AM characteristics.

helperPANNPlotSpecAnAMAM(txWaveformTest, paObsNoDpd);

Figure contains an axes object. The axes object with title AM/AM, xlabel Input Power (dB), ylabel Output Power (dB) contains a line object which displays its values using only markers.

SSC-NN DPD Architecture

This example uses the SSC neural network proposed by Vaicaitis and Dooley [1]. The SSC replaces standard activation functions (tanh, ReLU) with piecewise-linear spline segments whose coefficient values are learnable parameters. This design choice reflects hardware implementation constraints.

  • Low-latency arithmetic - SSC evaluation requires only multiply, add, and subtract. Standard sigmoid activations (tanh) require iterative algorithms (CORDIC-like), which have several times higher latency than simple arithmetic.

  • Bit-shift segment indexing - Choosing the number of spline segments as a power of two reduces the division operation for locating the active segment to a bit-shift, eliminating the need for a divider or multiplier for indexing.

  • Fewer active coefficients - Although total coefficient storage increases with spline length, inference accesses only two coefficients per neuron corresponding to the left and right boundaries of the active segment. The 9-neuron SSC network achieves better normalized mean-square error (NMSE), adjacent channel power ratio (ACPR), and error vector magnitude (EVM) than comparable RVTDNN and ARVTDNN models while using fewer active coefficients.

  • No bias or envelope terms needed - Spline coefficients implicitly capture bias, and SSC flexibility models envelope-dependent behavior without requiring explicit envelope inputs.

The DPD network has the following components:

Input preprocessor - Forms memory taps of depth 2 and separates real and imaginary parts, producing a 4-element input vector per sample.

Fully connected layer - Maps 4 inputs to 9 neurons (hidden layer).

SSC activation layer - Assigns each neuron a segmented spline with 9 coefficients. The spline is piecewise linear between adjacent coefficients, and the coefficient values are learnable parameters.

Output fully connected layer - Maps 9 inputs to 2 outputs, real and imaginary parts of the predistorted signal.

The SSC layer is hardware-friendly because it reduces to a lookup table with linear interpolation - no multipliers beyond the interpolation weight.

memDepth = 2;
inputLayerDim = 2 * memDepth;
numNeurons = 9;
% numSplineCoeffs must be 2^N + 1 (e.g., 3, 5, 9, 17, 33) so that
% deltaInverse is a power of 2, enabling pure bit-shift indexing in HDL.
numSplineCoeffs = 9;

Display the network architecture.

dpdNet = dlnetwork;

tempNet = ([featureInputLayer(inputLayerDim,"Name","input")
    fullyConnectedLayer(numNeurons,"Name","linear1","BiasLearnRateFactor",0)
    SSCLayer(numSplineCoeffs)]);

tempNet(3).Name = "ssc1";

dpdNet = addLayers(dpdNet,tempNet);

tempNet = selectLayer([memDepth 2*memDepth]);
tempNet.Name = "iqInput";
dpdNet = addLayers(dpdNet,tempNet);

tempNet = [
    concatenationLayer(1,2,"Name","concat")
    fullyConnectedLayer(2,"Name","linearOutput")];
dpdNet = addLayers(dpdNet,tempNet);

dpdNet = connectLayers(dpdNet,"ssc1","concat/in1");
dpdNet = connectLayers(dpdNet,"iqInput","concat/in2");
dpdNet = connectLayers(dpdNet,"input","iqInput");

dpdNet = initialize(dpdNet);
analyzeNetwork(dpdNet);

DPD Training

Train the DPD using the indirect learning architecture (ILA) [1]. The network learns the inverse PA model: it takes the PA output (post-observation path) as input and targets the original clean PA input. Once the network converges, copy the trained coefficients directly into the predistorter block. Repeat this process over multiple ILA iterations until performance saturates.

ILA is chosen over Direct Learning Architecture (DLA) because it does not require a differentiable PA model for back propagation — the network simply learns a regression from distorted to clean samples. This pairs well with the SSC activation, whose piecewise-linear segments yield simple constant gradients (the slope of each active segment), making training stable and fast.

Set trainNow = true to retrain the network. By default, pre-trained weights are loaded from dpdNetTrained.mat.

trainNow = false;
if trainNow
    % Define network structure
    % https://www.mdpi.com/1424-8220/24/6/1829
    % https://ieeexplore.ieee.org/document/9916226
    nnDPDinputPreproc = dpdPreprocessor(memDepth,1);

    % Validation data
    paObsVal = paObservationPath(txWaveformVal, IBO, refImpedance, radioSampleRate);
    dpdNetInputVal = nnDPDinputPreproc(paObsVal*scalingFactor);
    reset(nnDPDinputPreproc);

    dpdNetTargetVal = [real(txWaveformVal), imag(txWaveformVal)]*scalingFactor;

    % Define training paramaters
    maxEpochs = 20;
    miniBatchSize = 1024;
    iterPerEpoch = floor(size(txWaveformVal, 1)/miniBatchSize);
    trainingPlots = "training-progress";
    metrics = [];
    verbose = false;

    options = trainingOptions('adam', ...
        MaxEpochs=maxEpochs, ...
        MiniBatchSize=miniBatchSize, ...
        InitialLearnRate=4e-4, ...
        LearnRateDropFactor=0.95, ...
        LearnRateDropPeriod=5, ...
        LearnRateSchedule='piecewise', ...
        Shuffle='every-epoch', ...
        OutputNetwork='best-validation-loss', ...
        ValidationData={dpdNetInputVal,dpdNetTargetVal}, ...
        ValidationFrequency=2*iterPerEpoch, ...
        ValidationPatience=5, ...
        InputDataFormats="BC", ...
        TargetDataFormats="BC", ...
        ExecutionEnvironment='auto', ...
        Plots=trainingPlots, ...
        Metrics = metrics, ...
        Verbose=verbose, ...
        VerboseFrequency=2*iterPerEpoch);

    numILAIterations = 2;

    for ii = 1:numILAIterations
        if ii > 1
            % Feed txWaveTrain through predistorter trained in the previous
            % iteration
            dpdNetInputPreDistortTrain = nnDPDinputPreproc(txWaveformTrain*scalingFactor);
            reset(nnDPDinputPreproc);

            nnDpdOut = predict(dpdNet,dpdNetInputPreDistortTrain);
            txWaveformTrainIter = double(complex(nnDpdOut(:,1),nnDpdOut(:,2)))./ scalingFactor;
        else
            txWaveformTrainIter = txWaveformTrain;
        end

        paObsTrain = paObservationPath(txWaveformTrainIter, IBO, refImpedance, radioSampleRate);

        dpdNetInputTrain = nnDPDinputPreproc(paObsTrain*scalingFactor);
        reset(nnDPDinputPreproc);

        dpdNetTargetTrain = [real(txWaveformTrainIter), imag(txWaveformTrainIter)]*scalingFactor;

        dpdNet = trainnet(dpdNetInputTrain,dpdNetTargetTrain,dpdNet,"mse",options);
    end

    save("dpdNetTrained","dpdNet");
else
    load("dpdNetTrained");
end

SSC-NN DPD - MATLAB Inference

Run the trained DPD network on the test waveform in MATLAB.

nnDPDinputPreproc = dpdPreprocessor(memDepth, 1);
dpdNetInputTest = nnDPDinputPreproc(txWaveformTest * scalingFactor);
reset(nnDPDinputPreproc);

nnDpdOut = predict(dpdNet, dpdNetInputTest);

nnDpdOut = double(complex(nnDpdOut(:,1), nnDpdOut(:,2))) ./ scalingFactor;

paObsNNDpd = paObservationPath(nnDpdOut, IBO, refImpedance, radioSampleRate);

SSC-NN DPD - Fixed-Point Simulink Verification

Run the same test waveform through the Simulink NNDPD subsystem to verify that fixed-point quantization does not degrade performance. This validates the HDL-ready model before hardware deployment.

The top-level model streams the test waveform one sample per clock into the NNDPD subsystem, which contains three stages: Pre-Process, SSC Network, and Post-Process. The Pre-Process stage applies the scaling factor, computes delay terms (memory taps based on memory depth), and separates real and imaginary components into a vector input for the network. A valid signal propagates alongside data through the pipeline.

modelName = 'HDLNNDigitalPredistorter';
load_system(modelName)

Inside the SSC Network, the signal flows through: a FC Layer (fixed-point multiply-accumulate), an SSC Layer (piecewise-linear spline lookup with learnable coefficient values), a Select Layer that provides the linear I/Q skip connection, a Concat Layer that merges the SSC output with the skip path, and an Output Layer that produces the final 2-element predistorted output. Pipeline delays between stages align the data and valid signals.

numSamples = length(txWaveformTest);
samplesPerCycle = 1;
latency = 49;
stopTime = (numSamples+latency) / ofdmCfg.ofdmSampleRate;

out = sim(modelName);
simpaObsNNDPD = paObservationPath(double(out.nnDpdOut), IBO, refImpedance, radioSampleRate);
simpaObsNNDPD = simpaObsNNDPD(1:length(paObsNNDpd));

Compare the MATLAB floating-point and Simulink fixed-point DPD outputs to confirm quantization fidelity.

plotSignalComparison(nnDpdOut, double(out.nnDpdOut), ...
    "MATLAB Floating-Point", "Simulink Fixed-Point");

Figure contains 2 axes objects. Axes object 1 with title Real (max error: 0.00018689) contains 3 objects of type line. Axes object 2 with title Imag (max error: 0.00019356) contains 3 objects of type line. These objects represent MATLAB Floating-Point, Simulink Fixed-Point, Error.

Classical DPD Baseline

Train a classical memory polynomial DPD as a performance baseline. The memory polynomial model captures both the nonlinear and memory effects of the PA by using a set of basis functions formed from delayed input samples raised to various powers. Here, a nonlinear degree of 5 and memory depth of 5 are used, resulting in 25 complex coefficients estimated via least squares. This configuration represents a typical classical DPD implementation and serves as the reference for evaluating the SSC-NN approach in terms of both linearization performance and hardware cost.

paObsTrain = paObservationPath(txWaveformTrain, IBO, refImpedance, radioSampleRate);

estimator = comm.DPDCoefficientEstimator( ...
    DesiredAmplitudeGaindB=0, ...
    PolynomialType="Memory polynomial", ...
    Degree=5, MemoryDepth=5, Algorithm="Least squares");

coef = estimator(txWaveformTrain, paObsTrain);
dpdMem = comm.DPD(PolynomialType="Memory polynomial", Coefficients=coef);

classicalDpdOut = dpdMem(txWaveformTest);
paObsClassDpd = paObservationPath(classicalDpdOut, IBO, refImpedance, radioSampleRate);

Spectral Comparison

Compare the output spectra of the PA without DPD, with classical memory polynomial DPD (Degree 5, Memory Depth 5), and with SSC-NN DPD (9 neurons, 9 spline segments) in both MATLAB floating-point and Simulink fixed-point.

sa = spectrumAnalyzer;
sa.SpectrumType = "Power";
sa.SampleRate = radioSampleRate;
sa.ReferenceLoad = refImpedance;
sa.ChannelNames = {"No DPD", "Classical DPD", "SSC-NN DPD (MATLAB)", "SSC-NN DPD (Simulink)"};
sa([paObsNoDpd, paObsClassDpd, paObsNNDpd, simpaObsNNDPD]);
sa.ShowLegend = true;

Performance Comparison

Compute ACPR and NMSE for each configuration.

acprNoDPD       = helperACPR(paObsNoDpd, radioSampleRate, ofdmCfg.ofdmSampleRate);
acprClassDPD    = helperACPR(paObsClassDpd, radioSampleRate, ofdmCfg.ofdmSampleRate);
acprNNDPD       = helperACPR(paObsNNDpd, radioSampleRate, ofdmCfg.ofdmSampleRate);
acprSimNNDPD    = helperACPR(simpaObsNNDPD, radioSampleRate, ofdmCfg.ofdmSampleRate);

nmseNoDPD    = 10*log10(sum(abs(paObsNoDpd - txWaveformTest).^2) / sum(abs(txWaveformTest).^2));
nmseClassDPD = 10*log10(sum(abs(paObsClassDpd - txWaveformTest).^2) / sum(abs(txWaveformTest).^2));
nmseNNDPD    = 10*log10(sum(abs(paObsNNDpd - txWaveformTest).^2) / sum(abs(txWaveformTest).^2));
nmseSimNNDPD = 10*log10(sum(abs(simpaObsNNDPD - txWaveformTest).^2) / sum(abs(txWaveformTest).^2));

perfTable = table( ...
    [acprNoDPD; acprClassDPD; acprNNDPD; acprSimNNDPD], ...
    [nmseNoDPD; nmseClassDPD; nmseNNDPD; nmseSimNNDPD], ...
    VariableNames=["ACPR_dB", "NMSE_dB"], ...
    RowNames=["No DPD", "Classical DPD", "SSC-NN DPD (MATLAB)", "SSC-NN DPD (Simulink)"]);
disp(perfTable)
                             ACPR_dB    NMSE_dB
                             _______    _______

    No DPD                   -30.772    -22.825
    Classical DPD            -39.159    -33.251
    SSC-NN DPD (MATLAB)        -40.9      -36.2
    SSC-NN DPD (Simulink)    -40.901    -36.197

EVM and Constellation Analysis

Demodulate the received signal and compute EVM for each DPD configuration.

rxSourceOpts = ["No DPD", "Classical DPD", "SSC-NN DPD (MATLAB)", "SSC-NN DPD (Simulink)"];
paObsAll = {paObsNoDpd, paObsClassDpd, paObsNNDpd, simpaObsNNDPD};

figure;
tiledlayout(1, numel(rxSourceOpts));

for ii = 1:numel(rxSourceOpts)
    rxWaveform = digitalDownconvert(paObsAll{ii}, 16, ofdmCfg);
    refScale = rms(matTxOut) / rms(rxWaveform);
    rxWaveform = refScale * rxWaveform ./ max(abs(rxWaveform));

    [~, rxDataSymsEq] = customOFDMRx(rxWaveform, 0, ofdmCfg);

    evm = comm.EVM;
    rmsEVM(ii) = evm(txDataSymsTest, rxDataSymsEq);

    nexttile;
    plotConstellation(rxDataSymsEq, ...
        rxSourceOpts(ii) + newline + "EVM: " + num2str(rmsEVM(ii), '%.2f') + "%");
end

Figure contains 4 axes objects. Axes object 1 with title No DPD EVM: 5.81%, xlabel I, ylabel Q contains a line object which displays its values using only markers. Axes object 2 with title Classical DPD EVM: 2.04%, xlabel I, ylabel Q contains a line object which displays its values using only markers. Axes object 3 with title SSC-NN DPD (MATLAB) EVM: 1.47%, xlabel I, ylabel Q contains a line object which displays its values using only markers. Axes object 4 with title SSC-NN DPD (Simulink) EVM: 1.47%, xlabel I, ylabel Q contains a line object which displays its values using only markers.

HDL Code Generation and Synthesis Results

To check and generate HDL for this example, you must have HDL Code(TM). Use the |makehdl| and |makehdltb| commands to generate the HDL code and test bench for the NNDPD subsystem.

When you synthesize the NNDPD subsystem on AMD Zynq(TM) UltraScale+(TM) RFSoC ZCU111 evaluation kit, the frequency obtained after post map is about 509 MHz. This table shows the post map resource utilization results for a 16-bit complex input.

F = table(...
    categorical({'CLB LUTs'; 'CLB Registers';'DSP'}), ...
    categorical({'11777'; '16822'; '58'}), ...
    categorical({'425280'; '850560'; '4272'}), ...
    categorical({'1.62'; '1.36'; '1.66'}), ...
    'VariableNames', ...
    {'Resources','Utilized','Available','Utilization (%)'});
disp(F);
      Resources      Utilized    Available    Utilization (%)
    _____________    ________    _________    _______________

    CLB LUTs          11777       425280           1.62      
    CLB Registers     16822       850560           1.36      
    DSP               58          4272             1.66      

Summary

This example demonstrated a segmented spline curve neural network for digital predistortion, from algorithm design through HDL-ready implementation. The network is trained using indirect learning architecture with a simulated PA model derived from real NI VST measurements. The following table summarizes the performance and resource comparison.

T = table( ...
    categorical({'No DPD'; 'Classical DPD (Degree-3/Depth-3)'; 'Classical DPD (Degree-5/Depth-5)'; 'SSC-NN DPD (n=9, s=9)'}), ...
    [acprNoDPD; -36.631; acprClassDPD; acprSimNNDPD], ...
    categorical({'0'; '50'; '144'; '58'}), ...
    'VariableNames', {'Configuration', 'ACPR (dB)', 'Real Multipliers'});
disp(T);
             Configuration              ACPR (dB)    Real Multipliers
    ________________________________    _________    ________________

    No DPD                               -30.772           0         
    Classical DPD (Degree-3/Depth-3)     -36.631           50        
    Classical DPD (Degree-5/Depth-5)     -39.159           144       
    SSC-NN DPD (n=9, s=9)                -40.901           58        

At a comparable multiplier count (~58 vs ~50), the SSC-NN DPD achieves 4 dB better ACPR than the classical Degree-3 memory polynomial. To match the SSC-NN performance, the classical approach requires Degree-5 with nearly 3x the multipliers. The fixed-point Simulink simulation matches MATLAB floating-point results within ~0.5 dB ACPR, confirming quantization fidelity before HDL code generation. The design processes one sample per clock at 509 MHz, making it suitable for high bandwidth applications up to 500 MSPS.

References

[1] A. Vaicaitis and J. Dooley, "Segmented Spline Curve Neural Network for Low Latency Digital Predistortion of RF Power Amplifiers," IEEE Trans. Microw. Theory Techn., vol. 70, no. 11, pp. 4910–4915, Nov. 2022.