Engee documentation
Notebook

OFDM modulation: principles of operation and practical implementation*

Orthogonal Frequency Division Multiplexing (OFDM) is a modern modulation technology that has become the basis for many wireless communication standards, including Wi–Fi, LTE, and DVB digital television. Its key idea is to transform a high-speed serial data stream into a set of parallel low-speed streams, each of which is transmitted on a separate orthogonal subcarrier frequency. This approach fundamentally solves the problem of intersymbol interference (ISI) and resistance to frequency-selective fading, which are especially pronounced in multipath signal propagation channels typical of modern wireless systems.

This article discusses the full cycle of signal processing in the OFDM system – from the theoretical foundations to practical implementation in the Julia programming language. We will analyze the entire process step by step: starting with the generation of quadrature amplitude modulation (QAM) symbols, through the formation of an OFDM signal and modeling its passage through a distorting channel, and ending with reception, alignment, and demodulation, which allows you to restore the original data.

The problem that OFDM is designed to solve is particularly relevant for systems with a single carrier and high transmission rates. They require short character intervals and, as a result, wide bandwidth. When such a broadband signal passes through a multipath channel with frequency-selective properties, the pulse response of the channel can be stretched over time and superimposed on several subsequent symbols. This is the intersymbol interference, which leads to reception errors.

OFDM offers an elegant solution: instead of transmitting a single fast stream in a wide band, multiple slow streams are used in narrow orthogonal sub-bands. Since each such subchannel has a much lower character rate, the duration of each character increases significantly relative to the delay time in the channel. As a result, each symbol "sees" an almost flat (undistorted) frequency response of the channel within its narrow band, which practically eliminates intersymbol interference.

From a mathematical point of view, the OFDM signal is the sum of N orthogonal subcarriers modulated by independent data symbols. The discrete output of the modulator can be written as the inverse discrete Fourier transform (IDFT) from a vector of complex modulation symbols. In practice, the inverse fast Fourier Transform (IFFT) algorithm is used to efficiently calculate this transformation, which makes the OFDM implementation computationally efficient even with a large number of subcarriers in the hundreds or thousands.

Moving on to the implementation, the code below contains two demo functions that visualize the fundamental concepts of OFDM technology. According to the myoma of the main functions, there are auxiliary ones rectpulse and rectpuls used to form rectangular pulses of the required shape.

The first function, helperPlotMultipath(), clearly demonstrates the main problem that OFDM is designed to solve — intersymbol interference in a frequency-selective channel. It generates two signals: high-speed with a short character duration and low-speed with an extended character duration. The function simulates their passage through a multipath channel and plots them in both time and frequency domains. The time domain graph shows how the impulse response of the channel is superimposed on several symbols of the high-speed signal, causing their interference, while the low-speed signal remains undistorted due to the longer symbol. In the frequency domain, it becomes obvious that the bandwidth of a low-speed signal lies within an almost flat section of the channel's frequency response, whereas a broadband signal experiences significant distortion over its entire band.

The second function, helperPlotOFDM(), illustrates the key principle of OFDM — the orthogonality of subcarriers. It creates four rectangular pulses in the time domain, shifted in frequency by an amount equal to the symbolic speed. The function plots both the time shape of these pulses and their spectral characteristics. The graph in the frequency domain is especially important, which shows that the spectra of these pulses are arranged in such a way that the peak of each spectrum falls on the zero values of all the others. It is this orthogonality that allows subcarriers not to interfere with each other, despite the overlap of their spectra, which ensures efficient use of the frequency band.

In [ ]:
include("Helperplot.jl")
include("HeplerplotOFDM.jl")
display(helperPlotMultipath())
display(helperPlotOFDM())
(nothing, nothing)
(nothing, nothing)

The code below is a practical implementation of the basic OFDM modulator. The key stages of OFDM signal generation are performed sequentially in it. First, the components for working with the Fourier transform are initialized and the main parameters of the system are set: 16-QAM modulation is used, where each symbol encodes 4 bits of information, and the size of the Fourier transform is set to 128 points. Then a random integer generator is created, which generates a stream of 128 characters for transmission. These symbols are sent to the 16-QAM modulator, where each of them is displayed at the corresponding point on the complex plane. The modulation constellation is pre-normalized to provide a single average signal power. The next step is the core of OFDM signal generation. The resulting vector of complex symbols txgrid The inverse fast Fourier transform (IFFT) operation is applied to the input. Operations performed conj (complex conjugation) before and after the transformation is a technical feature of the implementation that ensures compliance with the mathematical definition of IFFT. The result is also scaled by dividing by the length of the transformation. The output is a temporary OFDM signal. txout, which is the sum of all 128 orthogonal subcarriers modulated by the corresponding QAM symbols.

The result is visualized by plotting the real part of the generated OFDM signal in the time domain, where each sample is displayed as a vertical column. This graph allows us to observe the characteristic multicomponent structure of the OFDM symbol, which is the result of the interference of multiple sinusoidal subcarriers with different amplitudes and phases specified by the modulation symbols.

In [ ]:
using EngeeComms
using EngeeDSP

var_fft = EngeeFFT();
var_fftshift = EngeeFFTshift();   
bps = 4;    # Number of bits per character
M = 2^bps;  # 16QAM
nFFT = 128; # Number of FFT cells

var_rand = RandomIntegerGenerator(SetSize=M,InitialSeed=1234,SampleTime=1.0,SamplesPerFrame=nFFT);
var_qam = GeneralQAMModulatorBaseband(Constellation=[-3+3im,-3+1im,-3-3im,-3-1im,-1+3im,-1+1im,-1-3im,-1-1im,3+3im,3+1im,3-3im,3-1im,1+3im,1+1im,1-3im,1-1im]./sqrt(10));
txsymbols = step!(var_rand,0.0);
txgrid = step!(var_qam,vec(txsymbols));
txout = conj(step!(var_fft,conj(txgrid)))/length(txgrid);
pl = plot([1:nFFT...],real(txout),linecolor = :blue, line=:stem,marker=:dot,)
Out[0]:

The following code snippet simulates the simplified process of receiving and demodulating an OFDM signal under ideal conditions. After the OFDM signal is generated txout it enters the communication channel. First, an additive white Gaussian noise (AWGN) channel model is created. Parameter EbNo=100 sets a very high signal-to-noise ratio, which practically corresponds to the noise-free conditions and allows you to isolate the operation of the algorithm from its influence. The generated OFDM signal is passed through this channel, and the received signal is received at the output. rxin. Then the key step of OFDM demodulation is performed — converting the received time signal back to the frequency domain using the direct fast Fourier transform (FFT). This operation, the reverse of the IFFT on the transmitter side, separates the orthogonal subcarriers and extracts the modulation symbols that were applied to them. Result rxgrid It is a vector of accepted complex symbols in frequency cells. Next, a 16-QAM demodulator is created, tuned to the same constellation that was used during the modulation. This demodulator decides which character has been transmitted by comparing each received complex count from rxgrid with the nearest point of the reference constellation. The output of the demodulator is a stream of integers rxsymbols. Finally, the correctness of the entire processing chain is checked. The original transmitted characters txsymbols compared with demodulated accepted symbols rxsymbols. Since the simulation is carried out in a virtually silent channel and without taking into account realistic effects such as multipath propagation or frequency offset, the demodulation result should be ideal. The message "Restored symbols correspond to transmitted symbols" confirms that the basic algorithms of modulation (QAM), transformation (IFFT/FFT) and demodulation work correctly in this simplified scheme. This serves as the basis for further complicating the model by adding real-channel effects, such as frequency-selective fading, which will require the introduction of additional processing steps, in particular, signal equalization.

In [ ]:
st = AWGN( EbNo=100, BitsPerSymbol=M, SignalPower=1);
rxin =  step!(st, txout);
rxgrid =step!(var_fft, rxin);
var_demod = GeneralQAMDemodulatorBaseband(Constellation=[-3+3im,-3+1im,-3-3im,-3-1im,-1+3im,-1+1im,-1-3im,-1-1im,3+3im,3+1im,3-3im,3-1im,1+3im,1+1im,1-3im,1-1im]./sqrt(10),OutType="Integer");
rxsymbols = step!(var_demod, vec(rxgrid));
if isequal(vec(txsymbols),vec(rxsymbols))
    display("The restored characters correspond to the transmitted characters.")
else
    display("The restored characters do not match the transmitted characters.")
end
"Восстановленные символы соответствуют переданным символам."

The code below sets the test input signal u1 and the impulse response of the channel h, and then visualizes them. This preparation is necessary to clearly demonstrate the key problem: linear convolution of a signal with a channel in systems without a cyclic prefix leads to inter-character interference, since the "tail" of the impulse response is superimposed on subsequent characters. Understanding this effect is the basis for the introduction of a cyclic prefix in OFDM, which transforms linear convolution into cyclic convolution, which in turn allows for simple alignment in the frequency domain.

In [ ]:
using EngeeDSP.Functions

u1 = [1:8...]; 
h = [0.4,1,0.4];

p1 = plot(u1,linecolor = :blue, line=:stem,marker=:dot, title = "The input signal");
ylims!(0,10);
xlims!(0,10);

p2 = plot(h,linecolor = :blue, line=:stem,marker=:dot,title ="Pulse response of the channel");
ylims!(0,2);
xlims!(0,10);
plots1 = plot(p1,p2,layout=(2,1),legend = false)
WARNING: using Functions.cos in module Main conflicts with an existing identifier.
WARNING: using Functions.sin in module Main conflicts with an existing identifier.
WARNING: using Functions.rectpuls in module Main conflicts with an existing identifier.
Out[0]:

The following code clearly demonstrates the difference between linear and cyclic convolution, a fundamental problem that OFDM solves. First, the linear convolution is calculated yl1 The signal u1 with a channel h, simulating the real distortion in the communication channel. Then, to get a cyclic convolution yc1 the vectors are padded with zeros to the same length and multiplied in the frequency domain via the FFT, after which the result is converted back to the time domain. The graph with two overlapping sequences clearly shows that the results of linear and cyclic convolution do not match. It is this discrepancy that causes intersymbol interference and explains why a cyclic prefix is added to OFDM: it artificially makes the convolution cyclic, which is a necessary condition for subsequent simple alignment in the frequency domain.

In [ ]:
yl1 = conv(u1,h);
yc1 = ifft(fft(u1).*fft([h;zeros(length(u1)-length(h))];));

p3 = plot(yl1,linecolor = :blue, line=:stem,marker=:dot, label = "Linear");
p4 = plot!(yc1,linecolor = :red, line=:stem,marker=:cross, label = "Cyclic");
p3
Out[0]:

Next, a solution to the problem shown earlier is demonstrated using the method underlying OFDM — adding a cyclic prefix (CP). Last L samples of the original signal u1 they are copied and added to the beginning of it, forming an extended signal. u2. When this signal, which already has a cyclic prefix, passes through the channel, its linear convolution with an impulse response h calculated using the FFT, gives the result yl2. After removing the prefix from the beginning yl2 The result is a sequence that completely matches the previously calculated cyclic convolution. yc1. The graph confirms this coincidence. Thus, the code clearly shows how the cyclic prefix magically transforms the linear convolution of a real channel into a cyclic one, which is the cornerstone of OFDM technology, which then makes it possible to simply and effectively compensate for channel distortions in the frequency domain.

In [ ]:
L = length(h);                          # Length of channel
N = length(u1);                         # Length of input signal
ucp = u1[N-L+1:N];                      # Use last samples of input signal as the CP
u2 = vcat(ucp,u1);                      # Prepend the CP to the input signal
yl2 = real(ifft(fft(u2).*fft([h;zeros(length(u2)-length(h))];)));   # Convolution of input+CP and channel
yl2 = yl2[L+1:end];                     # Remove CP to compare signals

p5 = plot(yl2,linecolor = :blue, line=:stem,marker=:dot, label = "Linear");
p6 = plot!(yc1,linecolor = :red, line=:stem,marker=:plus, label = "Cyclic");
p5
Out[0]:

The code below is a complete implementation of the transmitting part (modulator) of the OFDM system, which includes all the practical elements necessary for operation in a real channel. The key parameters are set: 16-QAM modulation, 128 subcarriers (FFT points) and a cyclic prefix with a length of 8 samples. The process begins with the generation of random symbols and their modulation into complex points of the 16-QAM constellation. Then, unlike the previous simplified example, a critical step is performed here — the formation of an OFDM symbol in the time domain using the inverse FFT and the subsequent addition of a cyclic prefix. A cyclic prefix is created by copying the last ones nCP counts of the generated OFDM symbol and adds this copy to its beginning. The result is a ready-to-transmit signal. txout, which already contains a protective interval that allows it to resist intersymbol interference in the multipath channel. This stage completes the preparation of the signal for transmission through a distorting medium.

In [ ]:
using EngeeComms
using EngeeDSP.Functions

var_fft = EngeeFFT();
var_fftshift = EngeeFFTshift();

bps = 4;    # Number of bits per character
M = 2^bps;  # The order of modulation
nFFT = 128; # number of FFT cells
nCP = 8;    # Length of the cyclic prefix
var_rand = RandomIntegerGenerator(SetSize=M,InitialSeed=1234,SampleTime=1.0,SamplesPerFrame=nFFT);
var_qam = GeneralQAMModulatorBaseband(Constellation=[-3+3im,-3+1im,-3-3im,-3-1im,-1+3im,-1+1im,-1-3im,-1-1im,3+3im,3+1im,3-3im,3-1im,1+3im,1+1im,1-3im,1-1im]./sqrt(10));
  
txsymbols = vec(step!(var_rand,0.0));
txgrid = step!(var_qam, txsymbols);
txout = conj(step!(var_fft,conj(txgrid)))/length(txgrid);
# To process multiple characters, vectorize the txout matrix
txout = txout[:];
txcp = txout[nFFT-nCP+1:nFFT];
txout = vcat(txcp,txout);

A realistic communication channel for transmitting a prepared OFDM signal is modeled below.

In [ ]:
hchan = [0.4 1 0.4]'[:,1];
st = AWGN( EbNo=100, BitsPerSymbol=M, SignalPower=1);

rxin = step!(st,txout)                    # Adding noise
rxin = conv(rxin,hchan);          # Adding frequency dependence
Out[0]:
138×1 Matrix{ComplexF64}:
 -0.004613386424821975 + 0.01018410397582413im
  0.013652038144603758 + 0.043197035894136454im
    0.0492270347950972 + 0.06620316556688652im
  0.001454364935108754 + 0.07097230525150806im
  -0.05289394861331751 + 0.05033647217286611im
  -0.10623842534926266 + 0.007631835760060518im
  -0.07220724444152067 + 0.0793883297034132im
  -0.08507639447568857 + 0.058457945333152114im
 -0.059006500236081214 + 0.02197075541671166im
 -0.027234998054386908 + 0.08887937404769576im
  -0.07888122025262403 + 0.18952011862455057im
 -0.008990523559159254 + 0.1256668429507964im
   0.11094712063193093 + 0.00907828481820107im
                       ⋮
   0.09460731132990707 - 0.0004812715748047959im
   0.07015399117915509 + 0.008909309963818066im
   -0.0371498347875724 + 0.02023774785437928im
 -0.013717251481083662 + 0.04775861884404914im
   0.04922799919124048 + 0.06620289608421998im
  0.001458269015173954 + 0.07096986997488722im
 -0.052889984395700716 + 0.050335079514633896im
  -0.10623688395213834 + 0.007637221385099306im
  -0.07220640713224899 + 0.07939103226752264im
  -0.08507420603847964 + 0.05846119723940281im
  -0.06888399812904436 + 0.012094766301959506im
 -0.016912783242377258 + 0.0017110779803630666im

The received signal is processed below — time synchronization and conversion to the frequency domain. First, a random time offset is generated, simulating the imperfection of synchronization between the transmitter and receiver. Then from the distorted signal rxin the cyclic prefix is removed, and the remaining useful part of the OFDM symbol is allocated to rxsync. The key step is to apply a direct FFT to this synchronized fragment, which converts the signal from a temporal representation back to a frequency representation, separating the orthogonal subcarriers and preparing the data for subsequent alignment and demodulation.

In [ ]:
var_rand1 = RandomIntegerGenerator(SetSize=nCP,InitialSeed=1234,SampleTime=1.0,SamplesPerFrame=1);
offset = Int64(step!(var_rand1,0.0)[1])      # the random offset is less than the length of the cyclic prefix
# Removing the cyclic prefix and synchronizing the received signal
# rxsync = rxin[nCP+1+1-offset:end];
rxsync = rxin[nCP+1:end];
rxgrid = step!(var_fft,rxsync[1:nFFT]);

The next block of code completes the receiving chain, performing the key operation of equalization and demodulation. If the equalizer is turned on, the frequency response of the channel is calculated first. hfchan using the FFT. Then the received symbols in the frequency domain rxgrid They are divided by this response, which compensates for the amplitude and phase distortions introduced by the channel. The resulting aligned characters rxgrideq They are fed to a 16-QAM demodulator, which converts them back into a stream of integers. The final check compares the demodulated characters with the original transmitted ones. When the system is working correctly with the alignment enabled, they must completely match, which confirms the effectiveness of OFDM in combination with a cyclic prefix to combat distortion in the frequency-selective channel.

In [ ]:
useEqualizer = true;                # Enabling and disabling equalization
if useEqualizer
    hfchan = step!(var_fft,vcat(hchan,zeros(128-length(hchan))))
    # Linear phase shift related to time displacement
    offsetf = exp.(-1im * 2*pi*offset * (0:nFFT-1)/nFFT);
    # rxgrideq = rxgrid ./ (hfchan .* offsetf);
    rxgrideq = rxgrid ./ (hfchan);
else # Errors occur without equalization
    rxgrideq = rxgrid;
end
var_demod = GeneralQAMDemodulatorBaseband(Constellation=[-3+3im,-3+1im,-3-3im,-3-1im,-1+3im,-1+1im,-1-3im,-1-1im,3+3im,3+1im,3-3im,3-1im,1+3im,1+1im,1-3im,1-1im]./sqrt(10),OutType="Integer");
rxsymbols = step!(var_demod, rxgrideq[:,1]);
if maximum(txsymbols - rxsymbols) < 1e-8
    display("The restored characters correspond to the transmitted characters.");
else
    display("The restored characters do not match the transmitted characters.")
end
"Восстановленные символы соответствуют переданным символам."

Output

This work is a comprehensive demonstration of the principles of operation and practical implementation of the OFDM system, covering all key aspects of the technology: from the theoretical justification of subcarrier orthogonality to solving practical problems of synchronization and alignment in a real communication channel. Special attention was paid to the fundamental role of the cyclic prefix, an element that transforms the linear convolution of a signal with a channel impulse response into a cyclic one. This transformation is a prerequisite for the application of simple and efficient alignment in the frequency domain. It has been experimentally confirmed that even in the presence of frequency-selective fading and time delays not exceeding the length of the cyclic prefix, the OFDM system is able to provide reliable recovery of transmitted data.

The presented implementation on the Engee platform using Julia clearly demonstrates not only the correct operation of OFDM algorithms, but also the practical advantages of modern computing environments for modeling in the field of communications. Such tools allow researchers and engineers to quickly prototype, test, and visualize complex signal processing algorithms, deepening their understanding of how they work. This model provides a solid foundation for further study of more complex aspects of modern communication systems, such as adaptive modulation, multiple access methods (OFDMA), or algorithms for estimating and tracking channel parameters.