This note is based on my recent trial to decode and analyze LTE PHY signal. This is mainly to refresh my memory about the analysis process and method, but I hope it will be of help others as well.
It is not the program that process the signal real time. It was a script that I wrote in Python that process the baseband I/Q data captured from Amarisoft eNB.
NOTE : The baseband signal data (I/Q) data is captured with following condition.
- eNB and I/Q capturing device were connected with RF cable (mainly to reduce noise and get the best signal quality)
- LTE bandwidth 20 Mhz, SISO
- Sampling Rate : 30.72 MHz (Actually original capturing was done with 23.04 Mhz, but it was resampled by my script to 30.72 Mhz)
Followings are the list of procedure that I went through.
- SISO
- PSS detection
- Frequency Offset Estimation
- CP (Cyclic Prefix) Removal
- Resource Grid Reconstruction
- DC Removal
- SSS Detection
- PCI Calculation
- Channel Estimation
- H grid Construction
- Equalization
- MIMO 2x2
- Resource Grid Construction - Antenna 0
- Resource Grid Construction - Antenna 1
- H Grid Construction
- Equalization
- Precoding
- Large CDD
- Reference
SISO
One transmit antenna and one receive antenna is the case to work through first, because every step in it reappears in the MIMO section below. The order of the sub-sections is the order the script runs them, and each one depends on the one before it.
The chain starts with nothing but a stream of I/Q samples. It ends with a constellation that can be read off the screen. What happens in between is a sequence of increasingly specific guesses, each confirmed by the step that follows.
PSS detection
The first step to decode LTE PHY signal is to detect PSS and extract additional helper information. Personally, I think this would the most important step for LTE PHY signal processing because without this process you may not be able to decode any other signal.
This process involves multiple steps and each of those step can be a topic for separate note. For the details of nature of PSS itself and detailed algorithm to detect PSS, check out this note. In this note, I would just give you a big picture about result of PSS detection.
Following is a series of plots from my script with the detection of PSS.

PSS detection in one picture. The four lettered plots are the argument and the fourteen numbered ones are the evidence that the subframe boundary was found.
- Plot A is the frequency spectrum of the I/Q samples in dB, running to about 300000 on the frequency axis. The occupied band is the flat raised section in the middle.
- Plot B overlays Detected PSS in blue on Normalized PSS in orange. The blue points sit off the unit circle, which is the channel and the frequency error showing.
- Plot C is the generated PSS for NID2=0, and its points lie cleanly on the unit circle because the script computed them rather than received them.
- Plot D is the time domain magnitude with a vertical marker labelled Detected PSS at roughly sample 200000.
- The fourteen green numbered plots are one OFDM symbol each, 0 to 13, and all of them show the same occupied band. That consistency is what confirms the symbol boundaries were right.
[D] represents the absolute values of I/Q data. This is the plot of raw (unprocessed) data. The only process part on the plot is the part shown in orange color. It is the part on which the detected PSS is overlaid.
[A] is also a plot of non-processed data like [D]. The difference between [A] and [D] is difference between Time domain representation([D]) and Frequency Domain representation([A])
[B] is represent the detected signal (I/Q data) for PSS. The blue dots indicates the detected PSS as it is. Orange dots indicates the detected PSS projected onto the circle of radius 1. Since the blue dots does not give obvious visual correlation with the ideal PSS signal (a Zadoff sequence), I wanted to project it onto the circle like Zadoff sequence.
[C] is the ideal data that corresponds to the detected PSS data. It is the sequence generated by the script based on 3GPP spec and N_ID_2 (PSS sequence number) found during the PSS detection process.
[0]~[13] are the frequency domain plot for each OFDM symbol within the subframe where the PSS was found. Once PSS is found, we can figure out exact subframe and slot boundary of the subframe because PSS is always located in the same place (i.e, symbol 6 (7th OFDM symbol) in the subframe). With this fact, we can slice out each of OFDM data with exact start and end point. I cut out each OFDM symbols from the I/Q data and plot them in Frequency domain in dB scale.
Frequency Offset Estimation
One of the important information we can get from PSS detection is frequency error (frequency offset) which can be used to adjust (correct) frequency error of other signals (e.g, correcting frequency error of SSS).
Frequency offset can be calculated / estimated as follows)
def frequency_offset_estimation(received_pss, expected_pss):
phase_difference = np.angle(np.dot(received_pss, np.conj(expected_pss)))
frequency_offset = phase_difference / (2 * np.pi * 62 / SampleRate)
return frequency_offset
The details on each line of the code is as follows :
received_pss : an array of I/Q of the detected PSS in the form of complex number
expected_pss : an array of I/Q of the PSS generated by 3GPP algorithm
def frequency_offset_estimation(received_pss, expected_pss):
# Calculate the phase difference between the received PSS and expected PSS
# This is achieved by first taking the dot product of the received PSS and the conjugate of the expected PSS.
# Then, the angle (or phase) of this product is taken.
phase_difference = np.angle(np.dot(received_pss, np.conj(expected_pss)))
# Convert the phase difference into a frequency offset.
# The frequency offset is calculated by dividing the phase difference by the product of:
# 2 * pi (to convert from radians to cycles)
# The number of subcarriers comprising PSS (which is 62 for LTE)
# dividing by the sample rate to get the offset in Hz
frequency_offset = phase_difference / (2 * np.pi * 62 / SampleRate)
# Return the estimated frequency offset
return frequency_offset
NOTE : how phase_difference / (2 * np.pi * 62 / sample_rate) can indicate the frequency offset ?
Phase and frequency are directly related: the change in phase over time is frequency. In mathematical form, it can be represented as
dφ / dt = frequency.
In the frequency offset estimation function, the phase difference is divided by the time duration of the symbol (which is the number of subcarriers divided by the sample rate), giving us the rate of change of phase, or frequency offset.
In other words, the equation calculates the average change in phase per sample, which is then converted to Hz to represent the frequency offset. This frequency offset represents the difference in frequency between the received PSS and the expected PSS. This difference arises due to the Doppler effect or inaccuracies in the transmitter or receiver oscillators, among other reasons.
Once you get the proper frequency offset, you can compensate (correct) frequency error of other signals as follows.
signal : an array of I/Q data in the form of complex numbers.
frequency_offset : frequency offset in Hz
sample_rate : sample rate in samples / sec
def correct_frequency_offset(signal, frequency_offset, sample_rate):
signal = np.array(signal, dtype=np.complex128)
time = len(signal) / sample_rate
correction = np.exp(-1j * 2 * np.pi * frequency_offset * time)
corrected_signal = signal * correction
def correct_frequency_offset(signal, frequency_offset, sample_rate):
# Convert the input signal into a NumPy array of complex128 data type.
# This ensures the signal can be processed with complex arithmetic operations required for frequency correction.
signal = np.array(signal, dtype=np.complex128)
# Generate the time vector for the signal.
# Instead of creating a time array that corresponds to each sample point, the code calculates the
# total time duration of the signal by dividing its length by the sample rate.
time = len(signal) / sample_rate
# Calculate the complex exponential correction term.
# This term is used to shift the signal's frequency content.
# The negative sign in '-1j' ensures that we're shifting the frequency in the opposite direction
# of the detected offset, thereby correcting it.
# Multiplying by '2 * np.pi' converts the frequency offset to radians.
correction = np.exp(-1j * 2 * np.pi * frequency_offset * time)
# Apply the correction to the original signal by element-wise multiplication.
# This shifts the frequency content of the signal, thereby correcting the frequency offset.
corrected_signal = signal * correction
return corrected_signal
CP (Cyclic Prefix) Removal
Once you detected PSS and SSS, you are almost ready to reconstruct the resource grid (i.e, OFDM symbol vs Subcarrier). But before you trying to reconstruct the resource grid there is one more step to do. The time domain I/Q data carries a certain length of Cyclic Prefix (CP) data. You need to remove this part first before you reconstruct the resource grid.
The number of I/Q samples for CP varies depending on the LTE channel bandwidth as explained in this note.
Resource Grid Reconstruction
Once you have accuarate detection of timing and frequency boundary and removed CP properly, you can construct a LTE resource grid as follows. Once you have this kind of accurate resource grid, the retrieving and generating Physical layer data is something like reading and writing numbers in an Excel spreadsheet.

The reconstructed grid. Three labels mark the only parts whose position is known in advance, and finding them there is the check that the reconstruction worked.
- The vertical axis is Symbol 0 to Symbol 13, so this is one subframe of a normal cyclic prefix frame.
- SSS is marked on Symbol 5 and PSS on Symbol 6, which is the order they occupy in an FDD frame.
- PBCH is marked across Symbols 7 to 10, in the middle of the band.
- Symbol 0 is visibly darker than the rest. It carries the control region, which is modulated differently from the data.
DC Removal
There is still one more step to do after you reconstructed the ResourceGrid after CP removal. It is the process of an subcarrier at the center of the resource grid.
SSS Detection
Once you able to construct an accurate resource grid as shown above, the first thing you need to do is to detect SSS(Secondary Synchronized Signal). This process is done as in the following step :
i) Retrieve I/Q data from the resource elements for SSS from the Resource Grid
ii) Compensate the retrieved SSS I/Q with frequency offset obtained by PSS detection procedure.
iii) Generate all the possible SSS sequence that belong to the category for N_ID_2 (PSS sequence number)
iv) Compare (correlate) each and every SSS sequence from step iii) with the SSS IQ data from step ii) and find the best pair. The SSS sequence index (N_ID_1) that gives the best correlation is the detected SSS.
NOTE : The procedure for step iii) and iv) is explained in detail in this note.
Some highlights of SSS detection procedure are plotted below. (A) is the original IQ data retrieved from the resource grid(this corresponds to step i) mentioned above). (B) is the SSS data compensated by frequency offset (this corresponds to step ii) mentioned above). (C) is just one example of the generated SSS (this corresponds to step iii) mentioned above)

SSS detection in three columns. Reading left to right is reading the correction happening.
- The left column is sss_no_correction. Its constellation shows two clusters sitting at about 45 degrees off the real axis.
- The middle column is sss_corrected. The two clusters have rotated onto the real axis at plus and minus 1, and the imaginary part has collapsed to near zero.
- The right column is sss from N_ID_1, the sequence the script generated. It is two exact points and a flat zero imaginary part.
- The middle and right columns agreeing is the detection. SSS is BPSK, so a correct correction has to leave everything on the real axis.
PCI Calculation
Now you have detected PSS and SSS. With the PSS sequence index (N_ID_2) and SSS sequence index (N_ID_1), you can calculated Physical cell ID (PCI) as explained in this note.
Channel Estimation
One of the most important thing in decoding the received signal would be to estimate channel characteristics and correctly equalize the received signal using the estimated channel characteristics. In LTE, we use cell specific reference signal (CRS) to estimate the channel characteristics (channel coefficient). To do this, we need to have the received CRS and the ideal/expected CRS. The first step required for retrieving the received CRS and expected CRS is PCI which is already obtained in previous step.
Once you have the accurate PCI, the process of estimating channel coefficient goes as follows (NOTE : this is for SISO case for simplicity).
i) using the PCI, calculate the position of CRS. You can do this based on 3GPP specification explained in this note.
ii) then retrieve the I/Q data(complex number) from the CRS position in the resource grid (let's store all these retrieved data to a variable crs_rx).
iii) Now calculate the expected CRS for the specific PCI(let's store all these calculated data to a variable crs_ex). You can generate the expected CRS based on 3GPP specification explained in this note.
iv) Once you have the received crs (crs_rx) and the expected crs (crs_ex), you can estimate the channel coefficient just takding "crs_rx divided by crs_ex". (NOTE : this is a simplified way assuming that it is SISO and noise level is very low.) For futher study on this process, check out this note and this slide.
Following is an plots showing the result of some important steps described above.
- The column (A) shows the received CRS in step i). Each of the rows indicates different OFDM symbols. Symbol 0, 4, 7, 11 within a subframe (subframe 0 in this specific example).
- The column (B)/(C) shows the same data as in (A), but just different way. In these plots, the plots shows I and Q part of the crs for each data sample (each sample indicates different subcarrier position in the resource grid)
- The column (D) shows the expected CRS obtained by step iii). It shows only 4 dots, but this is the plot of 200 CRS data (this is the number of CRS in a specific symbol for 20 Mhz LTE, SISO).
- The column (E) shows the channel coefficient for each CRS symbols described in step iv).

Channel estimation, one row per CRS-bearing symbol. Column E is the division that the four columns before it set up.
- The rows are symNo 0, 4, 7 and 11, which are the symbols carrying CRS in a normal cyclic prefix subframe. Each row prints its own rsrp, around 24.0 in all four.
- Column A is the received CRS as a ring rather than points, because the channel has rotated each one differently.
- Columns B and C are the in-phase and quadrature sequences of that same data, plotted against index up to about 200.
- Column D is the expected CRS, four exact points at the QPSK positions, computed from the PCI.
- Column E is labelled H = Received CRS / Expected CRS, and the header prints that division explicitly. The result is the channel coefficient at each CRS position.
H grid Construction
Now you got the channel coefficients for every resource elements where CRS is located. For the proper equalization for every resource elements in the resource grid, we need to figure out the channel coefficient for all other resource elements where there is no CRS.
A common way to get the channel coefficient for every resource elements is interpolate the CRS channel coefficient in frequency and time domain and construct a resource grid filled out with the estimated (interpolated) channel coefficient. The plot for the channel coefficient resource grid (H resource grid) would be something like this.

The finished H grid as a heatmap. Every resource element now has a coefficient, and the vertical banding is the frequency selectivity of the channel.
The heatmap shown above may look fancy but would not give you much concrete meaning. Proabably plotting constellation for each OFDM symbol would give you more meaning / intuition of the channel as shown below.

The same grid plotted as I/Q. Each symbol traces an arc, which says the channel phase rotates steadily across the band.
- Every symbol shows an arc of radius about 2.5 rather than a filled disc. The magnitude is roughly constant and the phase is not.
- The arcs do not close into circles. The phase sweeps through less than a full turn across the occupied band.
- A small cluster sits near the origin in every plot. Those are the guard subcarriers and the DC position, where there is no signal to estimate from.
Just for a little bit different aspect (view point) of the channel coefficient for each resource elements, I plotted amplutude and phase plot for the H resource grid as follows. The smallest index on horizontal axis corresponds to the subcarrier at the lowest frequency.

The same information split into magnitude and phase. The lower plot is the more useful of the two.
- The magnitude plot sits between about 2.4 and 3.0 across the whole band, and all fourteen symbols lie almost on top of each other.
- The phase plot is a clean rising ramp from about -2 to about +3 radians, with the fourteen symbols stacked as parallel lines.
- A rising phase ramp across frequency is a timing offset. The slope is the size of it.
- The marked region past subcarrier 900 breaks up into vertical jumps. That is the wrap at plus and minus pi rather than a change in the channel.
Now let me show you how I got the full channel coefficient grid as shown above in step by step.
Step 1 : Populate H values into an empty resource grid
The first I did was to create an empty resource grid with same size as the data resource grid and the populate the H values for CRS at the proper CRS RE(Resource Element)s as shown below. You see that only CRS location has certain values (yellow / non-black) color and all other REs are empty (black)

Step 1 drawn. Only the CRS positions hold a value, and everything else is still empty.
- Four horizontal bands carry data, on symbols 0, 4, 7 and 11. The rest of the grid is black.
- The enlarged region at the foot shows the gaps clearly. Within a CRS symbol the values sit every sixth subcarrier, not on every one.
- The two visible gaps are the two interpolations that follow: across frequency within a symbol, then across time between symbols.
Step 2 : Frequency Domain Interpolation
Now fill in the gaps only in frequency domain. You can do this by interpolating the values (complex value) along frequency domain. You may apply various ways of interpolation, but I did it just by moving average. The result of the frequency domain interpolation looks as follows.

Step 2 drawn. The four CRS symbols are now filled right across the band, and the ten symbols between them are untouched.
- The four bands have become continuous stripes. The gaps every sixth subcarrier are gone.
- The enlarged region confirms it: within a stripe the colour now varies smoothly rather than alternating with black.
- Ten of the fourteen symbols are still black. Filling those is the time domain step.
Step 3 : Time Domain Interpolation
Now let's try to fill in the empty RE in time domain. Theoretically you can may use the same method as in frequency domain (moving average) or python package for the interpolation. But none of them work very good mainly because the number of points in time domain is too small. So I created my own interpolation function that can do the interpolation between only two end points. (NOTE : This is just for my own case, you may use different method of your own if you have any).
Here is broken down procedure of what I did
i) fill in symbol 1,2,3 by interpolating symbol 0 and 4 as end points
ii) fill in symbol 5,6 by interpolating symbol 4 and 7 as end points
iii) fill in symbol 8,9,10 by interpolating symbol 7 and 11 as end points
iv) fill in symbol 12, 13 by extrapolating symbol 9,10,11 (You may do this by interpolating the symbol 11 and symbol 0 of next subframe, but I did the extrapolation because I processed only one subframe with no next subframe).
Final result after all thse procedure, I get the resource grind as shown below.

Step 3 drawn, and the grid is complete. Every resource element now carries a coefficient.
- All fourteen symbol rows now carry colour. Nothing is black.
- The enlarged region shows smooth variation in both directions rather than the stripes of the previous step.
- The vertical structure survives the interpolation, which is what should happen. Interpolation fills gaps and does not invent frequency selectivity that was not measured.
Equalization
With the resource grid filled with channel coefficient for every resource element, now we are ready with equalizing every symbols (every resource elements) and recovering constellation.
The constellation before correction (i.e, before Equalization), it looks as below.

Before equalization. Thirteen of the fourteen symbols are a cloud with no readable structure at all.
Now let's compensate (equalize) the constellation with the channel coefficient. With the equalization, we can get the nicely aligned constellation as follows. The basic idea behind the equalization can be represented by a simple math as follows :
Corrected symbol (equalized symbol) = received symbol / channel coefficient

After equalization. The cloud has resolved into a regular square grid, and that grid is the whole point of everything above it.
- The corrected plots show an eight by eight arrangement of clusters, which is 64QAM. The cell was carrying its highest modulation during this capture.
- The axis range drops from about plus and minus 20 before to about plus and minus 10 after, because the channel gain has been divided out.
- OFDM symbol 0 resolves into four clusters rather than sixty four. The control region uses QPSK, so that is the correct result and not a failure.
- Comparing the two figures is the clearest before and after on the page. The same samples, the same axes, and the only difference is one division per resource element.
Equalization is one division : each received symbol is divided by the channel coefficient at its own resource element, which is why the H grid had to be complete first.The constellation is the test : nothing earlier in the chain proves the PCI, the timing or the interpolation were right, and a clean 64QAM grid proves all of them at once.Not every symbol should look the same : symbol 0 is QPSK control, and expecting 64QAM everywhere would read a correct result as a bug.
MIMO 2x2
Processing 2x2 MIMO signal is basically similar to the processing of SISO, but it just a little bit more complicated with some additional steps. The high level procedure that I went through is as follows :
i) Capture I/Q data from the two RX antenna (let's call the captured IQ data for each RX antenna as iq_rx_0 and iq_rx_1 respectively).
ii) Detect PSS and estimate frequency error from iq_rx_0
iii) Determine the IQ sample position corresponding to the start of the subframe where PSS is located (let's call this start IQ sample position as iq_subframe_start)
iv) Construct Resource Grid for iq_rx_0 (let's call this grid as re_grid_0)
v) Detect SSS from the re_grid_0
vi) Calculate PCI from the PSS and SSS
vii) Take out the IQ sequences of 1 subframe length from iq_rx_1 starting from iq_subframe_start obtained from iq_rx_0 processing
viii) Construct the resource grid from the IQ sequence obtained in step viii). Let's call this resource grid as re_grid_1.
ix) construct channel coefficient grid (H grid) from re_grid_0 and re_grid_1
x) Equalize the IQ data with the H grid
xi) Undo Precoding
xii) Undo Large CDD
Resource Grid Construction - Antenna 0
The construction of the resource grid for Antenna 0 is exactly same as the process used for SISO. Check out these for the details : PSS detection, Frequency Offset Estimation, CP (Cyclic Prefix) Removal, Resource Grid Reconstruction

Antenna 0 of the 2x2 capture. The same reconstruction as the SISO section produced, and the same two labels confirm it.
Resource Grid Construction - Antenna 1
Antenna 1 raises a question Antenna 0 never had to answer. The two receivers hear the same cell at the same moment, so the timing and the frequency offset found for Antenna 0 are already correct for Antenna 1. Whether to redo that work or reuse it is the choice below.
For the resource grid construction of Antenna 1 (RX 1), we may have a couple of different options as below
Option 1 : PSS detection, Frequency Offset Estimation, Finding the starting IQ sample position, CP (Cyclic Prefix) Removal, Resource Grid Reconstruction independently for RX antenna 1
Option 2 :

Antenna 1 of the same capture. PSS and SSS land on the same symbols and the same subcarriers, which is the check that the timing reused from antenna 0 was correct.
- Both grids mark SSS on Symbol 5 and PSS on Symbol 6, at the same position in the band.
- The two grids are not identical anywhere else. The two antennas see different channels, which is what makes 2x2 worth doing.
- Symbol 0 is dark in both, as it was in the SISO grid.
H Grid Construction
In case of SISO, the construction of channel coefficient grid is relatively simple as explained in here and here because the channel coefficient for each resource element is a scalar (a complex valued scalar). However, the channel coefficient for 2x2 MIMO is more complicated because the channel coefficient for each resource element is 2x2 matrix. How to get (construct) the 2x2 H matrix for each resource element from the two resource grid obtained in previous steps ?
It is difficult to explain it verbally. The best way I can do is to express it in illustration as follows. It may not be so clear to some of you even with this illustration. Just give some time to you and think about this until it become meaningful to you. Probably thinking about the concept of channel estimation itself from the beginning may help and check out this note if you want to revisit the concept of the channel estimation.

Why 2x2 channel estimation needs a drawing. Each antenna transmits CRS where the other one transmits nothing, and that is what makes the four coefficients separable.
- The two grids on the left are the CRS patterns for Antenna 0 and Antenna 1. Where one has a marked position, the other is labelled No data.
- That gap is the trick. When only Antenna 0 is transmitting on a resource element, whatever arrives at a receiver came through that antenna alone.
- The four bold arrows are the four paths, and they feed the four entries of H in the equation at the right.
- The equation is written y = Hx + n with the noise vector shown explicitly, which the SISO version of this page left out.
Based on the illustration shown above, I estimated channel coefficient for the resource elements of CRS (Cell specific Reference Signal) first as illustrated below. Since I have 4 elements in 2x2 Hmatrix, I got 4 resource grids showing the coefficient for the position of CRS as shown below.

The four channel coefficients at the CRS positions. The brightness difference between the rows is the result worth reading.
- h11 and h22 are the direct paths and they are visibly bright. The cross paths h21 and h12 are much darker.
- All four show the same four horizontal bands, because all four are estimated from the same CRS symbols.
- A weak cross term means low crosstalk between the two antennas, which is a good channel for spatial multiplexing.

The same four after interpolation. Every resource element now has all four coefficients, which is what equalizing a 2x2 signal needs.

The four coefficients plotted as I/Q, one column per symbol. The difference between the rows is much starker here than in the heatmaps.
- The h11 and h22 rows trace wide arcs in every column, the same shape the SISO H grid produced.
- The h21 and h12 rows are a single small dot near the origin in every column. Their magnitude is small enough that the phase hardly matters.
- Reading across the columns, the h11 and h22 arcs rotate gradually from s0 to s13. That is the channel changing slowly over the subframe.
- This one picture is the clearest statement of the channel condition in the capture: strong diagonal, weak off-diagonal.
Equalization
Equalizing a 2x2 capture divides each received symbol by the channel coefficient, exactly as the SISO section did. What it cannot do is separate the two layers, and the plots below show that limit clearly.

rx_p0 before equalization. Fourteen symbols, and not one of them shows anything a decoder could use.

rx_p1 before equalization. The second antenna looks the same as the first, which is expected when neither has been corrected yet.
- Both plots run to about plus and minus 20 on each axis, and both show rings rather than clusters. The channel has rotated every symbol by a different angle.
- OFDM symbol 0 differs from the rest in both, which is the control region carrying a different modulation.
- The rings are concentric rather than filled. That is the signature of a constellation whose amplitudes survive but whose phases have been scrambled.

Equalized rx_p0. The rings have collapsed into nine clusters, and the axis range has dropped from about 20 to about 5.

Equalized rx_p1. Nine clusters again, so both antennas are now readable and both are showing the same thing.
- Nine clusters in a three by three grid is what two summed QPSK streams look like. Each antenna port is carrying both layers at once.
- Nine is the number to notice. A single QPSK stream would give four clusters, and that is what the SISO section produced.
- OFDM symbol 6 stays disorderly in both plots. That is the symbol carrying PSS and SSS, which are not data and do not equalize to a data constellation.
- Equalization has removed the channel and nothing else. The layers are still added together, and separating them is what the next two sections do.
Equalization is per antenna, not per layer : it divides out the channel and leaves the sum of the layers exactly as it was.Nine clusters means two streams : the count of clusters is the quickest read of how many layers are still mixed together.The amplitude scale is the other clue : the axes shrink from about 20 to about 5 once the channel gain is divided out.
Precoding
The next matrix to undo is the precoding matrix W. The transmitter applied it to spread the layers across the antenna ports, so the receiver multiplies by its inverse. The result below is worth comparing with the equalized plots above.

What the transmitter did, drawn from the specification. The middle equation, y = W(i)D(i)Ux, is the one this capture has to undo.
- Layer 0 and Layer 1 enter the green Precoding box, and Antenna port 0 and Antenna port 1 leave it. Two in and two out makes W square here.
- The three equations are the three rules. Spatial multiplexing without CDD is y = W(i)x, large delay CDD adds D(i) and U, and transmit diversity has its own explicit matrix.
- The codebook at the right is 36.211 Table 6.3.4.2.3-1, the two antenna port table, with four codebook indices and columns for one and two layers.

inv(W).rx_p0_eq. Still nine clusters, so undoing W on its own has not separated the layers.

inv(W).rx_p1_eq. The same on the second antenna, and the same conclusion.
- The cluster count has not moved. It was nine after equalization and it is nine after inv(W), on both antennas.
- The clusters are tighter than the equalized plots, and the axis range has grown slightly. Undoing W has changed the geometry without changing how many points there are.
- This is the useful negative result of the page. W alone is not what combined the layers, so inverting it alone cannot take them apart.
Undoing W is necessary but not sufficient : the plots before and after it carry the same nine clusters.The transmission mode decides what else is in the product : this capture was made with large delay CDD, so D(i) and U are still in the way.
Large CDD
Two matrices are left, and undoing them is what finally separates the layers. D(i) applies a phase shift and U spreads the energy across the layers, so the receiver has to remove them in the reverse of the order the transmitter applied them.

36.211 Table 6.3.4.2.2-1 under the equation it belongs to. Three matrices multiply in a row and the notes say what each one is for.
- The leftmost note reads Precoding Matrix, this is to distribute the signal to each of physical antenna, and points at W(i).
- The middle note reads this is to apply phase shift and points at D(i), which is diagonal.
- The right note reads this is to distribute the energy among each layers and points at U, which is full rather than diagonal.
- The red boxes mark the 2 layer row, matching the annotation When number of Layer = 2. That is the row this capture uses.

inv(D).inv(W).rx_p0_eq. Nine clusters again, so D(i) was not the matrix doing the mixing either.

inv(D).inv(W).rx_p1_eq. Nine on the second antenna too. Only one matrix is left.
- D(i) is diagonal, so it can rotate each layer but cannot add one to another. Removing it therefore cannot change the cluster count, and the plots confirm it does not.
- Three steps have now been undone, and the constellation still shows two summed streams.

inv(U).inv(D).inv(W).rx_p0_eq. Nine clusters have become four, which is one QPSK stream on its own.

inv(U).inv(D).inv(W).rx_p1_eq. Four clusters on the second antenna as well, so both layers have come out separately.
- The cluster count drops from nine to four at this step and at no other. U is the matrix that added the layers together, so inverting it is what takes them apart.
- Four clusters is a single QPSK stream, which is what one layer carries. Reading the plot is now the same job as reading the SISO constellation.
- OFDM symbol 6 is still disorderly, and OFDM symbol 0 still shows its own pattern. Neither carries PDSCH data, so neither was ever going to resolve into four clusters.
- Running the three inversions in the order inv(W), then inv(D), then inv(U) reverses the transmitter’s W(i)D(i)U exactly.
U is where the layers are mixed : the cluster count falls from nine to four only when U is inverted, and nowhere else in the chain.The cluster count is the diagnostic : nine means two streams still summed, four means one stream recovered.The order of inversion is fixed : the transmitter applied W, then D, then U to the layer data, so the receiver removes them in that same left to right order.Three symbols never resolve : symbol 0 and symbol 6 carry control and synchronisation rather than PDSCH, so they are expected to look wrong.
Reference
- TS 36.211 v19.3.0 (Release 19) - E-UTRA Physical channels and modulation. Clause 6.11 Synchronization signals for PSS and SSS, clause 6.10.1 Cell-specific Reference Signal (CRS), and clause 6.3.4.2.2 with Table 6.3.4.2.2-1 for the large delay CDD matrices U and D(i).