GNU Radio's DVBS2RX Package
qpsk.h
Go to the documentation of this file.
1/* -*- c++ -*- */
2/*
3 * Copyright (c) 2023 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_CONSTELLATIONS_H
11#define INCLUDED_DVBS2RX_CONSTELLATIONS_H
12
13#include "dvb_defines.h"
14#include "pl_defs.h"
15#include <gnuradio/gr_complex.h>
16#include <volk/volk_alloc.hh>
17
18namespace gr {
19namespace dvbs2rx {
20
21/**
22 * @brief QPSK Constellation
23 *
24 * Implements vectorized QPSK operations.
25 */
27{
28private:
29 bool d_volk_generic; /**< Whether the VOLK_GENERIC env var is set */
30 volk::vector<int8_t> d_aux_8i_buffer; /**< Auxiliary int8 buffer */
31 volk::vector<gr_complex> d_aux_32fc_buffer; /**< Auxiliary gr_complex buffer */
32
33 /**
34 * @brief Estimate the linear SNR of input QPSK symbols.
35 *
36 * @param in_syms Buffer containing the input QPSK symbols.
37 * @param ref_syms Buffer containing the reference QPSK constellation points.
38 * @param n_syms Number of QPSK symbols available on the input buffer.
39 * @return float Measured linear SNR.
40 */
41 float _estimate_snr(const gr_complex* in_syms,
42 const gr_complex* ref_syms,
43 unsigned int n_syms)
44 {
45 // Sum of the magnitude of the reference symbols
46 gr_complex norm_sq_ref_fc = 0;
47 volk_32fc_x2_conjugate_dot_prod_32fc(&norm_sq_ref_fc, ref_syms, ref_syms, n_syms);
48
49 // Sum of the magnitude of the noise samples
50 gr_complex* p_aux_buffer = d_aux_32fc_buffer.data();
51 volk_32f_x2_subtract_32f(reinterpret_cast<float*>(p_aux_buffer),
52 reinterpret_cast<const float*>(in_syms),
53 reinterpret_cast<const float*>(ref_syms),
54 n_syms * 2);
55 gr_complex norm_sq_noise_fc = 0;
56 volk_32fc_x2_conjugate_dot_prod_32fc(
57 &norm_sq_noise_fc, p_aux_buffer, p_aux_buffer, n_syms);
58
59 float norm_sq_ref_f = norm_sq_ref_fc.real();
60 float norm_sq_noise_f = norm_sq_noise_fc.real();
61 if (norm_sq_noise_f == 0)
62 norm_sq_noise_f = 1e-12;
63
64 return norm_sq_ref_f / norm_sq_noise_f;
65 }
66
67public:
68 /**
69 * @brief Construct a new Qpsk Constellation object.
70 */
72 : d_volk_generic(getenv("VOLK_GENERIC") != nullptr),
73 d_aux_8i_buffer(FRAME_SIZE_NORMAL),
74 d_aux_32fc_buffer(MAX_XFECFRAME_LEN)
75 {
76 }
77
78 /**
79 * @brief Destroy the Qpsk Constellation object.
80 */
82
83 /**
84 * @brief Map input bits to QPSK symbols.
85 *
86 * Supports mapping with the standard (normal) convention and an inverted convention
87 * that is useful for the slicing implementation.
88 *
89 * Standard convention:
90 * b1b0 -> Real + j*Imaginary
91 * 00 -> +sqrt(2)/2 + j*sqrt(2)/2
92 * 01 -> +sqrt(2)/2 - j*sqrt(2)/2
93 * 10 -> -sqrt(2)/2 + j*sqrt(2)/2
94 * 11 -> -sqrt(2)/2 - j*sqrt(2)/2
95 *
96 * Note: the MSB b1 is tied to the real part and the LSB b0 to the imaginary part. The
97 * real part is positive for b1=0 and negative for b1=1. Likewise, the imaginary part
98 * is positive for b0=0 and negative for b0=1.
99 *
100 * Inverted convention:
101 * b1b0 -> Real + j*Imaginary
102 * 00 -> -sqrt(2)/2 - j*sqrt(2)/2
103 * 01 -> -sqrt(2)/2 + j*sqrt(2)/2
104 * 10 -> +sqrt(2)/2 - j*sqrt(2)/2
105 * 11 -> +sqrt(2)/2 + j*sqrt(2)/2
106 *
107 * The difference in the inverted convention is that bit=1 is mapped to a positive
108 * value (+sqrt(2)/2) and bit=0 to a negative value (-sqrt(2)/2) instead of the other
109 * way around.
110 *
111 * @param out_buf Buffer where the mapped symbols should be stored.
112 * @param in_bits Buffer containing the input bits on unpacked int8 values.
113 * @param n_bits Number of bits to map.
114 * @param inv_convention Whether to use the inverted mapping convention.
115 */
116 void map(gr_complex* out_buf,
117 const int8_t* in_bits,
118 unsigned int n_bits,
119 bool inv_convention = false)
120 {
121 if (n_bits % 2 != 0)
122 throw std::invalid_argument("Number of bits must be even");
123
124 // Standard mapping: multiply the input bits by -sqrt(2) and add sqrt(2)/2.
125 // Inverted mapping: multiply the input bits by +sqrt(2) and subtract sqrt(2)/2.
126 float div_scalar = inv_convention ? SQRT2_2 : -SQRT2_2;
127 float* out_buf_32f = reinterpret_cast<float*>(out_buf);
128 volk_8i_s32f_convert_32f(out_buf_32f, in_bits, div_scalar, n_bits);
129
130 // The volk_32f_s32f_add_32f kernel has a bug in which its generic implementation
131 // can never be found with volk version <= 3.0.0. Given the QA tests run with
132 // VOLK_GENERIC=1 (added by the GR_ADD_CPP_TEST macro), this bug leads to an
133 // infinite loop below when volk_32f_s32f_add_32f spins forever searching for the
134 // generic implementation. A fix was provided in gnuradio/volk#641, but it won't
135 // take effect until a new version following 3.0.0 is released. Hence, for
136 // affected versions, check if VOLK_GENERIC is set and use an alternative
137 // implementation if so.
138 float offset = inv_convention ? -SQRT2_2 : SQRT2_2;
139#if VOLK_VERSION <= 30000
140 if (d_volk_generic) {
141 for (unsigned int i = 0; i < n_bits; i++)
142 out_buf_32f[i] += offset;
143 } else {
144 volk_32f_s32f_add_32f(out_buf_32f, out_buf_32f, offset, n_bits);
145 }
146#else
147 volk_32f_s32f_add_32f(out_buf_32f, out_buf_32f, offset, n_bits);
148#endif
149 }
150
151 /**
152 * @overload
153 * @param out_buf Buffer where the mapped symbols should be stored.
154 * @param in_bits Vector containing the input bits on unpacked int8 values.
155 * @param inv_convention Whether to use the inverted mapping convention.
156 */
157 void map(gr_complex* out_buf,
158 const volk::vector<int8_t>& in_bits,
159 bool inv_convention = false)
160 {
161 map(out_buf, in_bits.data(), in_bits.size(), inv_convention);
162 }
163
164 /**
165 * @brief Slice noisy input QPSK symbols to the closest constellation points.
166 *
167 * @param out_buf Buffer where the sliced symbols should be stored.
168 * @param in_buf Buffer containing the input QPSK symbols.
169 * @param n_syms Number of symbols to slice.
170 */
171 void slice(gr_complex* out_buf, const gr_complex* in_buf, unsigned int n_syms)
172 {
173 // The volk_32f_binary_slicer_8i kernel facilitates decoding but returns inverted
174 // results relative to the convention adopted in the DVB-S2 standard. It returns
175 // bit=1 for non-negative values and bit=0 for negative values. Nevertheless, we
176 // can use it as-is as long as we stick to the same inverted convention when
177 // remapping the hard-decoded bits back to QPSK constellation symbols.
178 volk_32f_binary_slicer_8i(
179 d_aux_8i_buffer.data(), reinterpret_cast<const float*>(in_buf), n_syms * 2);
180 map(out_buf, d_aux_8i_buffer.data(), n_syms * 2, /*inv_convention=*/true);
181 }
182
183 /**
184 * @overload
185 * @param out_buf Buffer where the sliced symbols should be stored.
186 * @param in_syms Vector containing the input QPSK symbols.
187 */
188 void slice(gr_complex* out_buf, const volk::vector<gr_complex>& in_syms)
189 {
190 slice(out_buf, in_syms.data(), in_syms.size());
191 }
192
193 /**
194 * @brief Soft-demap noisy input QPSK symbols into quantized LLRs.
195 *
196 * As explained in the mapping function, for each pair of bits b1b0, the MSB b1 is
197 * tied to the real part and the LSB b0 to the imaginary part. Hence, the theoretical
198 * LLR values for each bit are:
199 *
200 * LLR(b1) = 2 * sqrt(2) * Re(x) / N0
201 * LLR(b0) = 2 * sqrt(2) * Im(x) / N0
202 *
203 * @param out_buf Buffer where the output quantized LLRs should be stored.
204 * @param in_buf Buffer containing the input QPSK symbols.
205 * @param N0 Noise energy per complex dimension extracted from the estimated Es/N0.
206 * @param n_syms Number of QPSK symbols to soft-decode.
207 */
208 void
209 demap_soft(int8_t* out_buf, const gr_complex* in_buf, unsigned int n_syms, float N0)
210 {
211 float scalar = 2 * M_SQRT2 / N0;
212 volk_32f_s32f_convert_8i(
213 out_buf, reinterpret_cast<const float*>(in_buf), scalar, n_syms * 2);
214 }
215
216 /**
217 * @overload
218 * @param out_buf Buffer where the output quantized LLRs should be stored.
219 * @param in_syms Vector containing the input QPSK symbols.
220 * @param N0 Noise energy per complex dimension extracted from the estimated Es/N0.
221 */
222 void demap_soft(int8_t* out_buf, const volk::vector<gr_complex>& in_syms, float N0)
223 {
224 demap_soft(out_buf, in_syms.data(), in_syms.size(), N0);
225 }
226
227 /**
228 * @brief Estimate the linear SNR of input QPSK symbols.
229 *
230 * Slices the input symbols with hard-demapping and uses the resulting sliced symbols
231 * as the reference (ideal constellation points) for the measurement.
232 *
233 * @param in_syms Buffer containing the input QPSK symbols.
234 * @param n_syms Number of QPSK symbols available on the input buffer.
235 * @return float Estimated linear SNR.
236 * @note Use the `estimate_snr()` overloaded method with the ref_llrs arguments to
237 * estimate the post-decoder SNR when decoded LLRs are available to obtain more
238 * accurate reference constellation points.
239 */
240 float estimate_snr(const gr_complex* in_syms, unsigned int n_syms)
241 {
242 slice(d_aux_32fc_buffer.data(), in_syms, n_syms);
243 return _estimate_snr(in_syms, d_aux_32fc_buffer.data(), n_syms);
244 }
245
246 /**
247 * @overload
248 * @param in_syms Vector containing the input QPSK symbols.
249 */
250 float estimate_snr(const volk::vector<gr_complex>& in_syms)
251 {
252 return estimate_snr(in_syms.data(), in_syms.size());
253 }
254
255 /**
256 * @brief Estimate the linear SNR based on input QPSK symbols and reference LLRs.
257 *
258 * Uses the input reference LLRs (e.g., out of the LDPC decoder) to obtain the
259 * reference constellation points. Then, measures the error between the input QPSK
260 * symbols and the reference constellation points to estimate the linear SNR.
261 *
262 * @param in_syms Buffer containing the input QPSK symbols.
263 * @param ref_llrs Buffer containing the reference LLRs.
264 * @param n_syms Number of QPSK symbols available on the input buffer.
265 * @return float Estimated linear SNR.
266 */
267 float
268 estimate_snr(const gr_complex* in_syms, const int8_t* ref_llrs, unsigned int n_syms)
269 {
270 // Remap the LLRs back to reference QPSK constellation symbols. Unfortunately,
271 // there is no binary slicer volk kernel for 8i type, so we convert the 8i LLRs to
272 // 32f values, then slice these by reusing the slice() method. Note this entails
273 // two conversions unnecessarily, from 8i to 32f and back.
274 //
275 // TODO Implement a volk kernel to slice 8i LLRs directly.
276 gr_complex* p_ref_syms = d_aux_32fc_buffer.data();
277 volk_8i_s32f_convert_32f(
278 reinterpret_cast<float*>(p_ref_syms), ref_llrs, 1.0, n_syms * 2);
279 slice(p_ref_syms, p_ref_syms, n_syms);
280 return _estimate_snr(in_syms, p_ref_syms, n_syms);
281 }
282
283 /**
284 * @overload
285 * @param in_syms Vector containing the input QPSK symbols.
286 * @param ref_llrs Vector containing the reference LLRs.
287 */
288 float estimate_snr(const volk::vector<gr_complex>& in_syms,
289 const volk::vector<int8_t>& ref_llrs)
290 {
291 if (in_syms.size() * 2 != ref_llrs.size())
292 throw std::invalid_argument("Input symbols and LLRs must have matching size");
293 return estimate_snr(in_syms.data(), ref_llrs.data(), in_syms.size());
294 }
295};
296
297} // namespace dvbs2rx
298} // namespace gr
299
300#endif /* INCLUDED_DVBS2RX_CONSTELLATIONS_H */
QPSK Constellation.
Definition qpsk.h:27
float estimate_snr(const gr_complex *in_syms, const int8_t *ref_llrs, unsigned int n_syms)
Estimate the linear SNR based on input QPSK symbols and reference LLRs.
Definition qpsk.h:268
float estimate_snr(const gr_complex *in_syms, unsigned int n_syms)
Estimate the linear SNR of input QPSK symbols.
Definition qpsk.h:240
void slice(gr_complex *out_buf, const volk::vector< gr_complex > &in_syms)
Definition qpsk.h:188
float estimate_snr(const volk::vector< gr_complex > &in_syms, const volk::vector< int8_t > &ref_llrs)
Definition qpsk.h:288
void demap_soft(int8_t *out_buf, const gr_complex *in_buf, unsigned int n_syms, float N0)
Soft-demap noisy input QPSK symbols into quantized LLRs.
Definition qpsk.h:209
~QpskConstellation()
Destroy the Qpsk Constellation object.
Definition qpsk.h:81
void demap_soft(int8_t *out_buf, const volk::vector< gr_complex > &in_syms, float N0)
Definition qpsk.h:222
float estimate_snr(const volk::vector< gr_complex > &in_syms)
Definition qpsk.h:250
void map(gr_complex *out_buf, const int8_t *in_bits, unsigned int n_bits, bool inv_convention=false)
Map input bits to QPSK symbols.
Definition qpsk.h:116
QpskConstellation()
Construct a new Qpsk Constellation object.
Definition qpsk.h:71
void slice(gr_complex *out_buf, const gr_complex *in_buf, unsigned int n_syms)
Slice noisy input QPSK symbols to the closest constellation points.
Definition qpsk.h:171
void map(gr_complex *out_buf, const volk::vector< int8_t > &in_bits, bool inv_convention=false)
Definition qpsk.h:157
#define FRAME_SIZE_NORMAL
Definition dvb_defines.h:37
Fixed-length double-ended queue with contiguous volk-aligned elements.
Definition gr_bch.h:22
#define SQRT2_2
Definition pl_defs.h:34
#define MAX_XFECFRAME_LEN
Definition pl_defs.h:27