GNU Radio's DVBS2RX Package
pl_frame_sync.h
Go to the documentation of this file.
1/* -*- c++ -*- */
2/*
3 * Copyright (c) 2021 Igor Freire.
4 *
5 * This file is part of gr-dvbs2rx.
6 *
7 * SPDX-License-Identifier: GPL-3.0-or-later
8 */
9
10#ifndef INCLUDED_DVBS2RX_PL_FRAME_SYNC_H
11#define INCLUDED_DVBS2RX_PL_FRAME_SYNC_H
12
13#include "cdeque.h"
14#include "delay_line.h"
15#include "pl_defs.h"
16#include "pl_submodule.h"
18#include <gnuradio/gr_complex.h>
19#include <chrono>
20
21/* correlator lengths, based on the number of differentials that we know in
22 * advance (25 for SOF and only 32 for PLSC) */
23#define SOF_CORR_LEN (SOF_LEN - 1)
24#define PLSC_CORR_LEN (PLSC_LEN / 2)
25
26namespace gr {
27namespace dvbs2rx {
28
29
31 searching, // searching for a cross-correlation peak
32 found, // found a peak but the lock is not confirmed yet
33 locked // lock confirmed
34};
35
36/**
37 * \brief Frame Synchronizer
38 *
39 * Searches for the start of PLFRAMEs by computing and adding independent
40 * cross-correlations between the SOF and the PLSC parts of the PLHEADER with
41 * respect to their expected values known a priori. The cross-correlations are
42 * based on the so-called *differential* metric given by `x[n]*conj(x[n+1])`,
43 * namely based on the angle difference between consecutive symbols. This
44 * non-coherent approach allows for frame synchronization despite the presence
45 * of large frequency offsets. That's crucial because the fine frequency offset
46 * estimation requires correct decoding of the PLSC, which, in turn, requires
47 * the frame search provided by the frame synchronizer. Hence, the frame
48 * synchronizer should act first, before the carrier recovery.
49 *
50 * Due to the interleaved Reed-Muller codeword construction (with a XOR given by
51 * the 7th PLSC bit), each pair of consecutive PLSC bits is either of equal or
52 * opposite bits. When the 7th PLSC bit is 0, all pairs of bits are equal, i.e.,
53 * `b[2i+1] = b[2i]`. In this case, the corresponding pair of scrambled bits are
54 * either equal to the original scrambler bits (when `b[2i] = b[2i+1] = 0`) or
55 * their opposite (when `b[2i] = b[2i+1] = 1`). In either case, the complex
56 * differential `x[2i]*conj(x[2i+1])` is the same due to the pi/2 BPSK mapping
57 * (refer to the even-to-odd mapping rules in the body of function
58 * `demap_bpsk_diff()` from `pi2_bpsk.cc`). Hence, when the 7th PLSC bit is 0,
59 * the differential is determined by the scrambler sequence, not the actual PLSC
60 * value that is unknown at this point.
61 *
62 * Next, consider the case when the 7th PLSC bit is 1 such that each pair of
63 * PLSC bits is composed of opposite bits, i.e., `b[2i+1] = !b[2i]`. In this
64 * case, if `b[2i] = 0` and `b[2i+1] = 1`, the pair of scrambled bits becomes
65 * `(s[2i], !s[2i+1])`. Otherwise, if `b[2i] = 1` and `b[2i+1] = 0`, the pair of
66 * scrambled bits becomes `(!s[2i], s[2i+1])`. In either case, the complex
67 * differential is equal to the differential due to the scrambler sequence
68 * alone, but shifted by 180 degrees (i.e., by `expj(j*pi)`, due to the pi/2
69 * BPSK mapping rules (again, see `demap_bpsk_diff()`). Thus, if the
70 * differentials due to the PLSC scrambler sequence are used as the correlator
71 * taps, the cross-correlation still yields a peak when processing the PLSC. The
72 * only difference is that the phase of the peak will be shifted by 180 degrees,
73 * but the magnitude will be the same. In this case, the 180-degree shift can be
74 * undone by taking the negative of the correlator peak.
75 *
76 * In the end, the PLSC correlator is implemented based on the scrambler
77 * sequence alone (known a priori), and it is independent of the actual PLSC
78 * embedded on each incoming PLHEADER. This correlator is composed of 32 taps
79 * only, given that only the pairwise PLSC differentials are known a priori. In
80 * contrast, the SOF correlator is based on all the 25 known SOF differentials,
81 * given that the entire 26-symbol SOF sequence is known a priori.
82 *
83 * The two correlators (SOF and PLSC) are expected to peak when they observe the
84 * SOF or PLSC in the input symbol sequence. The final timing metric is given by
85 * the sum or difference of these correlators, whichever has the largest
86 * magnitude. The sum (SOF + PLSC) peaks when the 7th PLSC bit is 0, and the
87 * difference (SOF - PLSC) peaks when the 7th PLSC bit is 1. That is, the
88 * difference metric essentially undoes the 180-degree shift on the PLSC
89 * correlator peak that would arise when the 7th bit is 1.
90 *
91 * Furthermore, as stated before, the implementation is robust to frequency
92 * offsets. The input symbol sequence can have any frequency offset, as long as
93 * it doesn't change significantly in the course of the PLHEADER, which is
94 * typically the case given that the PLHEADER is short enough (for typical
95 * DVB-S2 baud rates). If the frequency offset is the same for symbols x[n] and
96 * x[n+1], the differential metric includes a factor given by:
97 *
98 * ```
99 * exp(j*2*pi*f0*n) * conj(exp(j*2*pi*f0*(n+1))) = exp(-j*2*pi*f0).
100 * ```
101 *
102 * Moreover, if the frequency offset remains the same over the entire PLHEADER,
103 * all differentials include this factor. Ultimately, the cross-correlation peak
104 * is still observed, just with a different phase (shifted by `-2*pi*f0`). In
105 * fact, the phase of the complex timing metric (sum or difference between the
106 * correlator peaks) could be used to estimate the coarse frequency offset
107 * affecting the PLHEADER. However, a better method is implemented on the
108 * dedicated `freq_sync` class.
109 *
110 * Lastly, aside from the correlators, the implementation comprises a state
111 * machine with three states: "searching", "found", and "locked". As soon as an
112 * SOF is found, the state machine changes to the "found" state. At this point,
113 * the caller should decode the corresponding PLSC and call method
114 * `set_frame_len()` to inform the expected PLFRAME length following the
115 * detected SOF. Then, if the next SOF comes exactly after the informed frame
116 * length, the state machine changes into the "locked" state. From this point
117 * on, the frame synchronizer will check the correlation peak (i.e., the
118 * so-called "timing metric") at the expected index on every frame.
119 *
120 * Whenever the timing metric does not exceed a specific magnitude threshold,
121 * the implementation will increment an internal count for unlocking. After a
122 * chosen number of consecutive timing metric failures, this block will assume
123 * the frame lock has been lost and transition back to the "searching" state. At
124 * this point, it takes at least two more PLHEADERs to recover the lock, as the
125 * state machine needs to go over the "found" and "locked" states again.
126 */
128{
129private:
130 /* Parameters */
131 uint8_t d_unlock_thresh; /**< Number of frame detection failures before unlocking */
132
133 /* State */
134 uint32_t d_sym_cnt; /**< Symbol count since the last SOF */
135 gr_complex d_last_in; /**< Last input complex symbol */
136 float d_timing_metric; /**< Most recent timing metric */
137 uint32_t d_sof_interval; /**< Interval between the last two SOFs */
138 frame_sync_state_t d_state; /**< Frame timing recovery state */
139 uint32_t d_frame_len; /**< Current PLFRAME length */
140 uint8_t d_unlock_cnt; /**< Count of consecutive frame detection failures */
141 std::chrono::system_clock::time_point d_lock_time; /**< Frame lock timestamp */
142
143 delay_line<gr_complex> d_plsc_delay_buf; /**< Buffer used as delay line */
144 delay_line<gr_complex> d_sof_buf; /**< SOF correlator buffer */
145 delay_line<gr_complex> d_plsc_e_buf; /**< Even PLSC correlator buffer */
146 delay_line<gr_complex> d_plsc_o_buf; /**< Odd PLSC correlator buffer */
147 cdeque<gr_complex> d_plheader_buf; /**< Buffer to store the PLHEADER symbols */
148 volk::vector<gr_complex> d_payload_buf; /**< Buffer to store the PLFRAME payload */
149 volk::vector<gr_complex> d_sof_taps; /**< SOF cross-correlation taps */
150 volk::vector<gr_complex> d_plsc_taps; /**< PLSC cross-correlation taps */
151
152 /* Timing metric threshold for inferring a start of frame.
153 *
154 * When unlocked, use a conservative threshold, as it is important
155 * to avoid false positive SOF detection. In contrast, when locked,
156 * we only want to periodically check whether the correlation is
157 * sufficiently strong where it is expected to be (at the start of
158 * the next frame). Since it is very important not to unlock
159 * unnecessarily, use a lower threshold for this task. */
160 const float threshold_u = 30; /** unlocked threshold */
161 /* TODO: make this a top-level parameter */
162 const float threshold_l = 25; /** locked threshold */
163
164 /**
165 * \brief Cross-correlation between a delay line buffer and a given vector.
166 * \param d_line Reference to a delay line buffer with the newest sample at
167 * index 0 and the oldest sample at the last index.
168 * \param taps Reference to a volk vector with taps for cross-correlation.
169 * \param res Pointer to result.
170 * \note The tap vector should consist of the folded version of the target
171 * sequence (SOF or PLSC scrambler differentials).
172 */
173 void correlate(delay_line<gr_complex>& d_line,
174 volk::vector<gr_complex>& taps,
175 gr_complex* res);
176
177public:
178 /**
179 * @brief Construct a new frame sync object
180 *
181 * @param debug_level (int) Target debugging log level (0 disables logs).
182 * @param unlock_thresh (uint8_t) Number of consecutive frame detection
183 * failures before unlocking. A failure occurs when the timing metric does
184 * not exceed the expected magnitude threshold. By default, 3 failures will
185 * lead to unlocking.
186 *
187 * @note The number of consecutive timing metric failures before unlocking
188 * must be tuned to avoid unlocking prematurely under high noise, when the
189 * timing metric deviates significantly from the nominal peak of 57 for
190 * unit-energy symbols (57 due to the 26+32=57 correlator taps). On the
191 * other hand, this threshold parameter should not be very high to avoid too
192 * much delay in unlocking. For example, if a PLSC decoding error occurs and
193 * a wrong PLFRAME length is informed to the frame synchronizer, the timing
194 * metric observed after the wrong frame length will most certainly fail to
195 * exceed the threshold. In this scenario, all subsequent `unlock_thresh`
196 * frames will likely fail, as the frame synchronizer will search for their
197 * PLHEADERs in wrong indexes. Hence, in this example, it is better to
198 * unlock reasonably fast than to wait further.
199 */
200 frame_sync(int debug_level, uint8_t unlock_thresh = 3);
201
202 /**
203 * \brief Process the next input symbol.
204 * \param in (gr_complex &) Input symbol.
205 * \return (bool) Whether the input symbol consists of the last PLHEADER
206 * symbol, where the timing metric is expected to peak.
207 * \note This function should return true for the last PLHEADER symbol only.
208 * For all other symbols, it should return false.
209 */
210 bool step(const gr_complex& in);
211
212 /**
213 * \brief Set the current PLFRAME length.
214 *
215 * This information is used to predict when the next SOF should be
216 * observed. If a timing metric peak is indeed observed at the next expected
217 * SOF index, the synchronizer achieves frame lock.
218 *
219 * \param len (uint32_t) Current PLFRAME length.
220 */
221 void set_frame_len(uint32_t len);
222
223 /**
224 * \brief Check whether frame lock has been achieved
225 * \return (bool) True if locked.
226 */
227 bool is_locked() const { return d_state == frame_sync_state_t::locked; }
228
229 /**
230 * \brief Check whether frame lock has been achieved or a SOF has been found.
231 * \return (bool) True if locked or if at least a SOF has been found.
232 */
233 bool is_locked_or_almost() const { return d_state != frame_sync_state_t::searching; }
234
235 /**
236 * @brief Get the symbol count on the internal payload buffer
237 *
238 * @return uint32_t Current number of payload symbols buffered internally if locked.
239 */
240 uint32_t get_sym_count() const
241 {
242 return std::min(d_sym_cnt, (uint32_t)MAX_PLFRAME_PAYLOAD);
243 }
244
245 /**
246 * \brief Get the interval between the last two detected SOFs.
247 * \return (uint32_t) Interval in symbol periods.
248 */
249 uint32_t get_sof_interval() const { return d_sof_interval; }
250
251 /**
252 * \brief Get the PLHEADER buffered internally.
253 * \return (const gr_complex*) Pointer to the internal PLHEADER buffer.
254 */
255 const gr_complex* get_plheader() const { return &d_plheader_buf.back(); }
256
257 /**
258 * \brief Get the PLFRAME payload (data + pilots) buffered internally.
259 *
260 * The payload observed between consecutive SOFs is buffered internally. If
261 * a SOF is missed such that the last two observed SOFs are spaced by more
262 * than the maximum payload length, only up to MAX_PLFRAME_PAYLOAD symbols
263 * are buffered internally.
264 *
265 * \return (const gr_complex*) Pointer to the internal payload buffer.
266 */
267 const gr_complex* get_payload() const { return d_payload_buf.data(); }
268
269 /**
270 * \brief Get the SOF correlator taps.
271 * \return (const gr_complex*) Pointer to the SOF correlator taps.
272 */
273 const gr_complex* get_sof_corr_taps() const { return d_sof_taps.data(); }
274
275 /**
276 * \brief Get the PLSC correlator taps.
277 * \return (const gr_complex*) Pointer to the PLSC correlator taps.
278 */
279 const gr_complex* get_plsc_corr_taps() const { return d_plsc_taps.data(); }
280
281 /**
282 * \brief Get the last evaluated timing metric.
283 *
284 * Once locked, the timing metric updates only once per frame. Before that,
285 * it updates after every input symbol.
286 *
287 * \return (float) Last evaluated timing metric.
288 */
289 float get_timing_metric() const { return d_timing_metric; }
290
291 /**
292 * @brief Get the frame lock timestamp
293 *
294 * @return std::chrono::system_clock::time_point Timestamp in UTC time corresponding
295 * to when the frame synchronizer locked the frame timing. Valid only when locked.
296 */
297 std::chrono::system_clock::time_point get_lock_time() { return d_lock_time; }
298};
299
300} // namespace dvbs2rx
301} // namespace gr
302
303#endif /* INCLUDED_DVBS2RX_PL_FRAME_SYNC_H */
Definition cdeque.h:110
const T & back() const
Access the element at the back of the queue.
Definition cdeque.h:165
Fixed-size delay-line with contiguous storage of volk-aligned elements.
Definition delay_line.h:34
Frame Synchronizer.
Definition pl_frame_sync.h:128
std::chrono::system_clock::time_point get_lock_time()
Get the frame lock timestamp.
Definition pl_frame_sync.h:297
float get_timing_metric() const
Get the last evaluated timing metric.
Definition pl_frame_sync.h:289
const gr_complex * get_payload() const
Get the PLFRAME payload (data + pilots) buffered internally.
Definition pl_frame_sync.h:267
const gr_complex * get_sof_corr_taps() const
Get the SOF correlator taps.
Definition pl_frame_sync.h:273
const gr_complex * get_plheader() const
Get the PLHEADER buffered internally.
Definition pl_frame_sync.h:255
uint32_t get_sof_interval() const
Get the interval between the last two detected SOFs.
Definition pl_frame_sync.h:249
void set_frame_len(uint32_t len)
Set the current PLFRAME length.
const gr_complex * get_plsc_corr_taps() const
Get the PLSC correlator taps.
Definition pl_frame_sync.h:279
bool is_locked_or_almost() const
Check whether frame lock has been achieved or a SOF has been found.
Definition pl_frame_sync.h:233
bool step(const gr_complex &in)
Process the next input symbol.
frame_sync(int debug_level, uint8_t unlock_thresh=3)
Construct a new frame sync object.
bool is_locked() const
Check whether frame lock has been achieved.
Definition pl_frame_sync.h:227
uint32_t get_sym_count() const
Get the symbol count on the internal payload buffer.
Definition pl_frame_sync.h:240
Definition pl_submodule.h:25
#define DVBS2RX_API
Definition include/gnuradio/dvbs2rx/api.h:19
frame_sync_state_t
Definition pl_frame_sync.h:30
Fixed-length double-ended queue with contiguous volk-aligned elements.
Definition gr_bch.h:22
#define MAX_PLFRAME_PAYLOAD
Definition pl_defs.h:29