Main Content

ofdmCFOEstimate

R2026b

Estimate OFDM carrier frequency offset

Since R2026b

Description

The function computes the estimated carrier frequency offset (CFO) for an OFDM-modulated input signal. It optionally outputs the timing offset and the CFO-compensated version of the received signal. For more information, see Algorithms.

fEst = ofdmCFOEstimate(rxSignal,nfft,cplen) estimates the OFDM carrier frequency offset for input signal, rxSignal, using an FFT length specified by nfft and cyclic prefix length specified by cplen.

fEst = ofdmCFOEstimate(rxSignal,nfft,cplen,Name=Value) specifies options using one or more name-value arguments in addition to the input arguments in the previous syntax.

[fEst,tOffset] = ofdmCFOEstimate(___) also returns the timing offset, tOffset, using input arguments in any of the previous syntaxes.

[fEst,tOffset,compensatedSignal] = ofdmCFOEstimate(___) also returns the compensated signal, compensatedSignal, using input arguments in any of the previous syntaxes.

example

Examples

collapse all

Estimate the carrier frequency offset (CFO) for an OFDM signal. Demodulate the received signal and the CFO-compensated received signal, and then check if each matches the input signal.

M = 16;
nfft = 64;
cplen = 16;
nSym = 200;
dataSym = randi([0 M-1],nfft,nSym);
qamSig = qammod(dataSym,M,UnitAveragePower=true);
y = ofdmmod(qamSig,nfft,cplen);

Set a frequency offset that is 1% of the sample rate. Apply a frequency offset of 1 kHz at a sample rate of 1 MHz using the frequencyOffset function, and then add white Gaussian noise.

offset = 1e3;
samplerate = 1e6
samplerate = 
1000000
modSigOffset = frequencyOffset(y,samplerate,offset);
rxSig = awgn(modSigOffset,50);

Estimate the CFO of the input signal.

[fEst,tOffset,compensatedSignal] = ofdmCFOEstimate(rxSig,nfft,cplen);

Confirm that the demodulated received signal (with no CFO compensation) does not match the transmitted signal.

x1 = ofdmdemod(rxSig,nfft,cplen);
rxData1 = qamdemod(x1,M,UnitAveragePower=true);
isequal(rxData1,dataSym)
ans = logical
   0

Confirm that the demodulated CFO compensated signal matches the transmitted signal.

x2 = ofdmdemod(compensatedSignal,nfft,cplen);
rxData2 = qamdemod(x2,M,UnitAveragePower=true);
isequal(rxData2,dataSym)
ans = logical
   1

Plot constellation diagram of received signal, x1, and CFO compensated signal, x2.

refc = qammod(0:M-1,M,UnitAveragePower=true);
cd = comm.ConstellationDiagram(2, ...
    ReferenceConstellation=refc, ...
    ChannelNames={'x1','x2'});
cd(x1(1,1:end)',x2(1,1:end)')

Compute carrier frequency offset (CFO) and channel estimation for an OFDM signal filtered through a MIMO channel.

Initialize OFDM system and simulation parameters.

nFFT = 1024;
cpLen = 72;
numGuardSc = 200;
Fs = 15.36e6;
numTx = 2;
numRx = 4;
numOFDMSymbolsPerSlot = 16;

rng(11122025);
SNRdB = 20;
maximumDopplerShift = 30;
totalNumSlots = 101;
silentPeriodLength = 18.327; % In number of OFDM symbols
actualCFO = 2.3456; % Between -5 and 5 subcarrier spacings
cfoDelta = 0.001; % Change in CFO per slot

mimochan = comm.MIMOChannel( ...
    SampleRate=Fs, ...
    PathDelays=[0 1.0495e-08 1.1095e-08 1.1645e-08 1.088e-08 ...
      3.183e-08 3.224e-08 3.28e-08 3.292e-08 3.9675e-08 4.1065e-08 ...
      4.668e-08 6.1425e-08 6.5415e-08 1.0852e-07 1.35525e-07 2.12945e-07 ...
      2.30015e-07 2.7451e-07 2.80385e-07 3.15325e-07 3.3187e-07 ...
      3.52135e-07 4.32615e-07], ...
    AveragePathGains=[-4.4 -1.2 -3.5 -5.2 -2.5 0 -2.2 -3.9 -7.4 -7.1 ...
      -10.7 -11.1 -5.1 -6.8 -8.7 -13.2 -13.9 -13.9 -15.8 -17.1 -16 ...
      -15.7 -21.6 -22.8], ...
    NormalizePathGains=false, ...
    FadingDistribution="Rayleigh", ...
    MaximumDopplerShift=maximumDopplerShift, ...
    SpatialCorrelationSpecification="None", ...
    NumTransmitAntennas=numTx, ...
    NumReceiveAntennas=numRx, ...
    NormalizeChannelOutputs=false, ...
    FadingTechnique="Sum of sinusoids", ...
    NumSinusoids=48, ...
    InitialTimeSource="Property", ...
    InitialTime=0, ...
    RandomStream="mt19937ar with seed", ...
    Seed=456);

symLen = nFFT + cpLen;
scs = Fs/nFFT;

fprintf("SNR = %.2f dB\n", SNRdB);
SNR = 20.00 dB
fprintf("Subcarrier spacing = %.2f Hz\n", scs);
Subcarrier spacing = 15000.00 Hz
actualCFOHz = actualCFO*scs; % In Hz
phaseOffset = rand(1)*2*pi;

Generate reference signals by using a ofdmPilotConfig configuration object. Use random QPSK-modulated pilot symbols to make this reference signal suitable for the ofdmChannelEstimate and ofdmCFOEstimate functions.

pilotcfg = ofdmPilotConfig(FFTLength=nFFT, ...
    NumGuardBandCarriers=[numGuardSc numGuardSc], ...
    NumSymbols=numOFDMSymbolsPerSlot, ...
    NumTransmitStreams=numTx, ...
    StreamGroups={1:numTx}, ...
    PilotLocations={ofdmPilotGrid(1:numTx,1, ...
      (numGuardSc+1):numTx:(nFFT-numGuardSc), ...
      [1 numOFDMSymbolsPerSlot])}, ...
    PilotSymbols={reshape(pskmod(randi([0 3], ...
      (nFFT-2*numGuardSc)*2,1),4,pi/4),numTx,1,[]) ...
      .* hadamard(numTx)});
pilotcfg.validate
[sym,ind] = pilotSignal(pilotcfg);
txGrid = zeros(pilotcfg.FFTLength - ...
    sum(pilotcfg.NumGuardBandCarriers), ...
    pilotcfg.NumSymbols, ...
    pilotcfg.NumTransmitStreams);
txGrid(ind) = sym;
refSignalforCFOEst = ofdmmod(txGrid(:,1,:), ...
    nFFT,cpLen,[1:numGuardSc nFFT-numGuardSc+1:nFFT]');
refSignalTail = ofdmmod(txGrid(:,end,:), ...
    nFFT,cpLen,[1:numGuardSc nFFT-numGuardSc+1:nFFT]');

Define parameters for an OFDM data signal (64 QAM for each data subcarrier) and pilot subcarriers (QPSK) for common phase error correction.

numDataSymbols = pilotcfg.NumSymbols - 2;
cpePilotIdx = [325 450 575 700]';
cpePilots = pskmod([0 1 2 3]',4,pi/4);

pfo = comm.PhaseFrequencyOffset(SampleRate=Fs, ...
    FrequencyOffsetSource="Input port", ...
    PhaseOffset=phaseOffset);
constDiag = comm.ConstellationDiagram( ...
    Title="Equalized signal with CPE compensation", ...
    ReferenceConstellation=qammod(0:63,64,UnitAveragePower=true), ...
    EnableMeasurements=true, ...
    Position=[100 100 720 600], ...
    AxesLimits=[-1.5 1.5]);

txBitsBuffer = [];
txQamOutputBuffer = [];
totalNumBitErrors = 0;
totalNumBits = 0;
txSlotIndex = 1;
rxSlotIndex = 0;
isRxSigFound = false;
while txSlotIndex <= totalNumSlots
    txBits = randi([0 1], ...
        6*(nFFT-2*numGuardSc-length(cpePilotIdx)), ...
        numDataSymbols, ...
        numTx);
    qamOutput = qammod(txBits,64, ...
        UnitAveragePower=true,InputType="bit");
    dataSignal = ofdmmod(qamOutput,nFFT,cpLen, ...
        [1:numGuardSc nFFT-numGuardSc+1:nFFT]', ...
        cpePilotIdx, ...
        repmat(cpePilots, ...
        [1 numDataSymbols pilotcfg.NumTransmitStreams]));
    txSignal = [refSignalforCFOEst; dataSignal; refSignalTail];

    txBitsBuffer = [txBitsBuffer; txBits];
    txQamOutputBuffer = [txQamOutputBuffer; qamOutput];

Filter signal through the MIMO channel.

    chanOut = mimochan(txSignal);

    if txSlotIndex == 1
        % Initial silent period (unknown to the receiver)
        silentPeriod = round(silentPeriodLength*symLen);
        chanOut = [zeros(silentPeriod,size(chanOut,2)); chanOut];
        % Set noise variance for the whole simulation
        noiseVar = var(txSignal(:))*10^(-SNRdB/10);
    end

Impair signal with CFO and phase offset.

    signalWithCFO = pfo(chanOut,actualCFOHz);

Add noise to the signal.

    rxSignal = signalWithCFO + randn(size(signalWithCFO),like=1j)*sqrt(noiseVar);

Receiver processing. Detect the first symbol in the frame.

    if ~isRxSigFound
        signalPower = var(dataSignal(:));
        index = 0;
        while isRxSigFound == false
            currentSym = rxSignal(index+(1:symLen),:);
            if var(currentSym(:)) >= 0.5*signalPower % Simple power detector
                isRxSigFound = true;
            else
                index = index + symLen;
            end
        end

Remove the silent period that has power less than 0.5*signalPower.

        truncatedSignal = rxSignal(index+1:end,:);

Use the next four symbols to estimate carrier frequency offset. These symbols are expected to include the reference signal.

        [estimatedCFO, tOffset] = ofdmCFOEstimate( ...
            truncatedSignal(1:4*symLen,:), ...
            nFFT, ...
            cpLen, ...
            ReferenceSignal=refSignalforCFOEst, ...
            Output="total", ...
            SearchRange=[-5 5], ...
            SubcarrierSpacing=scs);
        truncatedSignal = truncatedSignal(tOffset+1:end,:);

        fprintf("Actual carrier frequency offset = %.2f Hz\n", ...
            actualCFOHz);
        fprintf("Estimated carrier frequency offset = %.2f Hz\n", ...
            estimatedCFO);

        residualCFO = actualCFOHz - estimatedCFO;
        fprintf("Residual carrier frequency offset (compared with actual CFO) = %.4f Hz\n", ...
            residualCFO);

        cfoCompensator = comm.PhaseFrequencyOffset(SampleRate=Fs, ...
            FrequencyOffsetSource="Input port");
        rxInputBuffer = truncatedSignal;
    else
        rxInputBuffer = [rxInputBuffer; rxSignal];
    end

Process the receiver input buffer variable when it contains a complete slot of data. Compensate for the CFO in every slot but update the CFO estimate only once every ten slots.

    if size(rxInputBuffer,1) >= symLen*pilotcfg.NumSymbols
        rxSlotIndex = rxSlotIndex + 1;
        % Call ofdmCFOEstimate once every 10 slots
        if mod(rxSlotIndex,10) == 0
            estimatedCFO = ofdmCFOEstimate(rxInputBuffer(1:4*symLen,:), ...
                nFFT, ...
                cpLen, ...
                ReferenceSignal=refSignalforCFOEst, ...
                Output="total", ...
                SearchRange=round(estimatedCFO/scs) + [-2 2], ...
                SubcarrierSpacing=scs);
        end
        % Compensate for CFO
        rxDataSignal = cfoCompensator( ...
            rxInputBuffer(1:symLen*pilotcfg.NumSymbols,:), ...
            -estimatedCFO);
        % Flush the receiver buffer to prepare for the next slot of data
        rxInputBuffer(1:symLen*pilotcfg.NumSymbols,:) = [];

OFDM demodulation, channel estimation, and equalization. The symbol offset equals cpLen/2. By default the ofdmChannelEstimate function denoises the channel estimate.

        demodOutput = ofdmdemod(rxDataSignal, ...
            nFFT, ...
            cpLen, ...
            cpLen/2, ...
            [1:numGuardSc nFFT-numGuardSc+1:nFFT]');
        [hEst, noiseVarEst] = ofdmChannelEstimate( ...
            demodOutput,pilotcfg,cpLen);
        equalizedSignal = ofdmEqualize( ...
            demodOutput, ...
            reshape(hEst,[],size(hEst,3),size(hEst,4)), ...
            noiseVarEst);

Discard pilot signals, keeping the data signals only.

        equalizedSignal = equalizedSignal(:,2:end-1,:);

Compensate for common phase error by averaging over all transmit streams and the four subcarriers: [325 450 575 700]

        cpeCorrection = mean(cpePilots./equalizedSignal(cpePilotIdx-numGuardSc,:,:),[1 3]);
        rxQAMSignal = equalizedSignal .* cpeCorrection;

        cpeAngle = -unwrap(angle(cpeCorrection))';
        coeff = [(1:length(cpeAngle))' ones(size(cpeAngle))]\cpeAngle; % Fit a straight line
        residualCFO_cpe = (coeff(1)/symLen)/(2*pi)*Fs; % Calculate the residual CFO from the slope

Calculate MER and BER.

        rxQAMSignalNoPilots = rxQAMSignal(setdiff(1:(nFFT-2*numGuardSc),cpePilotIdx-numGuardSc),:,:);
        numBitErrors = nnz(qamdemod(rxQAMSignalNoPilots,64, ...
            UnitAveragePower=true,OutputType="bit") - ...
            txBitsBuffer(1:size(txBits,1),:,:));
        % Flush the transmit buffer to prepare for the next slot of data
        txBitsBuffer(1:size(txBits,1),:,:) = [];

        measuredMER = 10*log10(var(reshape( ...
            txQamOutputBuffer(1:size(qamOutput,1),:,:),[],1)) / ...
            var(reshape(rxQAMSignalNoPilots - ...
            txQamOutputBuffer(1:size(qamOutput,1),:,:), ...
            [],1)));
        % Flush the QAM output buffer to prepare for the next slot of data
        txQamOutputBuffer(1:size(qamOutput,1),:,:) = [];

        totalNumBitErrors = totalNumBitErrors + numBitErrors;
        totalNumBits = totalNumBits + numel(txBits);

Plot the constellation diagram and the channel estimates for the first symbol in this slot.

        for n = 1:pilotcfg.NumSymbols-2
            constDiag(reshape(rxQAMSignal(:,n,:),[],1))
        end

        for nTx = 1:numTx
            for nRx = 1:numRx
                plot(abs(hEst(:,1,nTx,nRx)))
                if nTx == 1 && nRx == 1
                    axis([0 700 0 6])
                    legendStr = {"TX " + nTx + ", RX " + nRx};
                    hold on
                else
                    legendStr{end+1} = "TX " + nTx + ", RX " + nRx;
                end
            end
        end
        hold off
        title("Slot " + rxSlotIndex)
        xlabel("Subcarrier")
        ylabel("Magnitude of channel estimate")
        legend(legendStr,Location="northeastoutside")
        grid on
        drawnow
        % Update the estimated CFO for the next slot
        estimatedCFO = estimatedCFO + residualCFO_cpe;

Save values for plotting.

        actCFOHz(txSlotIndex) = actualCFOHz;
        estCFO(txSlotIndex) = estimatedCFO;
        nBitErr(txSlotIndex) = numBitErrors;
        ttlBitErr(txSlotIndex) = totalNumBitErrors;
        ttlBits(txSlotIndex) = totalNumBits;
        BER(txSlotIndex) = totalNumBitErrors/totalNumBits;
    end

Add a small CFO change for each slot, bound the CFO drift, and increment the transmit slot index.

    actualCFOHz = actualCFOHz + cfoDelta*scs;
    if mod(txSlotIndex,500) == 0
        cfoDelta = -cfoDelta; % Keep the CFO drift bounded
    end
    txSlotIndex = txSlotIndex + 1;
end
Actual carrier frequency offset = 35184.00 Hz
Estimated carrier frequency offset = 35194.44 Hz
Residual carrier frequency offset (compared with actual CFO) = -10.4394 Hz

Figure contains an axes object. The axes object with title Slot 100, xlabel Subcarrier, ylabel Magnitude of channel estimate contains 8 objects of type line. These objects represent TX 1, RX 1, TX 1, RX 2, TX 1, RX 3, TX 1, RX 4, TX 2, RX 1, TX 2, RX 2, TX 2, RX 3, TX 2, RX 4.

slotIdx = 1:totalNumSlots;
semilogy(slotIdx,actCFOHz,1:totalNumSlots,estCFO)
legend("Actual CFO","Estimated CFO",Location="southeast")
ylabel("CFO (Hz)")
xlabel("Slot")

Figure contains an axes object. The axes object with xlabel Slot, ylabel CFO (Hz) contains 2 objects of type line. These objects represent Actual CFO, Estimated CFO.

Present total bit errors (left) and BER (right) per slot on a dual y-axis plot.

figure
yyaxis left
h1 = plot(slotIdx,ttlBitErr,"-o","LineWidth",1.2);
ylabel("Total Bit Errors")

yyaxis right
h2 = plot(slotIdx,BER,"-x","LineWidth",1.2);
ylabel("BER")

xlabel("Slot")
title("Total Bit Errors and BER per Slot")
grid on
legend([h1 h2],{"Total Bit Errors","BER"},Location="southeast")

Figure contains an axes object. The axes object with title Total Bit Errors and BER per Slot, xlabel Slot, ylabel BER contains 2 objects of type line. These objects represent Total Bit Errors, BER.

Input Arguments

collapse all

Received OFDM-modulated signal, specified as a column vector or matrix. The number of rows must be at least (nfft + cplen). The number of columns is the number of receive antennas.

Data Types: single | double
Complex Number Support: Yes

FFT length, specified as an integer scalar greater than or equal to 8. The FFT length must align with that of the OFDM-modulated signal.

Data Types: double

Cyclic prefix length of one OFDM symbol, specified as an integer scalar in the range [0, nfft].

Data Types: double

Name-Value Arguments

collapse all

Specify optional pairs of arguments as Name1=Value1,...,NameN=ValueN, where Name is the argument name and Value is the corresponding value. Name-value arguments must appear after other arguments, but the order of the pairs does not matter.

Example: ofdmCFOEstimate(rxSignal,nfft,cplen,SearchRange=[–4,4]) estimates the CFO with the subcarrier search range set to [–4,4].

Type of estimate, specified as "fractional", "half-subcarrier-spacing", or "total".

  • When Output is "fractional", the ReferenceSignal setting is ignored and the returned frequency estimate, fEst, is in the range [–0.5, 0.5].

  • When Output is "half-subcarrier-spacing" or "total", you must specify ReferenceSignal.

    • For "half-subcarrier-spacing", fEst is estimated by correlating rxSignal with the reference signal shifted in frequency at half subcarrier spacing steps within the search range specified by SearchRange. For example, if SearchRange is [–2, 3], fEst is –2, –1.5, –1, –0.5, 0, 0.5, 1, 1.5, 2, 2.5, or 3.

    • For "total", fEst is the total CFO estimate, considering the possibility that the CFO offset might be greater than half a subcarrier spacing.

For more information, see Algorithms.

Data Types: char | string

OFDM reference signal, specified as a column vector or matrix. The number of rows must be less than or equal to the first dimension of rxSignal. The number of columns is the number of transmit streams.

Dependencies

The setting for this argument applies when you set Output to "half-subcarrier-spacing" or "total".

Data Types: single | double
Complex Number Support: Yes

Subcarrier search range used when correlating rxSignal with the frequency-shifted reference signal, specified as a two-element vector [k1 k2]. k1 and k2 must be integers, and –floor(nfft / 2) ≤ k1 ≤ k2 ≤ ceil(nfft / 2) – 1.

Dependencies

The setting for this argument applies when you set Output to "half-subcarrier-spacing" or "total".

Data Types: double

Subcarrier spacing in Hz, specified as a scalar.

  • When SubcarrierSpacing is equal to 1, fEst is returned as a normalized CFO relative to the subcarrier spacing.

  • When SubcarrierSpacing is not 1, fEst is returned in Hz.

Data Types: double

Oversampling factor, specified as a scalar. The oversampling factor setting applies for both rxSignal and ReferenceSignal. The products (OversamplingFactor × nfft) and (OversamplingFactor × cplen) must both result in integers.

Data Types: double

Maximum number of symbols for averaging the cyclic prefix (CP) correlation when estimating a fractional CFO, specified as an integer scalar. For the default setting, inf, the function averages the CP correlation by using all available OFDM symbols.

Dependencies

The argument applies when you set Output to "fractional" or "total".

Data Types: double

Output Arguments

collapse all

CFO estimate, returned as a scalar with the same data type as rxSignal. SubcarrierSpacing determines whether fEst is returned in Hz.

Timing offset estimate in samples, returned as a nonnegative scalar. toffset indicates the number of samples from the start of the input signal, rxSignal, to the beginning of the reference signal found in rxSignal.

  • When you set Output to "fractional", tOffset indicates the number of samples from the start of the input signal, rxSignal, to the start of the first OFDM symbol found in rxSignal.

  • When you set Output to "half-subcarrier-spacing" or "total", you must provide the reference signal, ReferenceSignal, when calling the function. tOffset indicates the number of samples from the start of the input signal to the beginning of the reference signal found in rxSignal.

Compensated signal, returned with the same data type and dimension as rxSignal.

Tips

  • The input must contain at least one OFDM symbol and a CP.

  • The CFO is common across all receive antennas.

  • If you provide a reference signal, ReferenceSignal, it must occur somewhere in rxSignal. Otherwise, the CFO estimate might be unpredictable.

Algorithms

This function estimates the CFO for OFDM waveforms using a two-stage approach. Depending on the Output setting, ofdmCFOEstimate computes a coarse CFO on a half-subcarrier grid using reference-signal correlation, a fractional CFO using cyclic-prefix (CP) correlation, or the sum of both. The final output is scaled by SubcarrierSpacing.

Stage 1: Half Subcarrier Estimate

If Output is "half-subcarrier-spacing" or "total", the first stage estimates a coarse CFO by searching on a half-subcarrier grid.

  • The algorithm searches candidate CFO values spaced by 1/2 subcarrier shifts over the specified subcarrier search range, SearchRange. For each 1/2 subcarrier shift, the candidate shift is applied to the reference signal and the cross-correlation is computed between the received waveform and the shifted reference signal. The candidate frequency shift for the biggest correlation peak determines the CFO estimate in this stage.

Stage 2: Fractional Subcarrier Estimate

If Output is "fractional" or "total", the second stage estimates fractional CFOs by using CP correlation.

  • When you set Output to "total", the CFO estimate found in stage 1 is first used to remove part of the CFO from the received signal, before CP correlation is performed to obtain the fractional CFO.

  • The fractional CFO is estimated from CP correlation by multiplying the signal by a delayed, conjugated version offset by nfft samples, and applying a moving sum over cplen samples.

  • Averaging across symbols, the CP correlation metric is summed across MaxNumSymbolsForAveraging OFDM symbols separated by (nfft + cplen) samples.

  • The fractional CFO estimate is obtained from the phase of the peak correlation value.

MIMO Considerations

For MIMO configurations, these considerations apply:

  • When you input a MIMO signal, the metric sums the correlation magnitude-squared across all NRx × NTx pairs to produce a single metric.

  • To reduce run time, consider limiting number of frequency shifts by adjusting the subcarrier search range, SearchRange. For stage 1, the algorithm compares peaks in numFreqShifts correlations. Each correlation combines all correlations across NRx × NTx pairs.

    • The number of frequency shifts, numFreqShifts, = (k2 – k1 +1) × 2.

    • SearchRange specifies k1 and k2.

    • NRx is the number of receive antennas.

    • NTx in the number of transmit streams.

Extended Capabilities

expand all

C/C++ Code Generation
Generate C and C++ code using MATLAB® Coder™.

GPU Code Generation
Generate CUDA® code for NVIDIA® GPUs using GPU Coder™.

Version History

Introduced in R2026b