MATLAB Examples

802.11ac Multi-User MIMO Precoding

This example shows the transmit and receive processing for a 802.11ac™ multi-user downlink transmission over a fading channel. The example uses linear precoding techniques based on a singular-value-decomposition (SVD) of the channel.



802.11ac supports downlink (access-point to station) multi-user transmissions for up to four users and up to eight transmit antennas to increase the aggregate throughput of the link [ 1 ]. Based on a scheduled transmission time for a user, the scheduler looks for other smaller packets ready for transmission to other users. If available, it schedules these users over the same interval, which reduces the overall time taken for multiple transmissions.

This simultaneous transmission comes at a higher complexity because successful reception of the individual user's payloads requires precoding, also known as transmit-end beamforming. Precoding assumes that channel state information (CSI) is known at the transmitter. A sounding packet, as described in the 802.11ac Transmit Beamforming example, is used to determine the CSI for each user in a multi-user transmission. Each of the users feed back their individual CSI to the beamformer. The beamformer uses the CSI from all users to set the precoding (spatial mapping) matrix for subsequent data transmission.

This example uses a channel inversion technique for a three-user transmission with a different number of spatial streams allocated per user and different rate parameters per user. The system can be characterized by the figure below.

The example generates the multi-user transmit waveform, passes it through a channel per user and decodes the received signal for each user to calculate the bits in error. Prior to the data transmission, the example uses a null-data packet (NDP) transmission to sound the different channels and determines the precoding matrix under the assumption of perfect feedback.

Simulation Parameters and Configuration

For 802.11ac, a maximum of eight spatial streams is allowed. A 6x6 MIMO configuration for three users is used in this example, where the first user has three streams, second has one, and the third has two streams allocated to it. Different rate parameters and payload sizes for up to four users are specified as vector parameters. These are indexed appropriately in the transmission configuration based on the number of active users.

s = rng(21);                             % Set RNG seed for repeatability

% Transmission parameters
chanBW      = 'CBW80';                   % Channel bandwidth
numUsers    = 3;                         % Number of active users
numSTSAll   = [3 1 2 2];                 % Number of streams for 4 users
userPos     = [0 1 2 3];                 % User positions for maximum 4 users
mcsVec      = [4 6 2 2];                 % MCS for maximum 4 users
apepVec     = [15120 8192 5400 6000];    % Payload, in bytes, for 4 users
chCodingVec = {'BCC', 'LDPC', 'LDPC', 'BCC'};  % Channel coding for 4 users

% Channel and receiver parameters
chanMdl       = 'Model-A';               % TGac fading channel model
precodingType = 'ZF';                    % Precoding type; ZF or MMSE
snr           = 38;                      % SNR in dB
eqMethod      = 'ZF';                    % Equalization method

% Create the multi-user VHT format configuration object, appropriately
% indexing into the vector values for the active users
if (numUsers==1)
    groupID = 0;
    groupID = 2;
numSTSVec = numSTSAll(1:numUsers);
numTx = sum(numSTSVec);
cfgVHTMU = wlanVHTConfig('ChannelBandwidth', chanBW,...
    'NumUsers', numUsers, ...
    'NumTransmitAntennas', numTx, ...
    'GroupID', groupID, ...
    'NumSpaceTimeStreams', numSTSVec,...
    'UserPositions', userPos(1:numUsers), ...
    'MCS', mcsVec(1:numUsers), ...
    'APEPLength', apepVec(1:numUsers), ...
    'ChannelCoding', chCodingVec(1:numUsers));

The number of transmit antennas is set to be the sum total of all the used space-time streams. This implies no space-time block coding (STBC) or spatial expansion is employed for the transmission.

Sounding (NDP) Configuration

For precoding, channel sounding is first used to determine the channel experienced by the users (receivers). This channel state information is sent back to the transmitter, for it to be used for subsequent data transmission. It is assumed that the channel varies slowly over the two transmissions. For multi-user transmissions, the same NDP (Null Data Packet) is transmitted to each of the scheduled users [ 2 ].

% VHT sounding (NDP) configuration, for same number of streams
cfgVHTNDP = wlanVHTConfig('ChannelBandwidth', chanBW,...
    'NumUsers', 1, ...
    'NumTransmitAntennas', numTx, ...
    'GroupID', 0, ...
    'NumSpaceTimeStreams', sum(numSTSVec),...
    'MCS', 0, ...
    'APEPLength', 0);

The number of streams specified is the sum total of all space-time streams used. This allows the complete channel to be sounded.

% Generate the null data packet, with no data
txNDPSig = wlanWaveformGenerator([], cfgVHTNDP);

Transmission Channel

The TGac multi-user channel consists of independent single-user MIMO channels between the access point and spatially separated stations [ 4 ]. In this example, the same delay profile Model-A channel is applied for each of the users, even though individual users can experience different conditions. The flat-fading channel allows a simpler receiver without front-end synchronization. It is also assumed that each user's number of receive antennas are equal to the number of space-time streams allocated to them.

Cell arrays are used in the example to store per-user elements which allow for a flexible number of users. Here, as an example, each instance of the TGac channel per user is stored as an element of a cell array.

% Create three independent channels
TGAC      = cell(numUsers, 1);
chanSeeds = [1111 2222 3333 4444];  % chosen for a maximum of 4 users
uIndex    = [10 5 2 1];             % chosen for a maximum of 4 users
chanDelay = zeros(numUsers, 1);
for uIdx = 1:numUsers
    TGAC{uIdx} = wlanTGacChannel(...
        'ChannelBandwidth', cfgVHTMU.ChannelBandwidth,...
        'DelayProfile', chanMdl, ...
        'UserIndex', uIndex(uIdx), ...
        'NumTransmitAntennas', numTx, ...
        'NumReceiveAntennas', numSTSVec(uIdx), ...
        'RandomStream', 'mt19937ar with seed', ...
        'Seed', chanSeeds(uIdx),...
        'SampleRate', wlanSampleRate(cfgVHTMU));
    chanInfo = info(TGAC{uIdx});
    chanDelay(uIdx) = chanInfo.ChannelFilterDelay;

The channels for each individual user use different seeds for random number generation. A different user index is specified to allow for random angle offsets to be applied to the arrival (AoA) and departure (AoD) angles for the clusters. The channel filtering delay is stored to allow for its compensation at the receiver. In practice, symbol timing estimation would be used.

% Append zeroes to allow for channel filter delay
txNDPSig = [txNDPSig; zeros(10, numTx)];

% Sound the independent channels per user for all transmit streams
rxNDPSig = cell(numUsers, 1);
for uIdx = 1:numUsers
    rxNDPChan = TGAC{uIdx}(txNDPSig);

    % Add WGN per receiver
    rxNDPSig{uIdx} = awgn(rxNDPChan, snr);

Channel State Information Feedback

Each user estimates its own channel using the received NDP signal and computes the channel state information that it can send back to the transmitter. This example uses the singular value decomposition of the channel seen by each user to compute the CSI feedback.

mat = cell(numUsers,1);
for uIdx = 1:numUsers
    % Compute the feedback matrix based on received signal per user
    mat{uIdx} = vhtCSIFeedback(rxNDPSig{uIdx}(chanDelay(uIdx)+1:end,:), ...
        cfgVHTNDP, uIdx, numSTSVec);

Assuming perfect feedback, with no compression or quantization loss of the CSI, the transmitter computes the steering matrix for the data transmission using either Zero-Forcing or Minimum-Mean-Square-Error (MMSE) based precoding techniques. Both methods attempt to cancel out the intra-stream interference for the user of interest and interference due to other users. The MMSE-based approach avoids the noise enhancement inherent in the zero-forcing technique. As a result, it performs better at low SNRs.

% Pack the per user CSI into a matrix
numST = length(mat{1});         % Number of subcarriers
steeringMatrix = zeros(numST, sum(numSTSVec), sum(numSTSVec));
%   Nst-by-Nt-by-Nsts
for uIdx = 1:numUsers
    stsIdx = sum(numSTSVec(1:uIdx-1))+(1:numSTSVec(uIdx));
    steeringMatrix(:,:,stsIdx) = mat{uIdx};     % Nst-by-Nt-by-Nsts

% Zero-forcing or MMSE precoding solution
if strcmp(precodingType, 'ZF')
    delta = 0; % Zero-forcing
    delta = (numTx/(10^(snr/10))) * eye(numTx); % MMSE
for i = 1:numST
    % Channel inversion precoding
    h = squeeze(steeringMatrix(i,:,:));
    steeringMatrix(i,:,:) = h/(h'*h + delta);

% Set the spatial mapping based on the steering matrix
cfgVHTMU.SpatialMapping = 'Custom';
cfgVHTMU.SpatialMappingMatrix = permute(steeringMatrix,[1 3 2]);

Data Transmission

Random bits are used as the payload for the individual users. A cell array is used to hold the data bits for each user, txDataBits. For a multi-user transmission the individual user payloads are padded such that the transmission duration is the same for all users. This padding process is described in Section 9.12.6 of [ 1 ]. In this example for simplicity the payload is padded with zeros to create a PSDU for each user.

% Create data sequences, one for each user
txDataBits = cell(numUsers, 1);
psduDataBits = cell(numUsers, 1);
for uIdx = 1:numUsers
    % Generate payload for each user
    txDataBits{uIdx} = randi([0 1], cfgVHTMU.APEPLength(uIdx)*8, 1, 'int8');

    % Pad payload with zeros to form a PSDU
    psduDataBits{uIdx} = [txDataBits{uIdx}; ...
        zeros((cfgVHTMU.PSDULength(uIdx)-cfgVHTMU.APEPLength(uIdx))*8, 1, 'int8')];

Using the format configuration, cfgVHTMU, with the steering matrix, the data is transmitted over the fading channel.

% Generate the multi-user VHT waveform
txSig = wlanWaveformGenerator(psduDataBits, cfgVHTMU);

% Transmit through per-user fading channel
rxSig = cell(numUsers, 1);
for uIdx = 1:numUsers
    % Append zeroes to allow for channel filter delay
    rxSig{uIdx} = TGAC{uIdx}([txSig; zeros(10, numTx)]);

Data Recovery Per User

The receive signals for each user are processed individually. The example assumes that there are no front-end impairments and that the transmit configuration is known by the receiver for simplicity.

A user number specifies the user of interest being decoded for the transmission. This is also used to index into the vector properties of the configuration object that are user-specific.

% Configure recovery object
cfgRec = wlanRecoveryConfig( ...
    'EqualizationMethod', eqMethod, 'PilotPhaseTracking', 'None');

% Get field indices from configuration, assumed known at receiver
ind = wlanFieldIndices(cfgVHTMU);

% Single-user receivers recover payload bits
rxDataBits = cell(numUsers, 1);
scaler = zeros(numUsers, 1);
spAxes = gobjects(sum(numSTSVec), 1);
hfig = figure('Name','Per-stream equalized symbol constellation');
for uIdx = 1:numUsers
    % Add WGN per receiver
    rxNSig = awgn(rxSig{uIdx}, snr);
    rxNSig = rxNSig(chanDelay(uIdx)+1:end, :);

    % User space-time streams
    stsU = numSTSVec(uIdx);

    % Estimate noise power in VHT fields
    lltf = rxNSig(ind.LLTF(1):ind.LLTF(2),:);
    demodLLTF = wlanLLTFDemodulate(lltf, chanBW);
    nVar = helperNoiseEstimate(demodLLTF, chanBW, sum(numSTSVec));

    % Perform channel estimation based on VHT-LTF
    rxVHTLTF  = rxNSig(ind.VHTLTF(1):ind.VHTLTF(2),:);
    demodVHTLTF = wlanVHTLTFDemodulate(rxVHTLTF, chanBW, numSTSVec);
    chanEst = wlanVHTLTFChannelEstimate(demodVHTLTF, chanBW, numSTSVec);

    % Recover information bits in VHT Data field
    rxVHTData = rxNSig(ind.VHTData(1):ind.VHTData(2),:);
    [rxDataBits{uIdx}, ~, eqsym] = wlanVHTDataRecover(rxVHTData, ...
        chanEst, nVar, cfgVHTMU, uIdx, cfgRec);

    % Plot equalized symbols for all streams per user
    scaler(uIdx) = ceil(max(abs([real(eqsym(:)); imag(eqsym(:))])));
    for i = 1:stsU
        subplot(numUsers, max(numSTSVec), (uIdx-1)*max(numSTSVec)+i);
        plot(reshape(eqsym(:,:,i), [], 1), '.');
        axis square
        spAxes(sum([0 numSTSVec(1:(uIdx-1))])+i) = gca; % Store axes handle
        title(['User ' num2str(uIdx) ', Stream ' num2str(i)]);
        grid on;

% Scale axes for all subplots and scale figure
for i = 1:numel(spAxes)
    xlim(spAxes(i),[-max(scaler) max(scaler)]);
    ylim(spAxes(i),[-max(scaler) max(scaler)]);
pos = get(hfig, 'Position');
set(hfig, 'Position', [pos(1)*0.7 pos(2)*0.7 1.3*pos(3) 1.3*pos(4)]);

Per-stream equalized symbol constellation plots validate the simulation parameters and convey the effectiveness of the technique. Note the discernible 16QAM, 64QAM and QPSK constellations per user as specified on the transmit end. Also observe the EVM degradation over the different streams for an individual user. This is a representative characteristic of the channel inversion technique.

The recovered data bits are compared with the transmitted payload bits to determine the bit error rate.

% Compare recovered bits against per-user APEPLength information bits
ber = inf(1, numUsers);
for uIdx = 1:numUsers
    idx = (1:cfgVHTMU.APEPLength(uIdx)*8).';
    [~, ber(uIdx)] = biterr(txDataBits{uIdx}(idx), rxDataBits{uIdx}(idx));
    disp(['Bit Error Rate for User ' num2str(uIdx) ': ' num2str(ber(uIdx))]);

rng(s); % Restore RNG state
Bit Error Rate for User 1: 0.00016534
Bit Error Rate for User 2: 0
Bit Error Rate for User 3: 0

The small number of bit errors, within noise variance, indicate successful data decoding for all streams for each user, despite the variation in EVMs seen in individual streams.

Conclusion and Further Exploration

The example shows multi-user transmit configuration, independent per-user channel modeling, and the individual receive processing using the channel inversion precoding techniques.

Further exploration includes modifications to the transmission and channel parameters, alternate precoding techniques, more realistic receivers and feedback mechanism incorporating delays and quantization.

A receiver function vhtSingleUserRxSigRec.m, which employs preamble field recovery to determine the transmitted configuration, is provided. This function assumes knowledge of only the channel bandwidth and transmission format and recovers other transmission parameters from the signaling fields of the VHT packet.


This example uses the following helper functions:

Selected Bibliography

  1. IEEE Std 802.11ac™-2013 IEEE Standard for Information technology - Telecommunications and information exchange between systems - Local and metropolitan area networks - Specific requirements - Part 11: Wireless LAN Medium Access Control (MAC) and Physical Layer (PHY) Specifications - Amendment 4: Enhancements for Very High Throughput for Operation in Bands below 6 GHz.
  2. Perahia, E., R. Stacey, "Next Generation Wireless LANS: 802.11n and 802.11ac", Cambridge University Press, 2013.
  3. IEEE Std 802.11™-2012 IEEE Standard for Information technology - Telecommunications and information exchange between systems - Local and metropolitan area networks - Specific requirements - Part 11: Wireless LAN Medium Access Control (MAC) and Physical Layer (PHY) Specifications.
  4. Breit, G., H. Sampath, S. Vermani, et al., "TGac Channel Model Addendum", Version 12. IEEE 802.11-09/0308r12, March 2010.