Main Content

Massive MIMO Hybrid Beamforming

This example shows how to use hybrid beamforming at the transmit end of a massive MIMO communications system, using multi-user and single-user systems techniques. The example determines the channel state information at the transmitter by using full channel sounding. It partitions the required precoding into digital baseband and analog RF components, using different techniques for multi-user and single-user systems. The example computes EVM and BER communications system figures of merit to compare the signal recovered by simplified all-digital receivers to the multiple transmitted data streams.

In the example a scattering-based spatial channel technique models the transmit/receive spatial locations and antenna patterns. A simpler static-flat MIMO channel is also offered for link validation purposes.


The ever-growing demand for high data rate and more user capacity increases the need to use the available spectrum more efficiently. Multi-user MIMO (MU-MIMO) improves the spectrum efficiency by allowing a base station (BS) transmitter to communicate simultaneously with multiple mobile stations (MS) receivers using the same time-frequency resources. Massive MIMO allows the number of BS antenna elements to be on the order of tens or hundreds, thereby also increasing the number of data streams in a cell to a large value.

5G wireless systems use millimeter wave (mmWave) bands to take advantage of their wider bandwidth. The 5G systems also deploy large scale antenna arrays to mitigate severe propagation loss in the mmWave band.

The small mmWave band wavelengths allow an array to contain more elements within the same physical dimension. It becomes much more expensive to provide one transmit-receive (TR) module, or an RF chain, for each antenna element. Hybrid transceivers are a practical solution as they use a combination of analog beamformers in the RF and digital beamformers in the baseband domains, with fewer RF chains than the number of transmit elements [ 1 ].

Hybrid Beamforming

Hybrid beamforming (also known as hybrid precoding) is a method that enables the use of massive MIMO antenna arrays in a lower power and cost-efficient manner [ 1 ]. In a traditional antenna array, each antenna requires a dedicated RF chain to transmit and receive each data stream; with hybrid precoding, each stream requires a dedicated RF chain. This greatly reduces the number of RF chains, thus reducing cost and power. The analog outputs of each chain are combined into a network of analog RF gains and phase shifters (the analog RF beamformer, denoted Frf) that are connected to a large antenna array, where the number of antennas >> number of streams. These analog units cannot change weights quickly; however the computed RF weights change slowly over time since they are primarily determined by spatial positions of receivers. The digital baseband precoding weights (denoted Fbb) may change from symbol to symbol due to smaller-scale multipath effects, and may also be different from subcarrier to subcarrier to account for frequency-selective fading.

This example uses a single-user or multi-user MIMO-OFDM system to highlight the partitioning of the required precoding into its digital baseband and RF analog components at the transmitter end. The example uses the orthogonal matching pursuit (OMP) algorithm [ 3 ] for a single-user system and the joint spatial division multiplexing (JSDM) technique [ 2, 4 ] for a multi-user system, to determine the digital baseband Fbb and RF analog Frf precoding weights for the selected system configuration. Building on the system highlighted in the MIMO-OFDM Precoding with Phased Arrays example, this example shows the formulation of the transmit-end precoding matrices and their application to a MIMO-OFDM system.

Virtual Sectorization Using Joint Spatial Division Multiplexing

JSDM takes advantage of spatial clustering of users within the cell and groups these clusters to create virtual sectors via RF analog beamforming to these groups. The spatial covariance matrices of the groups are related to the spatial direction of the groups with respect to the base station antenna array. These matrices are computed using the channel estimates produced from the sounding signals, and the analog RF beams are derived from these matrices. These beams focus transmitted energy to the respective groups and minimize intergroup interference.

Once the beams are formed, the digital precoding weights are computed and used to orthogonalize users within each of the groups. These precoding weights are based on the user's channel state information (CSI) of the "effective channel" as seen from the output of the RF chains (not the output of the antenna array). Such beam-based measurements are done in 5G systems.

Because the analog RF gains and phase shifters are done at the antennas, it is implied that the RF weights apply across all subcarriers. However, the digital precoding weights may be applied per subcarrier. In this example, each subcarrier applies digital precoding weights as computed from the subcarrier's CSI.

s = rng(67);                  % Set RNG state for repeatability

System Parameters

Define system parameters for the example. Modify these parameters to explore their impact on the system.

prm.fc = 28e9;               % 28 GHz system
prm.chanSRate = 100e6;       % Channel sampling rate, 100 Msps
prm.ChanType = 'Scattering'; % Channel options: 'Scattering', 'MIMO'
prm.NFig = 8;                % Noise figure (increase to worsen, 5-10 dB)
prm.nRays = 500;             % Number of rays for Frf, Fbb partitioning

% Each user has the same modulation
prm.bitsPerSubCarrier = 4;          % 2: QPSK, 4: 16QAM, 6: 64QAM, 8: 256QAM
prm.numDataSymbols = 10;            % Number of OFDM data symbols

% Create a vector of users where each element is a user and the value of
% that element describes the number of independent data streams. For a
% single user case, make this a scalar value.
prm.numSTSVec = [4 3 2 4 3];        % Number of independent data streams per user
prm.numSTS    = sum(prm.numSTSVec); % Must be a power of 2
prm.numTx     = prm.numSTS*8;       % Number of BS transmit antennas (power of 2)

Assign each user to a group. Users are randomly arranged around the center of each group. For a single user use case, the concept of grouping is not applicable, and the user is randomly placed in the cell.

% The group assignments for the active users must be in ascending
% order. The default setting matches the group diagram above; there are two
% groups of users with the first three users assigned to group one and the
% last two users assigned to group two.
prm.groups    = [1 1 1 2 2];  % Describes which users are in which groups

Two JSDM options are available for simulation. This option determines the method for computing the digital precoding weights; the RF analog weights are calculated independent of which option is chosen.

  • 'JGP': Joint Spatial Division Multiplexing with Joint Group Processing (JGP). MIMO streams are precoded based on the CSI feedback of the channel estimates for all users.

  • 'PGP': Joint Spatial Division Multiplexing with Per-Group Processing (PGP). This is a special case where user CSI feedback is reduced to only send back the CSI of the streams transmitted to the user's group, thus increasing available bandwidth that was reserved for CSI feedback. Performance is slightly worse than JGP since precoding is done only on the streams for each group, and is particularly sensitive to the rank of the channel relative to the number of transmitted streams.

prm.jsdmType = 'JGP'; % JSDM option: 'JGP' or 'PGP' (for multi-user simulation)

Note that selecting the MIMO channel via the ChanType parameter field will cause sub-optimal performance for JSDM, since the MIMO channel is not location-aware. If using a scattering channel, add in an additional receiver in each group to improve the channel rank of the group's observed covariance matrix and support the number of streams being transmitted to each group. These receivers are not actively receiving data, but are considered connected to the base station as part of a group.

prm.numSTSVecAll = prm.numSTSVec;       % Number of independent data streams per user
prm.numUsers  = length(prm.numSTSVec);  % Number of active users
prm.numGroups = max(prm.groups);  % Number of groups
numGroups = prm.numGroups;
if strcmp(prm.ChanType,'Scattering')
    for g = 1:numGroups
        prm.groups(prm.numUsers+g) = g;
        prm.numSTSVecAll(prm.numUsers+g) = 2;

% Define the center of each group's range, azimuth, and elevation,
% assuming the BS is at the origin.
%   Angles specified as [azimuth;elevation] degrees
% Note: the number of columns must equal the number of groups
maxRange = 700;            % all MSs within 700 meters of BS
groupRanges = randi([1 maxRange],1,numGroups);
groupAzimuth = -60 + 60/(numGroups+1) + ...
    (120-(120/(numGroups+1)))/(numGroups-1) * (0:numGroups-1);
groupElevations = randi([-10 10],1,numGroups);

% Position mobile units
prm.numConnectedUsers = length(prm.numSTSVecAll); % Number of connected users
if prm.numUsers == 1
    % For a single user, randomly place the user
    % MS positions: assumes BS at origin
    %   Angles specified as [azimuth;elevation] degrees
    %   az in range [-180 180], el in range [-90 90], e.g. [45;0]
    prm.mobileRanges = randi([1 maxRange],1,prm.numConnectedUsers);
    prm.mobileAngles = [rand(1,prm.numConnectedUsers)*360-180; ...
    % For multiple users, randomly place the users within their assigned
    % group locations
    prm.mobileRanges = zeros(prm.numConnectedUsers,1);
    prm.mobileAngles = zeros(2,prm.numConnectedUsers);
    for uIdx = 1:prm.numConnectedUsers
        g = prm.groups(uIdx);
        prm.mobileRanges(uIdx)   = groupRanges(g) + 30*rand(1);
        prm.mobileAngles(:,uIdx) = ...
            [groupAzimuth(g)+3*rand(1); groupElevations(g)+3*rand(1)];

Define OFDM modulation parameters used for the system.

prm.FFTLength = 256;
prm.CyclicPrefixLength = 64;
prm.numCarriers = 234;
prm.NullCarrierIndices = [1:7 129 256-5:256]'; % Guards and DC
prm.PilotCarrierIndices = [26 54 90 118 140 168 204 232]';
nonDataIdx = [prm.NullCarrierIndices; prm.PilotCarrierIndices];
prm.CarriersLocations = setdiff((1:prm.FFTLength)', sort(nonDataIdx));
prm.numRx = prm.numSTSVecAll;   % Number of receive antennas, per connected user

numSTS = prm.numSTS;
numTx = prm.numTx;
numRx = prm.numRx;
numSTSVec = prm.numSTSVec;
numUsers = prm.numUsers;
codeRate = 1/3;             % same code rate per user
numTails = 6;               % number of termination tail bits
prm.numFrmBits = numSTSVec.*(prm.numDataSymbols*prm.numCarriers* ...
prm.modMode = 2^prm.bitsPerSubCarrier; % Modulation order
% Account for channel filter delay
numPadSym = 3;          % number of symbols to zero pad
prm.numPadZeros = numPadSym*(prm.FFTLength+prm.CyclicPrefixLength);

Define transmit and receive arrays and positional parameters for the system.

prm.cLight = physconst('LightSpeed');
prm.lambda = prm.cLight/prm.fc;

% Get transmit and receive array information
prm.numSTSVec = prm.numSTSVecAll; % get array info for all connected users
prm.numUsers = prm.numConnectedUsers;
[isTxURA,expFactorTx,isRxURA,expFactorRx] = helperArrayInfo(prm,true);
prm.numSTSVec = numSTSVec;             % restore parameters to active users
prm.numUsers = numUsers;

% Transmit antenna array definition
%   Array locations and angles
prm.posTx = [0;0;0];       % BS/Transmit array position, [x;y;z], meters
if isTxURA
    % Uniform Rectangular array
    txarray = phased.PartitionedArray(...
        'Array',phased.URA([expFactorTx numSTS],0.5*prm.lambda),...
    % Uniform Linear array
    txarray = phased.ULA(numTx, 'ElementSpacing',0.5*prm.lambda, ...
prm.posTxElem = getElementPosition(txarray)/prm.lambda;

spLoss = zeros(prm.numConnectedUsers,1);
prm.posRx = zeros(3,prm.numConnectedUsers);
for uIdx = 1:prm.numConnectedUsers

    % Receive arrays
    if isRxURA(uIdx)
        % Uniform Rectangular array
        rxarray = phased.PartitionedArray(...
            'Array',phased.URA([expFactorRx(uIdx) numRx(uIdx)], ...
            0.5*prm.lambda),'SubarraySelection',ones(numRx(uIdx), ...
        prm.posRxElem = getElementPosition(rxarray)/prm.lambda;
        if numRx(uIdx)>1
            % Uniform Linear array
            rxarray = phased.ULA(numRx(uIdx), ...
                'ElementSpacing',0.5*prm.lambda, ...
            prm.posRxElem = getElementPosition(rxarray)/prm.lambda;
            rxarray = phased.IsotropicAntennaElement;
            prm.posRxElem = [0; 0; 0]; % LCS

    % Mobile positions
    [xRx,yRx,zRx] = sph2cart(deg2rad(prm.mobileAngles(1,uIdx)), ...
                             deg2rad(prm.mobileAngles(2,uIdx)), ...
    prm.posRx(:,uIdx) = [xRx;yRx;zRx];
    [toRxRange,toRxAng] = rangeangle(prm.posTx,prm.posRx(:,uIdx));
    spLoss(uIdx) = fspl(toRxRange,prm.lambda);

Channel State Information

For a spatially multiplexed system, availability of channel information at the transmitter allows for precoding to be applied to maximize the signal energy in the direction and channel of interest. Under the assumption of a slowly varying channel, this is facilitated by sounding the channel first. The BS sounds the channel by using a reference transmission, that the MS receiver uses to estimate the channel. The MS transmits the channel estimate information back to the BS for calculation of the precoding needed for the subsequent data transmission.

The following schematic shows the processing for the channel sounding modeled.

For the chosen MIMO system, a preamble signal is sent over all transmitting antenna elements, and processed at the receiver accounting for the channel. The receiver antenna elements perform pre-amplification, OFDM demodulation, and frequency domain channel estimation for all links.

% Generate the preamble signal
prm.numSTS = numTx;             % set to numTx to sound out all channels
preambleSig = helperGenPreamble(prm);

% Transmit preamble over channel
prm.numSTS = numSTS;            % keep same array config for channel
prm.numUsers = prm.numConnectedUsers; % transmit sounding to all connected users
prm.numSTSVec = prm.numSTSVecAll; % transmit sounding to all connected users
[rxPreSig,chanDelay] = helperApplyMUChannel(preambleSig,prm,spLoss);
prm.numUsers = numUsers;
prm.numSTSVec = numSTSVec;

% Channel state information feedback
hDp = cell(prm.numConnectedUsers,1);
prm.numSTS = numTx;             % set to numTx to estimate all links
for uIdx = 1:prm.numConnectedUsers

    % Front-end amplifier gain and thermal noise
    rxPreAmp = phased.ReceiverPreamp( ...
        'Gain',spLoss(uIdx), ...    % account for path loss
        'NoiseFigure',prm.NFig,'ReferenceTemperature',290, ...
    rxPreSigAmp = rxPreAmp(rxPreSig{uIdx});
    %   scale power for used sub-carriers
    rxPreSigAmp = rxPreSigAmp * (sqrt(prm.FFTLength - ...

    % OFDM demodulation
    rxOFDM = ofdmdemod(rxPreSigAmp(chanDelay(uIdx)+1: ...
        end-(prm.numPadZeros-chanDelay(uIdx)),:),prm.FFTLength, ...
        prm.CyclicPrefixLength,prm.CyclicPrefixLength, ...

    % Channel estimation from preamble
    %       numCarr, numTx, numRx
    hDp{uIdx} = helperMIMOChannelEstimate(rxOFDM(:,1:numTx,:),prm);

prm.numSTS = numSTS;                 % revert back for data transmission
prm.numRx  = prm.numRx(1:numUsers);  % set numRx to number of active users

For a single-user system, the OMP partitioning algorithm is sensitive to the array response vectors At. Ideally, these response vectors account for all the scatterers seen by the channel, but these are unknown for an actual system and channel realization, so a random set of rays within a 3-dimensional space to cover as many scatterers as possible is used. The prm.nRays parameter specifies the number of rays.

For a multi-user system, the JSDM algorithm requires the calculation of the spatial covariance from each group to calculate the RF beamformer weights based on the block diagonalization method [ 5 ]. The channel estimate is fed back from each MS, and used by the BS to determine the precoding weights. The example assumes perfect feedback with no quantization or implementation delays. The effective channel is calculated from the RF beamformer weights and the CSI, and the digital baseband precoder weights are then calculated.

% Calculate the hybrid weights on the transmit side
if numUsers==1
    % Single-user OMP
    %   Spread rays in [az;el]=[-180:180;-90:90] 3D space, equal spacing
    %   txang = [-180:360/prm.nRays:180; -90:180/prm.nRays:90];
    txang = [rand(1,prm.nRays)*360-180;rand(1,prm.nRays)*180-90]; % random
    At = steervec(prm.posTxElem,txang);
    AtExp = complex(zeros(prm.numCarriers,size(At,1),size(At,2)));
    for carrIdx = 1:prm.numCarriers
        AtExp(carrIdx,:,:) = At; % same for all sub-carriers

    % Orthogonal matching pursuit hybrid weights
    [Fbb,Frf] = omphybweights(hDp{1},numSTS,numSTS,AtExp);

    v = Fbb;    % set the baseband precoder (Fbb)
    % Frf is same across subcarriers for flat channels
    mFrf = permute(mean(Frf,1),[2 3 1]);

    % Multi-user Joint Spatial Division Multiplexing

    % Get number of streams per group
    prm.numSTSGroupVec = zeros(1,numGroups);
    for g = 1:numGroups
        prm.numSTSGroupVec(g) = sum(prm.numSTSVecAll(prm.groups(1:prm.numUsers)==g));

    % Get Rg (group covariances)
    % Note that there is only one R per group (not per user). It is up to the
    % caller to determine how this autocorrelation matrix is obtained from the
    % group.
    hcov = helperGetCovariance(hDp,prm);

    % Calculate the analog beamformer weights
    Bg = jsdmrfweights(hcov,prm.numSTSGroupVec);

    % Pack beamformer weights into a precoding array
    mFrf = cat(1,Bg{:});

    % Calculate (predict) the effective channel response for each user from
    % a beamformed sounding signal
    hEff = helperGetHeff(hDp,mFrf,prm);

    % Get the digital (baseband) weights
    Fbb = jsdmbbweights(hEff,prm.numSTSGroupVec,Bg);

    if strcmp(prm.jsdmType,'PGP')
        % Pack the per group CSI into a matrix (block diagonal) for PGP
        v = zeros(prm.numCarriers,sum(numSTSVec),sum(numSTSVec));
        for g = 1:prm.numGroups
            stsIdx = sum(prm.numSTSGroupVec(1:g-1))+(1:prm.numSTSGroupVec(g));
            v(:,stsIdx,stsIdx) = Fbb{g};  % Nst-by-Nsts-by-Nsts
        % JGP
        v = Fbb;


Plot the transmit array pattern. For a scattering channel, the beam directions should correlate closely with the spatial positions of the users, and the beam magnitude should correlate with the number of streams being sent in that direction.

% Transmit array pattern plots
if isTxURA
    % URA element response for the first subcarrier
    pattern(txarray,prm.fc,-180:180,-90:90,'Type','efield', ...
            'ElementWeights',mFrf.'*squeeze(v(1,:,:)), ...
else % ULA
    % Array response for first subcarrier
    wts = mFrf.'*squeeze(v(1,:,:));
    pattern(txarray,prm.fc,-180:180,-90:90,'Type','efield', ...

For the wideband OFDM system modeled, the analog weights, mFrf, are the averaged weights over the multiple subcarriers. The array response pattern shows distinct data streams represented by the stronger lobes. These lobes indicate the spread or separability achieved by beamforming. The Introduction to Hybrid Beamforming example compares the patterns realized by the optimal, fully digital approach, with those realized from the selected hybrid approach, for a single-user system.

Data Transmission

Next, we configure the system's data transmitter. This processing includes channel coding, bit mapping to complex symbols, splitting of the individual data stream to multiple transmit streams, baseband precoding of the transmit streams, OFDM modulation with pilot mapping and RF analog beamforming for all the transmit antennas employed.

% Convolutional encoder
encoder = comm.ConvolutionalEncoder( ...
    'TrellisStructure',poly2trellis(7,[133 171 165]), ...

txDataBits = cell(numUsers, 1);
gridData = complex(zeros(prm.numCarriers,prm.numDataSymbols,numSTS));
for uIdx = 1:numUsers
    % Generate mapped symbols from bits per user
    txDataBits{uIdx} = randi([0,1],prm.numFrmBits(uIdx),1);
    encodedBits = encoder(txDataBits{uIdx});

    % Bits to QAM symbol mapping
    mappedSym = qammod(encodedBits,prm.modMode,'InputType','bit', ...

    % Map to layers: per user, per symbol, per data stream
    stsIdx = sum(numSTSVec(1:(uIdx-1)))+(1:numSTSVec(uIdx));
    gridData(:,:,stsIdx) = reshape(mappedSym,prm.numCarriers, ...

% Apply precoding weights to the subcarriers, assuming perfect feedback
preData = complex(zeros(prm.numCarriers,prm.numDataSymbols,numSTS));
for symIdx = 1:prm.numDataSymbols
    for carrIdx = 1:prm.numCarriers
        Q = squeeze(v(carrIdx,:,:));
        normQ = Q * sqrt(numTx)/norm(Q,'fro');
        preData(carrIdx,symIdx,:) = squeeze(gridData(carrIdx,symIdx,:)).' ...
            * normQ;

% Multi-antenna pilots
pilots = helperGenPilots(prm.numDataSymbols,numSTS);

% OFDM modulation of the data
txOFDM = ofdmmod(preData,prm.FFTLength,prm.CyclicPrefixLength,...
%   scale power for used sub-carriers
txOFDM = txOFDM * (prm.FFTLength/ ...

% Generate preamble with the feedback weights and prepend to data
preambleSigD = helperGenPreamble(prm,v);
txSigSTS = [preambleSigD;txOFDM];

% RF beamforming: Apply Frf to the digital signal
%   Each antenna element is connected to each data stream
txSig = txSigSTS*mFrf;

For the selected, fully connected RF architecture, each antenna element uses prm.numSTS analog gains, as given by the individual columns of the mFrf matrix.

The processing for the data transmission and reception modeled is shown below.

Signal Propagation

The example offers an option for spatial MIMO channel and a simpler static-flat MIMO channel for validation purposes.

The scattering model uses a single-bounce ray tracing approximation with a parametrized number of scatterers. For this example, the number of scatterers is set to 50. The 'Scattering' option models the scatterers placed randomly within a sphere around the receiver, similar to the one-ring model [ 6 ].

The channel models allow path-loss modeling and both line-of-sight (LOS) and non-LOS propagation conditions. The example assumes non-LOS propagation and isotropic antenna element patterns with linear or rectangular geometry.

% Apply a spatially defined channel to the transmit signal
[rxSig,chanDelay] = helperApplyMUChannel(txSig,prm,spLoss,preambleSig);

The same channel is used for both sounding and data transmission. The data transmission has a longer duration and is controlled by the number of data symbols parameter, prm.numDataSymbols. The channel evolution between the sounding and transmission stages is modeled by prepending the preamble signal to the data signal. The preamble primes the channel to a valid state for the data transmission, and is ignored from the channel output.

For a multi-user system, independent channels per user are modeled.

Receive Amplification and Signal Recovery

The receiver modeled per user compensates for the path loss by amplification and adds thermal noise. Like the transmitter, the receiver used in a MIMO-OFDM system contains many stages including OFDM demodulation, MIMO equalization, QAM demapping, and channel decoding.

hfig = figure('Name','Equalized symbol constellation per stream');
scFact = ((prm.FFTLength-length(prm.NullCarrierIndices))...
nVar = noisepow(prm.chanSRate,prm.NFig,290)/scFact;
decoder = comm.ViterbiDecoder('InputFormat','Unquantized', ...
    'TrellisStructure',poly2trellis(7, [133 171 165]), ...

for uIdx = 1:numUsers
    stsU = numSTSVec(uIdx);
    stsIdx = sum(numSTSVec(1:(uIdx-1)))+(1:stsU);

    % Front-end amplifier gain and thermal noise
    rxPreAmp = phased.ReceiverPreamp( ...
        'Gain',spLoss(uIdx), ...        % account for path loss
        'NoiseFigure',prm.NFig,'ReferenceTemperature',290, ...
    rxSigAmp = rxPreAmp(rxSig{uIdx});

    % Scale power for occupied sub-carriers
    rxSigAmp = rxSigAmp*(sqrt(prm.FFTLength-length(prm.NullCarrierIndices)) ...

    % OFDM demodulation
    rxOFDM = ofdmdemod(rxSigAmp(chanDelay(uIdx)+1: ...
        end-(prm.numPadZeros-chanDelay(uIdx)),:),prm.FFTLength, ...
        prm.CyclicPrefixLength,prm.CyclicPrefixLength, ...

    % Channel estimation from the mapped preamble
    hD = helperMIMOChannelEstimate(rxOFDM(:,1:numSTS,:),prm);

    % MIMO equalization
    %   Index into streams for the user of interest
    [rxEq,CSI] = ofdmEqualize(rxOFDM(:,numSTS+1:end,:),hD(:,stsIdx,:),'Algorithm','zf');

    % Soft demodulation
    rxSymbs = rxEq(:)/sqrt(numTx);
    rxLLRBits = qamdemod(rxSymbs,prm.modMode,'UnitAveragePower',true, ...

    % Apply CSI prior to decoding
    rxLLRtmp = reshape(rxLLRBits,prm.bitsPerSubCarrier,[], ...
    csitmp = reshape(CSI,1,[],1,numSTSVec(uIdx));
    rxScaledLLR = rxLLRtmp.*csitmp;

    % Soft-input channel decoding
    rxDecoded = decoder(rxScaledLLR(:));

    % Decoded received bits
    rxBits = rxDecoded(1:prm.numFrmBits(uIdx));

    % Plot equalized symbols for all streams per user
    scaler = 2;
    for i = 1:stsU
        subplot(numUsers, max(numSTSVec), (uIdx-1)*max(numSTSVec)+i);
        plot(reshape(rxEq(:,:,i)/sqrt(numTx), [], 1), '.');
        axis square
        xlim(gca,[-scaler scaler]);
        ylim(gca,[-scaler scaler]);
        title(['U' num2str(uIdx) ', G' num2str(prm.groups(uIdx)) ', DS' num2str(i)]);
        grid on;

    % Compute and display the EVM
    evm = comm.EVM('Normalization','Average constellation power', ...
        'ReferenceSignalSource','Estimated from reference constellation', ...
        'ReferenceConstellation', ...
    rmsEVM = evm(rxSymbs);
    disp(['User ' num2str(uIdx) ' (Group ' num2str(prm.groups(uIdx)) ')']);
    disp(['  RMS EVM (%) = ' num2str(rmsEVM)]);

    % Compute and display bit error rate
    ber = comm.ErrorRate;
    measures = ber(txDataBits{uIdx},rxBits);
    fprintf('  BER = %.5f; No. of Bits = %d; No. of errors = %d\n', ...
User 1 (Group 1)
  RMS EVM (%) = 0.00055073
  BER = 0.00000; No. of Bits = 12474; No. of errors = 0
User 2 (Group 1)
  RMS EVM (%) = 0.00056676
  BER = 0.00000; No. of Bits = 9354; No. of errors = 0
User 3 (Group 1)
  RMS EVM (%) = 0.0007622
  BER = 0.00000; No. of Bits = 6234; No. of errors = 0
User 4 (Group 2)
  RMS EVM (%) = 0.00086693
  BER = 0.00000; No. of Bits = 12474; No. of errors = 0
User 5 (Group 2)
  RMS EVM (%) = 0.00058205
  BER = 0.00000; No. of Bits = 9354; No. of errors = 0

For the MIMO system modeled, the displayed receive constellation of the equalized symbols offers a qualitative assessment of the reception. The actual bit error rate offers the quantitative figure by comparing the actual transmitted bits with the received decoded bits per user.

rng(s);         % restore RNG state

Conclusion and Further Exploration

The example highlights the use of hybrid beamforming for multi-user MIMO-OFDM systems. It allows you to explore different system configurations for a variety of channel models by changing a few system-wide parameters.

The set of configurable parameters includes the number of users, number of data streams per user, number of transmit/receive antenna elements, array locations, and channel models. Adjusting these parameters, you can study the parameters' individual or combined effects on the overall system. As examples, vary:

  • the number of users and corresponding data streams, prm.numSTSVec, and their corresponding group, prm.groups, to switch between multi-user and single-user systems, or

  • the channel type, prm.ChanType, or

  • the number of rays, prm.nRays, used for a single-user system, or

  • the JSDM feedback type, prm.jsdmType, and the group locations, groupAngles and groupRanges, used in the multi-user system.

Explore the following helper functions used by the example:

To see JSDM used in a 5G communications system, see the TDD Reciprocity-Based PDSCH MU-MIMO Using SRS (5G Toolbox) example.


  1. Molisch, A. F., et al. "Hybrid Beamforming for Massive MIMO: A Survey." IEEE® Communications Magazine, Vol. 55, No. 9, September 2017, pp. 134-141.

  2. Li Z., S. Han, and A. F. Molisch. "Hybrid Beamforming Design for Millimeter-Wave Multi-User Massive MIMO Downlink." IEEE ICC 2016, Signal Processing for Communications Symposium.

  3. El Ayach, Oma, et al. "Spatially Sparse Precoding in Millimeter Wave MIMO Systems." IEEE Transactions on Wireless Communications, Vol. 13, No. 3, March 2014, pp. 1499-1513.

  4. Adhikary A., J. Nam, J-Y Ahn, and G. Caire. "Joint Spatial Division and Multiplexing - The Large-Scale Array Regime." IEEE Transactions on Information Theory, Vol. 59, No. 10, October 2013, pp. 6441-6463.

  5. Spencer Q., A. Swindlehurst, M. Haardt, "Zero-Forcing Methods for Downlink Spatial Multiplexing in Multiuser MIMO Channels." IEEE Transactions on Signal Processing, Vol. 52, No. 2, February 2004, pp. 461-471.

  6. Shui, D. S., G. J. Foschini, M. J. Gans and J. M. Kahn. "Fading Correlation and its Effect on the Capacity of Multielement Antenna Systems." IEEE Transactions on Communications, Vol. 48, No. 3, March 2000, pp. 502-513.