LTE Synchronization Constellations
Point a low-cost radio at a nearby cell tower, lock onto its timing signals, and find out which cell you are hearing.
Blocks used
Overview
Before a phone can decode anything from an LTE cell, it has to find the carrier, align to its symbol timing, and learn the cell's identity. Every downlink carries two known signals for this, the primary and secondary synchronization signals (PSS and SSS). They repeat every 5 ms and occupy the same 62 subcarriers around the center of the carrier, inside a 1.08 MHz band, whatever the channel bandwidth.[1] An LTE carrier can be up to 20 MHz wide,[2] far more than an RTL-SDR can capture without dropping samples,[3] but the synchronization signals fit comfortably in what it can.
This flowgraph captures the central 1.92 MHz of an LTE FDD downlink with normal cyclic prefix. A Python block written with NumPy finds the synchronization signals, estimates timing and frequency offset, identifies the physical cell, and equalizes both signals. Two constellation displays show the recovered symbols, next to a spectrum and waterfall fed directly by the radio. Running it needs an antenna for your local LTE downlink band, a strong LTE FDD cell within reach, and a radio supported by SoapySDR, such as an RTL-SDR.
Note
This flowgraph is adapted from the LTE downlink synchronization work by Daniel Estévez.
How it works
At 1.92 MS/s, one sixteenth of the LTE reference rate, an OFDM symbol is 128 samples with 15 kHz between subcarriers, and the normal cyclic prefix is 9 or 10 samples.[4] Each update holds eight batches of 4,096 samples, about 17.1 ms of signal, so it always contains at least three PSS transmissions. The Python block works in three steps.
Timing and fine frequency. The cyclic prefix repeats the end of its symbol, so correlating the signal with itself 128 samples later peaks at each symbol start. The phase of that peak gives the frequency offset within one subcarrier, up to 7.5 kHz either way.[5]
Primary signal. Candidate symbols go through a 128-point FFT and are compared with the three possible PSS sequences, one for each value of .[4] The block also tries shifts of up to 8 whole subcarriers, which widens the frequency search to about 127.5 kHz either way, and keeps only detections that repeat every 5 ms.
Secondary signal and cell identity. In an FDD cell the SSS sits in the symbol just before the PSS.[4] The block measures the channel on the known PSS, equalizes both symbols, and matches the SSS against every identity group . The best match also settles the whole-subcarrier offset and the subframe. The physical cell identity is
one of 504 values from 0 to 503.[4]
What to look for
The spectrum and waterfall show the 1.92 MHz window around the tuned frequency. A carrier of 3 MHz or wider fills the whole window with a flat OFDM spectrum.[2] The file is saved at 751 MHz, the center of the Band 13 downlink from 746 to 756 MHz.[2] Elsewhere, tune to the center of a local FDD carrier. Carrier centers sit on a 100 kHz raster,[2] and the frequency search is wide enough that a carrier one raster step away still locks.
On lock, the Python console prints the physical cell identity, the measured frequency offset, and the detection scores. The same values travel with both outputs as attributes. The offset includes the error of the RTL-SDR's own oscillator, and at 751 MHz every part per million adds 751 Hz.
The PSS display shows the received PSS with the known sequence removed, so a clean lock gathers near . Its channel estimate comes from the same symbol, so this cloud is a best case. The SSS display is the stronger test. The SSS is BPSK, so its points should split between and on the real axis.[1] Spread around those points comes from noise and from residual timing, frequency, and channel error. Each display keeps the last 496 points, eight symbols of 62 subcarriers, and clears after three updates without lock.
For the structure of the sequences and a full analysis of a real recording, see the sources below.
Going further
Logging the reported frequency offset over time and converting it to parts per million tracks the RTL-SDR's oscillator as it warms up. A Python block can also turn the SSS cloud into a number by computing the error vector magnitude against the decided symbols ,
Give the block one input and no outputs, connect it to the SSS output of the LTE sync block, and paste the code below. It skips updates without lock and the empty slots of a fresh history, and prints the result every 50 updates.
import numpy as np
_CALLS = 0
def compute(ctx):
global _CALLS
if not ctx.input_attrs[0].get("locked", False):
return
d = np.asarray(ctx.inputs[0]).reshape(-1)
d = d[np.abs(d) > 0]
if d.size == 0:
return
d_hat = np.sign(d.real)
evm = np.sqrt(np.sum(np.abs(d - d_hat) ** 2) / np.sum(np.abs(d_hat) ** 2))
_CALLS += 1
if _CALLS % 50 == 1:
print(f"SSS EVM: {100 * evm:.1f}%")
Frequency and gain can change freely. The sample rate must stay at 1.92 MS/s, because the symbol length and the 5 ms PSS period are written into the code, and each update must hold at least two PSS periods, just over 10 ms. The Python block reference explains how the block's inputs and outputs are declared.
The block handles only FDD with normal cyclic prefix. A TDD cell places the synchronization signals in different symbols, and an extended cyclic prefix moves every symbol boundary.[4] The code is written for readability rather than speed. If the displays stall, the block is falling behind the radio.
References
References
-
D. Estévez, "LTE downlink: synchronization signals," destevez.net, Apr. 2022. ↩ ↩2
-
Evolved Universal Terrestrial Radio Access (E-UTRA), User Equipment (UE) radio transmission and reception, ETSI, TS 136 101 V18.10.0 (3GPP TS 36.101 Release 18), Jul. 2025, Secs. 5.5, 5.6, and 5.7.2. ↩ ↩2 ↩3 ↩4
-
RTL-SDR Blog, "About RTL-SDR," RTL-SDR.com. rtl-sdr.com/about-rtl-sdr ↩
-
Evolved Universal Terrestrial Radio Access (E-UTRA), Physical channels and modulation, ETSI, TS 136 211 V18.0.2 (3GPP TS 36.211 Release 18), Aug. 2025, Secs. 6.11 and 6.12. ↩ ↩2 ↩3 ↩4 ↩5
-
J.-J. van de Beek, M. Sandell, and P. O. Börjesson, "ML estimation of time and frequency offset in OFDM systems," IEEE Trans. Signal Process., vol. 45, no. 7, pp. 1800-1805, Jul. 1997, doi:10.1109/78.599949. ↩