Skip to content

Synchronization algorithm (Xhonneux et al. 2021)

The receiver's synchronization stage is a faithful implementation of Algorithm 1 of M. Xhonneux, O. Afisiadis, D. Bol and J. Louveaux, A Low-Complexity LoRa Synchronization Algorithm Robust to Sampling Time Offsets, IEEE Internet of Things Journal, 2021 (arXiv:1912.11344). All code lives in softlora/sync.py; every estimator carries its equation number from the paper.

The paper models the received signal as

\[ y(t) = e^{j\,2\pi\,t\,\Delta f_c}\, x(t + \tau), \]

with a carrier frequency offset \(\Delta f_c = \frac{B}{N}\,(L_\text{CFO} + \lambda_\text{CFO})\) and a sampling time offset \(\tau = \frac{1}{B}\,(L_\text{STO} + \lambda_\text{STO})\). \(L_*\) and \(\lambda_*\) are the integer and fractional parts. The key property used throughout is that the two fractional offsets are not equivalent: the CFO adds a phase that is continuous across symbol boundaries, while the fractional STO resets to \(-2\pi\lambda_\text{STO}\) at each boundary (Eq. 8-9, Fig. 5).

Stage 1 — preamble detection and fractional CFO (Eq. 14)

stage1_detect_and_cfo slides N-sample windows, dechirps each with the reference downchirp and computes, for every window that has a predecessor,

\[ z^l = \sum_{p=-2}^{2} Y^l_{s+p}\,\overline{Y^{\,l-1}_{s+p}}, \]

with s the argmax bin. A preamble is declared when N_detect consecutive windows peak within one bin. The fractional CFO is then the phase of the sum of the last two bin-product vectors:

\[ \hat\lambda_\text{CFO} = \frac{\arg\!\big(z^l + z^{l-1}\big)}{2\pi}. \]

Because the CFO phase accumulates by \(2\pi\lambda_\text{CFO}\) per symbol, this estimate is independent of the STO (Section IV-A).

Stage 2 — first correction of the fractional STO (Eq. 20)

stage2_preliminary_sto dechirps the N_detect windows following detection with the fractional CFO removed (phase ramp \(e^{-j2\pi\hat\lambda_\text{CFO}\, n/N}\) with n the absolute sample index, so the three spectra add coherently) and averages them into Y_avg. \(\tilde s_\text{up}\) is taken from the first of those windows (Algorithm 1 step 12), giving \(\tilde M = N - \tilde s_\text{up}\), and the preliminary fractional STO is

\[ \tilde\lambda_\text{STO} = -\Re\!\left( \frac{e^{-j2\pi M/N}\,Y_{i+1} - e^{j2\pi M/N}\,Y_{i-1}} {2Y_i - e^{-j2\pi M/N}\,Y_{i+1} - e^{j2\pi M/N}\,Y_{i-1}} \right), \]

with \(M = \tilde M\) and \(i = \tilde s_\text{up}\). The paper then "realigns the receiver by \(\tilde\lambda_\text{STO}\) samples"; in this decoder that intermediate realignment is folded into the final timing correction (it cancels against the \(-\tilde\lambda_\text{STO}\) of Algorithm 1 step 22), so only the net correction is applied.

Stage 3 — integer offsets and definitive fractional STO (Eq. 17, 18, 20)

stage3_final_sync demodulates the final full preamble upchirp (window \(l+4\), i.e. U7) and the first SFD downchirp (window \(l + (N_\text{preamble\_up} - N_\text{detect}) + N_\text{netid} + 1\), which is \(l+8\) for the standard 8-upchirp preamble), again with the fractional CFO removed. The integer offsets follow from their peak bins (Eq. 15-16):

\[ \hat s_\text{up} = \arg\max_k |Y^\text{up}_k| = (L_\text{CFO} + L_\text{STO}) \bmod N \]
\[ \hat s_\text{down} = \arg\max_k |Y^\text{down}_k| = (L_\text{CFO} - L_\text{STO}) \bmod N \]
\[ \hat L_\text{CFO} = \tfrac{1}{2}\,\Gamma_N\!\big[(\hat s_\text{up} + \hat s_\text{down}) \bmod N\big] \]
\[ \hat L_\text{STO} = (\hat s_\text{up} - \hat L_\text{CFO}) \bmod N \]

The definitive fractional STO is Eq. 20 again, this time evaluated on the stored Y_avg with the exact \(\hat M = N - \hat L_\text{STO}\).

The receiver then removes the carrier offset by shifting the signal in frequency by \(-\frac{B}{N}(\hat L_\text{CFO} + \hat\lambda_\text{CFO})\) and slices the payload at

\[ \text{payload\_start} = \text{preamble\_start} + (N_\text{preamble\_up} + N_\text{netid} + N_\text{sfd\_down})\,N - \big(\Gamma_N(\hat L_\text{STO}) + \hat\lambda_\text{STO}\big) \]

which is the physical payload position implied by the paper's realignment steps (the paper's \(+N/4\) term is absorbed by the frame-origin convention of its window indexing). Eq. 18 leaves \(\hat L_\text{STO}\) in \([0, N)\), but a free-running scan has an arbitrary grid phase: a slightly negative STO comes back as \(\approx N\) and would push the payload a full symbol early. The decoder therefore unwraps it with \(\Gamma_N\) into \([-N/2, N/2)\) before subtracting (\(\text{total\_sto} = \Gamma_N(L_\text{STO}, N) + \lambda_\text{STO}\) in LoRaDecoder.sync).

Disambiguation of the N/2 boundary

When the estimated integer STO lands exactly on the wrap boundary \(\hat L_\text{STO} = N/2\), the single D1 window has demodulated at the upchirp peak (a sign that the receiver grid is misaligned with the frame and the D1 window straddles the net-id/SFD boundary). The decoder re-estimates the integer offsets from the second SFD downchirp (D2) to disambiguate (see LoRaDecoder.sync in decoder.py). This is a robustness measure of the scanning receiver; the estimators in sync.py are unchanged.

Preamble-length generalization

The paper assumes a standard 8-upchirp, 2-net-id preamble where D1 is window \(l+8\). Real captures (e.g. dataset/lora_capture2.dat) may carry more upchirps, so the D1 window is computed as \(l + (N_\text{preamble\_up} - N_\text{detect}) + N_\text{netid} + 1\), which reduces to \(l+8\) for the standard preamble and keeps the exact window indexing otherwise.

The final-upchirp window is likewise generalized. Algorithm 1 uses U7 (window \(l+4\)); the implementation uses \(l + \min(4,\, N_\text{preamble\_up} - N_\text{detect})\), which is U7 whenever the preamble has at least 7 upchirps (unchanged behavior) and the last upchirp otherwise. This makes preambles with 6 upchirps (e.g. 6 up + 2 sync + 2.25 SFD, a common short configuration) decode correctly — configure the decoder with N_preamble_up=6. Six upchirps is the practical minimum: stage 2 averages the U4-U6 windows, so fewer upchirps would leak a net-id symbol into the Y_avg estimate.

Validation

  • Estimator tests (tests/test_sync_paper.py): Eq. 19 is the \(\hat M = 0\) case of Eq. 20; Eq. 17/18 recover every \((L_\text{CFO}, L_\text{STO})\) pair; Eq. 14 recovers a noiseless fractional CFO exactly (also under an STO); Eq. 20 recovers a noiseless fractional STO; and the full pipeline decodes synthetic packets with injected integer/fractional CFO and STO.
  • gr-lora_sdr packets (scripts/gr_packet_test.py): packets generated by gr-lora_sdr's own TX (SF 7-10, code rates 1/2/4, with/without CRC, with and without injected offsets) decode with the correct payload and CRC; payloads are cross-checked against gr-lora_sdr's RX chain.
  • Over-the-air packets (scripts/ota_test.py): the 4 packets of dataset/lora_capture2.dat decode at the reference positions with "Hi from Shayan" and a valid CRC; gr-lora_sdr's RX reports the same payloads.