Sync
softlora.sync
Synchronization for a Nyquist-rate LoRa receiver.
This module implements the three-stage low-complexity synchronization algorithm of Xhonneux et al., "A Low-Complexity LoRa Synchronization Algorithm Robust to Sampling Time Offsets", IEEE Internet of Things Journal, 2021 (arXiv:1912.11344v2). Algorithm 1 of the paper is transcribed line-by-line; every estimator carries its equation number from the paper.
The three stages are:
stage1_detect_and_cfo-- preamble detection on three consecutive upchirps and estimation of the fractional CFO (Eq. 14).stage2_preliminary_sto-- preliminary estimation of the fractional STO from the three upchirps following detection (Eq. 20).stage3_final_sync-- definitive estimation of the integer CFO and STO (Eq. 17, 18) and of the fractional STO (Eq. 20), using the last full preamble upchirp (U7) and the first SFD downchirp (D1), followed by the frequency and time corrections.
The paper's "realign the receiver by lambda_tilde_STO" step of stage 2 is
folded into the final time correction: the net timing correction applied to
the received signal is L_STO + lambda_sto (the lambda_tilde_STO and
-lambda_tilde_STO terms of the paper's final realignment cancel), which
yields the same physical payload position while keeping every estimator exact.
The paper assumes a standard 8-upchirp preamble (N_preamble_up=8,
N_netid=2), in which case D1 is window l+8; the general formula
l + (N_preamble_up - N_detect) + N_netid + 1 is used so that preambles
with a different number of upchirps (e.g. the 10-upchirp OTA capture) are
handled identically.
apply_freq_correction(signal, L_CFO, lambda_cfo, N)
Remove the estimated carrier frequency offset from signal.
The total CFO in bins is L_CFO + lambda_cfo; the signal is shifted in
frequency by -(L_CFO + lambda_cfo) * BW / N.
Source code in softlora/sync.py
apply_time_shift(signal, shift_samples)
Delay signal by shift_samples samples (positive = later).
The integer part is applied by dropping/prepending samples and the
fractional part by a zero-padded FFT phase ramp. The padding absorbs the
circular wrap of the FFT delay so the interior of signal is delayed
exactly (a plain FFT phase ramp is circular and would alias the wrap back
into the signal, which badly distorts wideband chirps). A negative shift
advances the signal.
Source code in softlora/sync.py
cfo_bin_products(Y_cur, Y_prev, i, N)
Sum of the five DFT bin products centered on i (Eq. 14).
z = sum_{p=-2}^{2} Y_cur[(i+p) mod N] * conj(Y_prev[(i+p) mod N]).
For two consecutive unmodulated upchirps the phase of z equals
2*pi*lambda_CFO, the per-symbol phase accumulation of the fractional
CFO.
Source code in softlora/sync.py
est_integer_offsets(s_up, s_down, N)
Integer CFO and STO from the upchirp/downchirp peak bins (Eq. 17, 18).
.. math::
\hat L_{CFO} = \tfrac12 \Gamma_N[(\hat s_{up} + \hat s_{down}) \bmod N]
\hat L_{STO} = (\hat s_{up} - \hat L_{CFO}) \bmod N
with Gamma_N the wrap to [-N/2, N/2) and hat L_STO left in
[0, N) as in the paper.
Source code in softlora/sync.py
est_lambda_cfo(z_sum)
Fractional CFO in bins from the summed bin products (Eq. 14).
lambda_hat_CFO = angle(z) / (2*pi), in (-0.5, 0.5].
est_lambda_sto(Y, i, M_hat, N)
Fractional STO in samples from three DFT bins (Eq. 20).
.. math::
\hat\lambda_{STO} = -\Re\left(
\frac{e^{-j2\pi\hat M/N}Y_{i+1} - e^{j2\pi\hat M/N}Y_{i-1}}
{2Y_i - e^{-j2\pi\hat M/N}Y_{i+1} - e^{j2\pi\hat M/N}Y_{i-1}}
\right)
M_hat is an estimate of M = N - floor(L_STO + lambda_STO). With
M_hat = 0 the estimator reduces to the Jacobsen estimator of Eq. 19.
Source code in softlora/sync.py
stage1_detect_and_cfo(iq, downchirp, N, N_detect=3)
Stage 1: preamble detection and fractional CFO estimation.
Slides N-sample windows over iq, dechirps each with the reference
downchirp and computes z^l (Eq. 14) for every window that has a
predecessor. A preamble is declared when N_detect consecutive windows
peak within one FFT bin. The fractional CFO is then the phase of the sum
of the last two bin-product vectors (Eq. 14):
lambda_hat_CFO = angle(z^l + z^{l-1}) / (2*pi).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iq
|
ndarray
|
Complex baseband signal at fs = BW (N samples per symbol). |
required |
downchirp
|
ndarray
|
N-sample reference downchirp (conjugate of the upchirp). |
required |
N
|
int
|
Samples per symbol (2**sf). |
required |
N_detect
|
int
|
Number of consecutive matching windows required. |
3
|
Returns:
| Name | Type | Description |
|---|---|---|
l |
int or None
|
Index of the last window of the detected preamble run (None if no preamble was found). |
symbols |
list of int
|
Argmax bin of every processed window. |
fft_results |
list of ndarray
|
Dechirped window FFTs of every processed window. |
lambda_cfo |
float or None
|
Fractional CFO in bins (units of BW/N), or None if undetected. |
Source code in softlora/sync.py
stage2_preliminary_sto(iq, downchirp, N, l, lambda_cfo, N_detect=3)
Stage 2: first correction of the fractional STO.
Dechirps the N_detect windows following the detected preamble with the
fractional CFO removed (e^{-j2*pi*lambda_hat_CFO * n/N} with n the
absolute sample index) and averages their spectra. s_tilde_up is the
peak bin of the first of these windows (Algorithm 1 step 12), giving
M_tilde = N - s_tilde_up; the preliminary fractional STO is then
estimated on the averaged spectrum with Eq. 20.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iq
|
ndarray
|
Complex baseband signal at fs = BW. |
required |
downchirp
|
ndarray
|
N-sample reference downchirp. |
required |
N
|
int
|
Samples per symbol. |
required |
l
|
int
|
Last window index of the detected preamble (from stage 1). |
required |
lambda_cfo
|
float
|
Fractional CFO in bins (from stage 1). |
required |
N_detect
|
int
|
Number of windows to average. |
3
|
Returns:
| Name | Type | Description |
|---|---|---|
s_tilde_up |
int
|
Peak bin of the first post-detection window (used as the stage-2
approximation of |
lambda_sto_prelim |
float
|
Preliminary fractional STO in samples (Eq. 20 with |
Y_avg |
ndarray
|
CFO-corrected, averaged spectrum of the post-detection windows (used by stage 3 to compute the definitive fractional STO). |
Source code in softlora/sync.py
stage3_final_sync(iq, downchirp, N, l, lambda_cfo, Y_avg, N_preamble_up=8, N_netid=2, N_detect=3, down_shift=0)
Stage 3: integer CFO/STO and definitive fractional STO.
Demodulates the final full preamble upchirp (window l + up_shift:
U7 = l+4 when the preamble has at least 7 upchirps, the last upchirp
l + (N_preamble_up - N_detect) otherwise) and the first SFD downchirp
D1 (window l + (N_preamble_up - N_detect) + N_netid + 1, equal to
l+8 for the standard 8-upchirp preamble) with the fractional CFO
removed, resolves the integer offsets with Eq. 17/18, and computes the
definitive fractional STO with Eq. 20 using the averaged spectrum
Y_avg stored by stage 2 and M_hat = N - L_STO.
down_shift selects which SFD downchirp window is demodulated (0 = D1,
the paper's window; 1 = D2, used by the decoder to disambiguate an integer
STO falling exactly on the N/2 wrap boundary).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iq
|
ndarray
|
Complex baseband signal at fs = BW. |
required |
downchirp
|
ndarray
|
N-sample reference downchirp. |
required |
N
|
int
|
Samples per symbol. |
required |
l
|
int
|
Last window index of the detected preamble (from stage 1). |
required |
lambda_cfo
|
float
|
Fractional CFO in bins (from stage 1). |
required |
Y_avg
|
ndarray
|
Averaged spectrum from stage 2. |
required |
N_preamble_up
|
int
|
Number of preamble upchirps. |
8
|
N_netid
|
int
|
Number of network-identifier symbols before the SFD. |
2
|
N_detect
|
int
|
Preamble detection window count. |
3
|
down_shift
|
int
|
Offset (in symbols) from D1 of the downchirp window to demodulate. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
L_CFO |
int
|
Integer CFO in bins (Eq. 17). |
L_STO |
int
|
Integer STO in samples, in |
lambda_sto |
float
|
Definitive fractional STO in samples (Eq. 20). |
s_up |
int
|
Peak bin of the final preamble upchirp (U7, or the last upchirp for preambles with fewer than 7 upchirps). |
s_down |
int
|
Peak bin of the SFD downchirp (D1 or D2). |
M_hat |
int
|
|
Source code in softlora/sync.py
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | |