mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-11-30 03:38:55 -05:00
1893 lines
60 KiB
C++
1893 lines
60 KiB
C++
// This file is part of LeanSDR Copyright (C) 2016-2019 <pabr@pabr.org>.
|
|
// See the toplevel README for more information.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#ifndef LEANSDR_SDR_H
|
|
#define LEANSDR_SDR_H
|
|
|
|
#include "leansdr/dsp.h"
|
|
#include "leansdr/math.h"
|
|
|
|
namespace leansdr
|
|
{
|
|
|
|
// Abbreviations for floating-point types
|
|
|
|
typedef float f32;
|
|
|
|
typedef complex<u8> cu8;
|
|
typedef complex<s8> cs8;
|
|
typedef complex<u16> cu16;
|
|
typedef complex<s16> cs16;
|
|
typedef complex<f32> cf32;
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// SDR blocks
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
// AUTO-NOTCH FILTER
|
|
|
|
// Periodically detects the [nslots] strongest peaks with a FFT,
|
|
// removes them with a first-order filter.
|
|
|
|
template <typename T>
|
|
struct auto_notch : runnable
|
|
{
|
|
int decimation;
|
|
float k;
|
|
|
|
auto_notch(scheduler *sch,
|
|
pipebuf<complex<T>> &_in,
|
|
pipebuf<complex<T>> &_out,
|
|
int _nslots,
|
|
T _agc_rms_setpoint) : runnable(sch, "auto_notch"),
|
|
decimation(1024 * 4096),
|
|
k(0.002), // k(0.01)
|
|
fft(4096),
|
|
in(_in),
|
|
out(_out, fft.n),
|
|
nslots(_nslots),
|
|
phase(0),
|
|
gain(1),
|
|
agc_rms_setpoint(_agc_rms_setpoint)
|
|
{
|
|
__slots = new slot[nslots];
|
|
|
|
for (int s = 0; s < nslots; ++s)
|
|
{
|
|
__slots[s].i = -1;
|
|
__slots[s].expj = new complex<float>[fft.n];
|
|
}
|
|
}
|
|
|
|
~auto_notch()
|
|
{
|
|
delete[] __slots;
|
|
}
|
|
|
|
void run()
|
|
{
|
|
while (in.readable() >= fft.n && out.writable() >= fft.n)
|
|
{
|
|
phase += fft.n;
|
|
|
|
if (phase >= decimation)
|
|
{
|
|
phase -= decimation;
|
|
detect();
|
|
}
|
|
|
|
process();
|
|
in.read(fft.n);
|
|
out.written(fft.n);
|
|
}
|
|
}
|
|
|
|
void detect()
|
|
{
|
|
complex<T> *pin = in.rd();
|
|
complex<float> *data = new complex<float>[fft.n];
|
|
float m0 = 0, m2 = 0;
|
|
|
|
for (int i = 0; i < fft.n; ++i)
|
|
{
|
|
data[i].re = pin[i].re;
|
|
data[i].im = pin[i].im;
|
|
m2 += (float)pin[i].re * pin[i].re + (float)pin[i].im * pin[i].im;
|
|
if (gen_abs(pin[i].re) > m0)
|
|
m0 = gen_abs(pin[i].re);
|
|
if (gen_abs(pin[i].im) > m0)
|
|
m0 = gen_abs(pin[i].im);
|
|
}
|
|
|
|
if (agc_rms_setpoint && m2)
|
|
{
|
|
float rms = gen_sqrt(m2 / fft.n);
|
|
if (sch->debug)
|
|
fprintf(stderr, "(pow %f max %f)", rms, m0);
|
|
float new_gain = agc_rms_setpoint / rms;
|
|
gain = gain * 0.9 + new_gain * 0.1;
|
|
}
|
|
|
|
fft.inplace(data, true);
|
|
float *amp = new float[fft.n];
|
|
|
|
for (int i = 0; i < fft.n; ++i)
|
|
amp[i] = hypotf(data[i].re, data[i].im);
|
|
|
|
for (slot *s = __slots; s < __slots + nslots; ++s)
|
|
{
|
|
int iamax = 0;
|
|
|
|
for (int i = 0; i < fft.n; ++i)
|
|
if (amp[i] > amp[iamax])
|
|
iamax = i;
|
|
|
|
if (iamax != s->i)
|
|
{
|
|
if (sch->debug)
|
|
fprintf(stderr, "%s: slot %d new peak %d -> %d\n", name, (int)(s - __slots), s->i, iamax);
|
|
|
|
s->i = iamax;
|
|
s->estim.re = 0;
|
|
s->estim.im = 0;
|
|
s->estt = 0;
|
|
|
|
for (int i = 0; i < fft.n; ++i)
|
|
{
|
|
float a = 2 * M_PI * s->i * i / fft.n;
|
|
s->expj[i].re = cosf(a);
|
|
s->expj[i].im = sinf(a);
|
|
}
|
|
}
|
|
|
|
amp[iamax] = 0;
|
|
|
|
if (iamax - 1 >= 0)
|
|
amp[iamax - 1] = 0;
|
|
|
|
if (iamax + 1 < fft.n)
|
|
amp[iamax + 1] = 0;
|
|
}
|
|
|
|
delete[] amp;
|
|
delete[] data;
|
|
}
|
|
|
|
void process()
|
|
{
|
|
complex<T> *pin = in.rd(), *pend = pin + fft.n, *pout = out.wr();
|
|
|
|
for (slot *s = __slots; s < __slots + nslots; ++s)
|
|
s->ej = s->expj;
|
|
|
|
for (; pin < pend; ++pin, ++pout)
|
|
{
|
|
complex<float> out = *pin;
|
|
// TODO Optimize for nslots==1 ?
|
|
|
|
for (slot *s = __slots; s < __slots + nslots; ++s->ej, ++s)
|
|
{
|
|
complex<float> bb(pin->re * s->ej->re + pin->im * s->ej->im,
|
|
-pin->re * s->ej->im + pin->im * s->ej->re);
|
|
s->estim.re = bb.re * k + s->estim.re * (1 - k);
|
|
s->estim.im = bb.im * k + s->estim.im * (1 - k);
|
|
complex<float> sub(
|
|
s->estim.re * s->ej->re - s->estim.im * s->ej->im,
|
|
s->estim.re * s->ej->im + s->estim.im * s->ej->re);
|
|
out.re -= sub.re;
|
|
out.im -= sub.im;
|
|
}
|
|
|
|
pout->re = gain * out.re;
|
|
pout->im = gain * out.im;
|
|
}
|
|
}
|
|
|
|
private:
|
|
cfft_engine<float> fft;
|
|
pipereader<complex<T>> in;
|
|
pipewriter<complex<T>> out;
|
|
int nslots;
|
|
|
|
struct slot
|
|
{
|
|
int i;
|
|
complex<float> estim;
|
|
complex<float> *expj, *ej;
|
|
int estt;
|
|
};
|
|
|
|
slot *__slots;
|
|
int phase;
|
|
float gain;
|
|
T agc_rms_setpoint;
|
|
};
|
|
|
|
// SIGNAL STRENGTH ESTIMATOR
|
|
|
|
// Outputs RMS values.
|
|
|
|
template <typename T>
|
|
struct ss_estimator : runnable
|
|
{
|
|
unsigned long window_size; // Samples per estimation
|
|
unsigned long decimation; // Output rate
|
|
|
|
ss_estimator(scheduler *sch, pipebuf<complex<T>> &_in, pipebuf<T> &_out) : runnable(sch, "SS estimator"),
|
|
window_size(1024),
|
|
decimation(1024),
|
|
in(_in),
|
|
out(_out),
|
|
phase(0)
|
|
{
|
|
}
|
|
|
|
void run()
|
|
{
|
|
while (in.readable() >= window_size && out.writable() >= 1)
|
|
{
|
|
phase += window_size;
|
|
|
|
if (phase >= decimation)
|
|
{
|
|
phase -= decimation;
|
|
complex<T> *p = in.rd(), *pend = p + window_size;
|
|
float s = 0;
|
|
|
|
for (; p < pend; ++p)
|
|
s += (float)p->re * p->re + (float)p->im * p->im;
|
|
|
|
out.write(sqrtf(s / window_size));
|
|
}
|
|
in.read(window_size);
|
|
}
|
|
}
|
|
|
|
private:
|
|
pipereader<complex<T>> in;
|
|
pipewriter<T> out;
|
|
unsigned long phase;
|
|
};
|
|
|
|
template <typename T>
|
|
struct ss_amp_estimator : runnable
|
|
{
|
|
unsigned long window_size; // Samples per estimation
|
|
unsigned long decimation; // Output rate
|
|
|
|
ss_amp_estimator(scheduler *sch,
|
|
pipebuf<complex<T>> &_in,
|
|
pipebuf<T> &_out_ss,
|
|
pipebuf<T> &_out_ampmin,
|
|
pipebuf<T> &_out_ampmax) : runnable(sch, "SS estimator"),
|
|
window_size(1024),
|
|
decimation(1024),
|
|
in(_in),
|
|
out_ss(_out_ss),
|
|
out_ampmin(_out_ampmin),
|
|
out_ampmax(_out_ampmax),
|
|
phase(0)
|
|
{
|
|
}
|
|
|
|
void run()
|
|
{
|
|
while (in.readable() >= window_size && out_ss.writable() >= 1 && out_ampmin.writable() >= 1 && out_ampmax.writable() >= 1)
|
|
{
|
|
phase += window_size;
|
|
|
|
if (phase >= decimation)
|
|
{
|
|
phase -= decimation;
|
|
complex<T> *p = in.rd(), *pend = p + window_size;
|
|
float s2 = 0;
|
|
float amin = 1e38, amax = 0;
|
|
|
|
for (; p < pend; ++p)
|
|
{
|
|
float mag2 = (float)p->re * p->re + (float)p->im * p->im;
|
|
s2 += mag2;
|
|
float mag = sqrtf(mag2);
|
|
if (mag < amin)
|
|
amin = mag;
|
|
if (mag > amax)
|
|
amax = mag;
|
|
}
|
|
|
|
out_ss.write(sqrtf(s2 / window_size));
|
|
out_ampmin.write(amin);
|
|
out_ampmax.write(amax);
|
|
}
|
|
|
|
in.read(window_size);
|
|
}
|
|
}
|
|
|
|
private:
|
|
pipereader<complex<T>> in;
|
|
pipewriter<T> out_ss, out_ampmin, out_ampmax;
|
|
unsigned long phase;
|
|
};
|
|
|
|
// AGC
|
|
|
|
template <typename T>
|
|
struct simple_agc : runnable
|
|
{
|
|
float out_rms; // Desired RMS output power
|
|
float bw; // Bandwidth
|
|
float estimated; // Input power
|
|
static const int chunk_size = 128;
|
|
|
|
simple_agc(scheduler *sch,
|
|
pipebuf<complex<T>> &_in,
|
|
pipebuf<complex<T>> &_out) : runnable(sch, "AGC"),
|
|
out_rms(1),
|
|
bw(0.001),
|
|
estimated(0),
|
|
in(_in),
|
|
out(_out, chunk_size)
|
|
{
|
|
}
|
|
|
|
private:
|
|
pipereader<complex<T>> in;
|
|
pipewriter<complex<T>> out;
|
|
|
|
void run()
|
|
{
|
|
while (in.readable() >= chunk_size && out.writable() >= chunk_size)
|
|
{
|
|
complex<T> *pin = in.rd(), *pend = pin + chunk_size;
|
|
float amp2 = 0;
|
|
|
|
for (; pin < pend; ++pin)
|
|
amp2 += pin->re * pin->re + pin->im * pin->im;
|
|
|
|
amp2 /= chunk_size;
|
|
|
|
if (!estimated)
|
|
estimated = amp2;
|
|
|
|
estimated = estimated * (1 - bw) + amp2 * bw;
|
|
float gain = estimated ? out_rms / sqrtf(estimated) : 0;
|
|
pin = in.rd();
|
|
complex<T> *pout = out.wr();
|
|
float bwcomp = 1 - bw;
|
|
|
|
for (; pin < pend; ++pin, ++pout)
|
|
{
|
|
pout->re = pin->re * gain;
|
|
pout->im = pin->im * gain;
|
|
}
|
|
|
|
in.read(chunk_size);
|
|
out.written(chunk_size);
|
|
}
|
|
}
|
|
};
|
|
// simple_agc
|
|
|
|
typedef uint16_t u_angle; // [0,2PI[ in 65536 steps
|
|
typedef int16_t s_angle; // [-PI,PI[ in 65536 steps
|
|
|
|
// GENERIC CONSTELLATION DECODING BY LOOK-UP TABLE.
|
|
|
|
// Metrics and phase errors are pre-computed on a RxR grid.
|
|
// R must be a power of 2.
|
|
// Up to 256 symbols.
|
|
|
|
struct softsymbol
|
|
{ // TBD obsolete
|
|
int16_t cost; // For Viterbi with TBM=int16_t
|
|
uint8_t symbol;
|
|
};
|
|
|
|
// Target RMS amplitude for AGC
|
|
//const float cstln_amp = 73; // Best for 32APSK 9/10
|
|
//const float cstln_amp = 90; // Best for QPSK
|
|
//const float cstln_amp = 64; // Best for BPSK
|
|
//const float cstln_amp = 75; // Best for BPSK at 45°
|
|
const float cstln_amp = 75; // Trade-off
|
|
|
|
// A struct that temporarily holds all the info we precompute for the LUT.
|
|
struct full_ss
|
|
{
|
|
uint8_t nearest; // Index of nearest in constellation
|
|
uint16_t dists2[256]; // Squared distances
|
|
float p[8]; // 0..1 probability of bits being 1
|
|
};
|
|
|
|
// Options for soft-symbols.
|
|
// These functions are overloaded to keep cstln_lut<SOFTSYMB> generic:
|
|
// to_softsymb(const full_ss *fss, SOFTSYMB *ss)
|
|
// softsymb_harden(SOFTSYMB *ss) {
|
|
// softsymb_to_dump(const SOFTSYMB &ss, int bit) To grey 0..255
|
|
// For LUT initialization only. Performance is not critical.
|
|
|
|
// Hard decision soft-symbols.
|
|
// Value is the symbol index, 0..255.
|
|
typedef uint8_t hard_ss;
|
|
void to_softsymb(const full_ss *fss, hard_ss *ss);
|
|
void softsymb_harden(hard_ss *ss);
|
|
uint8_t softsymb_to_dump(const hard_ss &ss, int bit);
|
|
|
|
// Euclidian QPSK soft-symbols.
|
|
// Additive metric suitable for Viterbi.
|
|
// Backward-compatible with simplified Viterbi (TBD remove)
|
|
struct eucl_ss
|
|
{
|
|
static const int MAX_SYMBOLS = 4;
|
|
uint16_t dists2[MAX_SYMBOLS];
|
|
uint16_t discr2; // 2nd_nearest - nearest
|
|
uint8_t nearest;
|
|
};
|
|
|
|
void to_softsymb(const full_ss *fss, eucl_ss *ss);
|
|
void softsymb_harden(eucl_ss *ss);
|
|
uint8_t softsymb_to_dump(const eucl_ss &ss, int bit);
|
|
|
|
// Log-Likelihood Ratios soft-symbols
|
|
typedef int8_t llr_t; // log(p(0)/p(1)), clip -127=1 +127=0
|
|
|
|
inline bool llr_harden(llr_t v)
|
|
{
|
|
return v & 128;
|
|
}
|
|
|
|
struct llr_ss
|
|
{
|
|
llr_t bits[8]; // Up to 8 bit considered independent
|
|
};
|
|
|
|
void to_softsymb(const full_ss *fss, llr_ss *ss);
|
|
void softsymb_harden(llr_ss *ss);
|
|
uint8_t softsymb_to_dump(const llr_ss &ss, int bit);
|
|
|
|
struct cstln_base
|
|
{
|
|
enum predef
|
|
{
|
|
BPSK, // DVB-S2 (and DVB-S variant)
|
|
QPSK, // DVB-S
|
|
PSK8,
|
|
APSK16,
|
|
APSK32, // DVB-S2
|
|
APSK64E, // DVB-S2X
|
|
QAM16,
|
|
QAM64,
|
|
QAM256, // For experimentation only
|
|
COUNT
|
|
};
|
|
|
|
static const char *names[];
|
|
float amp_max; // Max amplitude. 1 for PSK, 0 if not applicable.
|
|
complex<int8_t> *symbols;
|
|
int nsymbols;
|
|
int nrotations;
|
|
};
|
|
// cstln_base
|
|
|
|
template <typename SOFTSYMB, int R>
|
|
struct cstln_lut : cstln_base
|
|
{
|
|
cstln_lut(cstln_base::predef type,
|
|
float mer = 10,
|
|
float gamma1 = 0,
|
|
float gamma2 = 0,
|
|
float gamma3 = 0)
|
|
{
|
|
switch (type)
|
|
{
|
|
case BPSK:
|
|
amp_max = 1;
|
|
nrotations = 2;
|
|
nsymbols = 2;
|
|
symbols = new complex<signed char>[nsymbols];
|
|
#if 0 // BPSK at 0°
|
|
symbols[0] = polar(1, 2, 0);
|
|
symbols[1] = polar(1, 2, 1);
|
|
#else // BPSK at 45°
|
|
symbols[0] = polar(1, 8, 1);
|
|
symbols[1] = polar(1, 8, 5);
|
|
#endif
|
|
make_lut_from_symbols(mer);
|
|
break;
|
|
case QPSK:
|
|
amp_max = 1;
|
|
// EN 300 421, section 4.5 Baseband shaping and modulation
|
|
// EN 302 307, section 5.4.1
|
|
nrotations = 4;
|
|
nsymbols = 4;
|
|
symbols = new complex<signed char>[nsymbols];
|
|
symbols[0] = polar(1, 4, 0.5);
|
|
symbols[1] = polar(1, 4, 3.5);
|
|
symbols[2] = polar(1, 4, 1.5);
|
|
symbols[3] = polar(1, 4, 2.5);
|
|
make_lut_from_symbols(mer);
|
|
break;
|
|
case PSK8:
|
|
amp_max = 1;
|
|
// EN 302 307, section 5.4.2
|
|
nrotations = 8;
|
|
nsymbols = 8;
|
|
symbols = new complex<signed char>[nsymbols];
|
|
symbols[0] = polar(1, 8, 1);
|
|
symbols[1] = polar(1, 8, 0);
|
|
symbols[2] = polar(1, 8, 4);
|
|
symbols[3] = polar(1, 8, 5);
|
|
symbols[4] = polar(1, 8, 2);
|
|
symbols[5] = polar(1, 8, 7);
|
|
symbols[6] = polar(1, 8, 3);
|
|
symbols[7] = polar(1, 8, 6);
|
|
make_lut_from_symbols(mer);
|
|
break;
|
|
case APSK16:
|
|
{
|
|
// Default gamma for non-DVB-S2 applications.
|
|
if (!gamma1)
|
|
gamma1 = 2.57;
|
|
// EN 302 307, section 5.4.3
|
|
float r1 = sqrtf(4 / (1 + 3 * gamma1 * gamma1));
|
|
float r2 = gamma1 * r1;
|
|
amp_max = r2;
|
|
nrotations = 4;
|
|
nsymbols = 16;
|
|
symbols = new complex<signed char>[nsymbols];
|
|
symbols[0] = polar(r2, 12, 1.5);
|
|
symbols[1] = polar(r2, 12, 10.5);
|
|
symbols[2] = polar(r2, 12, 4.5);
|
|
symbols[3] = polar(r2, 12, 7.5);
|
|
symbols[4] = polar(r2, 12, 0.5);
|
|
symbols[5] = polar(r2, 12, 11.5);
|
|
symbols[6] = polar(r2, 12, 5.5);
|
|
symbols[7] = polar(r2, 12, 6.5);
|
|
symbols[8] = polar(r2, 12, 2.5);
|
|
symbols[9] = polar(r2, 12, 9.5);
|
|
symbols[10] = polar(r2, 12, 3.5);
|
|
symbols[11] = polar(r2, 12, 8.5);
|
|
symbols[12] = polar(r1, 4, 0.5);
|
|
symbols[13] = polar(r1, 4, 3.5);
|
|
symbols[14] = polar(r1, 4, 1.5);
|
|
symbols[15] = polar(r1, 4, 2.5);
|
|
make_lut_from_symbols(mer);
|
|
break;
|
|
}
|
|
case APSK32:
|
|
{
|
|
// Default gammas for non-DVB-S2 applications.
|
|
if (!gamma1)
|
|
gamma1 = 2.53;
|
|
if (!gamma2)
|
|
gamma2 = 4.30;
|
|
// EN 302 307, section 5.4.3
|
|
float r1 = sqrtf(
|
|
8 / (1 + 3 * gamma1 * gamma1 + 4 * gamma2 * gamma2));
|
|
float r2 = gamma1 * r1;
|
|
float r3 = gamma2 * r1;
|
|
amp_max = r3;
|
|
nrotations = 4;
|
|
nsymbols = 32;
|
|
symbols = new complex<signed char>[nsymbols];
|
|
symbols[0] = polar(r2, 12, 1.5);
|
|
symbols[1] = polar(r2, 12, 2.5);
|
|
symbols[2] = polar(r2, 12, 10.5);
|
|
symbols[3] = polar(r2, 12, 9.5);
|
|
symbols[4] = polar(r2, 12, 4.5);
|
|
symbols[5] = polar(r2, 12, 3.5);
|
|
symbols[6] = polar(r2, 12, 7.5);
|
|
symbols[7] = polar(r2, 12, 8.5);
|
|
symbols[8] = polar(r3, 16, 1);
|
|
symbols[9] = polar(r3, 16, 3);
|
|
symbols[10] = polar(r3, 16, 14);
|
|
symbols[11] = polar(r3, 16, 12);
|
|
symbols[12] = polar(r3, 16, 6);
|
|
symbols[13] = polar(r3, 16, 4);
|
|
symbols[14] = polar(r3, 16, 9);
|
|
symbols[15] = polar(r3, 16, 11);
|
|
symbols[16] = polar(r2, 12, 0.5);
|
|
symbols[17] = polar(r1, 4, 0.5);
|
|
symbols[18] = polar(r2, 12, 11.5);
|
|
symbols[19] = polar(r1, 4, 3.5);
|
|
symbols[20] = polar(r2, 12, 5.5);
|
|
symbols[21] = polar(r1, 4, 1.5);
|
|
symbols[22] = polar(r2, 12, 6.5);
|
|
symbols[23] = polar(r1, 4, 2.5);
|
|
symbols[24] = polar(r3, 16, 0);
|
|
symbols[25] = polar(r3, 16, 2);
|
|
symbols[26] = polar(r3, 16, 15);
|
|
symbols[27] = polar(r3, 16, 13);
|
|
symbols[28] = polar(r3, 16, 7);
|
|
symbols[29] = polar(r3, 16, 5);
|
|
symbols[30] = polar(r3, 16, 8);
|
|
symbols[31] = polar(r3, 16, 10);
|
|
make_lut_from_symbols(mer);
|
|
break;
|
|
}
|
|
case APSK64E:
|
|
{
|
|
// Default gammas for non-DVB-S2 applications.
|
|
if (!gamma1)
|
|
gamma1 = 2.4;
|
|
if (!gamma2)
|
|
gamma2 = 4.3;
|
|
if (!gamma3)
|
|
gamma3 = 7.0;
|
|
// EN 302 307-2, section 5.4.5, Table 13e
|
|
float r1 = sqrtf(
|
|
64 / (4 + 12 * gamma1 * gamma1 + 20 * gamma2 * gamma2 + 28 * gamma3 * gamma3));
|
|
float r2 = gamma1 * r1;
|
|
float r3 = gamma2 * r1;
|
|
float r4 = gamma3 * r1;
|
|
amp_max = r4;
|
|
nrotations = 4;
|
|
nsymbols = 64;
|
|
symbols = new complex<signed char>[nsymbols];
|
|
polar2(0, r4, 1.0 / 4, 7.0 / 4, 3.0 / 4, 5.0 / 4);
|
|
polar2(4, r4, 13.0 / 28, 43.0 / 28, 15.0 / 28, 41.0 / 28);
|
|
polar2(8, r4, 1.0 / 28, 55.0 / 28, 27.0 / 28, 29.0 / 28);
|
|
polar2(12, r1, 1.0 / 4, 7.0 / 4, 3.0 / 4, 5.0 / 4);
|
|
polar2(16, r4, 9.0 / 28, 47.0 / 28, 19.0 / 28, 37.0 / 28);
|
|
polar2(20, r4, 11.0 / 28, 45.0 / 28, 17.0 / 28, 39.0 / 28);
|
|
polar2(24, r3, 1.0 / 20, 39.0 / 20, 19.0 / 20, 21.0 / 20);
|
|
polar2(28, r2, 1.0 / 12, 23.0 / 12, 11.0 / 12, 13.0 / 12);
|
|
polar2(32, r4, 5.0 / 28, 51.0 / 28, 23.0 / 28, 33.0 / 28);
|
|
polar2(36, r3, 9.0 / 20, 31.0 / 20, 11.0 / 20, 29.0 / 20);
|
|
polar2(40, r4, 3.0 / 28, 53.0 / 28, 25.0 / 28, 31.0 / 28);
|
|
polar2(44, r2, 5.0 / 12, 19.0 / 12, 7.0 / 12, 17.0 / 12);
|
|
polar2(48, r3, 1.0 / 4, 7.0 / 4, 3.0 / 4, 5.0 / 4);
|
|
polar2(52, r3, 7.0 / 20, 33.0 / 20, 13.0 / 20, 27.0 / 20);
|
|
polar2(56, r3, 3.0 / 20, 37.0 / 20, 17.0 / 20, 23.0 / 20);
|
|
polar2(60, r2, 1.0 / 4, 7.0 / 4, 3.0 / 4, 5.0 / 4);
|
|
make_lut_from_symbols(mer);
|
|
break;
|
|
}
|
|
case QAM16:
|
|
amp_max = 0;
|
|
make_qam(16);
|
|
break;
|
|
case QAM64:
|
|
amp_max = 1;
|
|
make_qam(64);
|
|
break;
|
|
case QAM256:
|
|
amp_max = 1;
|
|
make_qam(256);
|
|
break;
|
|
default:
|
|
fail("Constellation not implemented");
|
|
}
|
|
}
|
|
|
|
struct result
|
|
{
|
|
SOFTSYMB ss;
|
|
s_angle phase_error;
|
|
uint8_t symbol; // Nearest symbol, useful for C&T recovery
|
|
};
|
|
|
|
inline result *lookup(float I, float Q)
|
|
{
|
|
// Handling of overflows beyond the lookup table:
|
|
// - For BPSK/QPSK/8PSK we only care about the phase,
|
|
// so the following is harmless and improves locking at low SNR.
|
|
// - For amplitude modulations this is not appropriate.
|
|
// However, if there is enough noise to cause overflow,
|
|
// demodulation would probably fail anyway.
|
|
//
|
|
// Comment-out for better throughput at high SNR.
|
|
#if 1
|
|
while (I < -128 || I > 127 || Q < -128 || Q > 127)
|
|
{
|
|
I *= 0.5;
|
|
Q *= 0.5;
|
|
}
|
|
#endif
|
|
return &lut[(u8)(s8)I][(u8)(s8)Q];
|
|
}
|
|
|
|
inline result *lookup(int I, int Q)
|
|
{
|
|
// Ignore wrapping modulo 256
|
|
return &lut[(u8)I][(u8)Q];
|
|
}
|
|
|
|
private:
|
|
complex<signed char> polar(float r, int n, float i)
|
|
{
|
|
float a = i * 2 * M_PI / n;
|
|
return complex<signed char>(r * cosf(a) * cstln_amp,
|
|
r * sinf(a) * cstln_amp);
|
|
}
|
|
|
|
// Helper function for some constellation tables
|
|
void polar2(int i, float r, float a0, float a1, float a2, float a3)
|
|
{
|
|
float a[] = {a0, a1, a2, a3};
|
|
|
|
for (int j = 0; j < 4; ++j)
|
|
{
|
|
float phi = a[j] * M_PI;
|
|
symbols[i + j] = complex<signed char>(r * cosf(phi) * cstln_amp,
|
|
r * sinf(phi) * cstln_amp);
|
|
}
|
|
}
|
|
|
|
void make_qam(int n)
|
|
{
|
|
nrotations = 4;
|
|
nsymbols = n;
|
|
symbols = new complex<signed char>[nsymbols];
|
|
int m = sqrtl(n);
|
|
float scale;
|
|
|
|
{ // Average power in first quadrant with unit grid
|
|
int q = m / 2;
|
|
float avgpower = 2 * (q * 0.25 + (q - 1) * q / 2 + (q - 1) * q * (2 * q - 1) / 6) / q;
|
|
scale = 1.0 / sqrtf(avgpower);
|
|
}
|
|
// Arbitrary mapping
|
|
|
|
int s = 0;
|
|
|
|
for (int x = 0; x < m; ++x)
|
|
{
|
|
for (int y = 0; y < m; ++y)
|
|
{
|
|
float I = x - (float)(m - 1) / 2;
|
|
float Q = y - (float)(m - 1) / 2;
|
|
symbols[s].re = I * scale * cstln_amp;
|
|
symbols[s].im = Q * scale * cstln_amp;
|
|
++s;
|
|
}
|
|
}
|
|
|
|
make_lut_from_symbols(20); // TBD
|
|
}
|
|
|
|
result lut[R][R];
|
|
|
|
void make_lut_from_symbols(float mer)
|
|
{
|
|
// Note: Excessively low values of MER will break 16APSK and 32APSK.
|
|
float sigma = cstln_amp * exp10f(-mer / 20);
|
|
|
|
// Precomputed values.
|
|
// Shared scope so that we don't have to reset dists2[nsymbols..] to -1.
|
|
struct full_ss fss;
|
|
|
|
for (int s = 0; s < 256; ++s)
|
|
fss.dists2[s] = -1;
|
|
|
|
for (int I = -R / 2; I < R / 2; ++I)
|
|
{
|
|
for (int Q = -R / 2; Q < R / 2; ++Q)
|
|
{
|
|
// Nearest symbol
|
|
fss.nearest = 0;
|
|
fss.dists2[0] = 65535;
|
|
// Conditional probabilities:
|
|
// Sum likelyhoods from all candidate symbols.
|
|
//
|
|
// P(TX[b]==B | RX==IQ) =
|
|
// sum(S=0..nsymbols-1, P(TX[b]==B | RX==IQ && TXs==S))
|
|
//
|
|
// P(TX[b] == B | RX==IQ && TX==S) =
|
|
// P(TX[b]==B && RX==IQ && TX==S) / P(RX==IQ && TX==S)
|
|
float probs[8][2];
|
|
memset(probs, 0, sizeof(probs));
|
|
|
|
for (int s = 0; s < nsymbols; ++s)
|
|
{
|
|
float d2 = ((I - symbols[s].re) * (I - symbols[s].re) + (Q - symbols[s].im) * (Q - symbols[s].im));
|
|
|
|
if (d2 < fss.dists2[fss.nearest])
|
|
fss.nearest = s;
|
|
|
|
fss.dists2[s] = d2;
|
|
float p = expf(-d2 / (2 * sigma * sigma)) / (sqrtf(2 * M_PI) * sigma);
|
|
|
|
for (int bit = 0; bit < 8; ++bit)
|
|
{
|
|
probs[bit][(s >> bit) & 1] += p;
|
|
}
|
|
}
|
|
|
|
// Normalize
|
|
for (int b = 0; b < 8; ++b)
|
|
{
|
|
float p = probs[b][1] / (probs[b][0] + probs[b][1]);
|
|
// Avoid trouble when sigma is unrealistically low.
|
|
if (!isnormal(p))
|
|
p = 0;
|
|
fss.p[b] = p;
|
|
}
|
|
|
|
result *pr = &lut[I & (R - 1)][Q & (R - 1)];
|
|
to_softsymb(&fss, &pr->ss);
|
|
// Always record nearest symbol and phase error for C&T.
|
|
pr->symbol = fss.nearest;
|
|
float ph_symbol = atan2f(symbols[pr->symbol].im,
|
|
symbols[pr->symbol].re);
|
|
float ph_err = atan2f(Q, I) - ph_symbol;
|
|
pr->phase_error = (int32_t)(ph_err * 65536 / (2 * M_PI)); // Mod 65536
|
|
}
|
|
}
|
|
}
|
|
|
|
public:
|
|
void dump(FILE *f)
|
|
{
|
|
int bps = log2(nsymbols);
|
|
fprintf(f, "P5\n%d %d\n255\n", R, R * (bps + 1));
|
|
|
|
for (int bit = 0; bit < bps + 1; ++bit)
|
|
{
|
|
for (int Q = R / 2 - 1; Q >= -R / 2; --Q)
|
|
{
|
|
for (int I = -R / 2; I < R / 2; ++I)
|
|
{
|
|
result *pr = &lut[I & (R - 1)][Q & (R - 1)];
|
|
uint8_t v;
|
|
if (bit < bps)
|
|
v = softsymb_to_dump(pr->ss, bit);
|
|
else
|
|
v = 128 + pr->phase_error / 64;
|
|
// Highlight the constellation symbols.
|
|
for (int s = 0; s < nsymbols; ++s)
|
|
{
|
|
if (symbols[s].re == I && symbols[s].im == Q)
|
|
v ^= 128;
|
|
}
|
|
|
|
fputc(v, f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert soft metric to Hamming distance
|
|
void harden()
|
|
{
|
|
for (int i = 0; i < R; ++i)
|
|
{
|
|
for (int q = 0; q < R; ++q)
|
|
softsymb_harden(&lut[i][q].ss);
|
|
}
|
|
}
|
|
};
|
|
// cstln_lut
|
|
|
|
// SAMPLER INTERFACE FOR CSTLN_RECEIVER
|
|
|
|
template <typename T>
|
|
struct sampler_interface
|
|
{
|
|
virtual ~sampler_interface()
|
|
{
|
|
}
|
|
virtual complex<T> interp(const complex<T> *pin, float mu, float phase) = 0;
|
|
virtual void update_freq(float freqw, int period = 1)
|
|
{
|
|
(void) freqw;
|
|
(void) period;
|
|
} // 65536 = 1 Hz
|
|
virtual int readahead() = 0;
|
|
};
|
|
|
|
// NEAREST-SAMPLE SAMPLER FOR CSTLN_RECEIVER
|
|
// Suitable for bandpass-filtered, oversampled signals only
|
|
|
|
template <typename T>
|
|
struct nearest_sampler : sampler_interface<T>
|
|
{
|
|
int readahead()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
complex<T> interp(const complex<T> *pin, float mu, float phase)
|
|
{
|
|
(void) mu;
|
|
return pin[0] * trig.expi(-phase);
|
|
}
|
|
|
|
private:
|
|
trig16 trig;
|
|
};
|
|
// nearest_sampler
|
|
|
|
// LINEAR SAMPLER FOR CSTLN_RECEIVER
|
|
|
|
template <typename T>
|
|
struct linear_sampler : sampler_interface<T>
|
|
{
|
|
int readahead()
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
complex<T> interp(const complex<T> *pin, float mu, float phase)
|
|
{
|
|
// Derotate pin[0] and pin[1]
|
|
complex<T> s0 = pin[0] * trig.expi(-phase);
|
|
complex<T> s1 = pin[1] * trig.expi(-(phase + freqw));
|
|
// Interpolate linearly
|
|
return s0 * (1 - mu) + s1 * mu;
|
|
}
|
|
|
|
void update_freq(float _freqw, int period = 1)
|
|
{
|
|
(void) period;
|
|
freqw = _freqw;
|
|
}
|
|
|
|
private:
|
|
trig16 trig;
|
|
float freqw;
|
|
};
|
|
// linear_sampler
|
|
|
|
// FIR SAMPLER FOR CSTLN_RECEIVER
|
|
|
|
template <typename T, typename Tc>
|
|
struct fir_sampler : sampler_interface<T>
|
|
{
|
|
fir_sampler(int _ncoeffs, Tc *_coeffs, int _subsampling = 1) : ncoeffs(_ncoeffs),
|
|
coeffs(_coeffs),
|
|
subsampling(_subsampling),
|
|
shifted_coeffs(new complex<T>[ncoeffs]),
|
|
update_freq_phase(0)
|
|
{
|
|
do_update_freq(0); // In case application never calls update_freq()
|
|
}
|
|
|
|
int readahead()
|
|
{
|
|
return ncoeffs - 1;
|
|
}
|
|
|
|
complex<T> interp(const complex<T> *pin, float mu, float phase)
|
|
{
|
|
// Apply FIR filter with subsampling
|
|
complex<T> acc(0, 0);
|
|
complex<T> *pc = shifted_coeffs + (int)((1 - mu) * subsampling);
|
|
complex<T> *pcend = shifted_coeffs + ncoeffs;
|
|
|
|
if (subsampling == 1)
|
|
{
|
|
// Special case for heavily oversampled signals,
|
|
// where filtering is expensive.
|
|
// gcc-4.9.2 can vectorize this form with NEON on ARM.
|
|
while (pc < pcend)
|
|
acc += (*pc++) * (*pin++);
|
|
}
|
|
else
|
|
{
|
|
// Not vectorized because the coefficients are not
|
|
// guaranteed to be contiguous in memory.
|
|
for (; pc < pcend; pc += subsampling, ++pin)
|
|
acc += (*pc) * (*pin);
|
|
}
|
|
|
|
// Derotate
|
|
return trig.expi(-phase) * acc;
|
|
}
|
|
|
|
void update_freq(float freqw, int period)
|
|
{
|
|
// Throttling: Update one coeff per 16 processed samples,
|
|
// to keep the overhead of freq tracking below about 10%.
|
|
update_freq_phase -= period;
|
|
|
|
if (update_freq_phase <= 0)
|
|
{
|
|
update_freq_phase = ncoeffs * 16;
|
|
do_update_freq(freqw);
|
|
}
|
|
}
|
|
|
|
private:
|
|
void do_update_freq(float freqw)
|
|
{
|
|
float f = freqw / subsampling;
|
|
for (int i = 0; i < ncoeffs; ++i)
|
|
shifted_coeffs[i] = trig.expi(-f * (i - ncoeffs / 2)) * coeffs[i];
|
|
}
|
|
|
|
trig16 trig;
|
|
int ncoeffs;
|
|
Tc *coeffs;
|
|
int subsampling;
|
|
cf32 *shifted_coeffs;
|
|
int update_freq_phase;
|
|
};
|
|
// fir_sampler
|
|
|
|
// CONSTELLATION RECEIVER
|
|
|
|
// Linear interpolation: good enough for 1.2 samples/symbol,
|
|
// but higher oversampling is recommended.
|
|
|
|
template <typename T, typename SOFTSYMB>
|
|
struct cstln_receiver : runnable
|
|
{
|
|
sampler_interface<T> *sampler;
|
|
cstln_lut<SOFTSYMB, 256> *cstln;
|
|
unsigned long meas_decimation; // Measurement rate
|
|
float omega, min_omega, max_omega; // Samples per symbol
|
|
float freqw, min_freqw, max_freqw; // Freq offs (65536 = 1 Hz)
|
|
float pll_adjustment;
|
|
bool allow_drift; // Follow carrier beyond safe limits
|
|
static const unsigned int chunk_size = 128;
|
|
float kest;
|
|
|
|
cstln_receiver(scheduler *sch,
|
|
sampler_interface<T> *_sampler,
|
|
pipebuf<complex<T>> &_in,
|
|
pipebuf<SOFTSYMB> &_out,
|
|
pipebuf<float> *_freq_out = NULL,
|
|
pipebuf<float> *_ss_out = NULL,
|
|
pipebuf<float> *_mer_out = NULL,
|
|
pipebuf<cf32> *_cstln_out = NULL) : runnable(sch, "Constellation receiver"),
|
|
sampler(_sampler),
|
|
cstln(NULL),
|
|
meas_decimation(1048576),
|
|
pll_adjustment(1.0),
|
|
allow_drift(false),
|
|
kest(0.01),
|
|
in(_in),
|
|
out(_out, chunk_size),
|
|
est_insp(cstln_amp * cstln_amp),
|
|
agc_gain(1),
|
|
mu(0),
|
|
phase(0),
|
|
est_sp(0),
|
|
est_ep(0),
|
|
meas_count(0)
|
|
{
|
|
set_omega(1);
|
|
set_freq(0);
|
|
freq_out = _freq_out ? new pipewriter<float>(*_freq_out) : NULL;
|
|
ss_out = _ss_out ? new pipewriter<float>(*_ss_out) : NULL;
|
|
mer_out = _mer_out ? new pipewriter<float>(*_mer_out) : NULL;
|
|
cstln_out = _cstln_out ? new pipewriter<cf32>(*_cstln_out) : NULL;
|
|
memset(hist, 0, sizeof(hist));
|
|
}
|
|
|
|
void set_omega(float _omega, float tol = 10e-6)
|
|
{
|
|
omega = _omega;
|
|
min_omega = omega * (1 - tol);
|
|
max_omega = omega * (1 + tol);
|
|
update_freq_limits();
|
|
}
|
|
|
|
void set_freq(float freq)
|
|
{
|
|
freqw = freq * 65536;
|
|
update_freq_limits();
|
|
refresh_freq_tap();
|
|
}
|
|
|
|
void set_allow_drift(bool d)
|
|
{
|
|
allow_drift = d;
|
|
}
|
|
|
|
void update_freq_limits()
|
|
{
|
|
// Prevent PLL from crossing +-SR/n/2 and locking at +-SR/n.
|
|
int n = 4;
|
|
|
|
if (cstln)
|
|
{
|
|
switch (cstln->nsymbols)
|
|
{
|
|
case 2:
|
|
n = 2;
|
|
break; // BPSK
|
|
case 4:
|
|
n = 4;
|
|
break; // QPSK
|
|
case 8:
|
|
n = 8;
|
|
break; // 8PSK
|
|
case 16:
|
|
n = 12;
|
|
break; // 16APSK
|
|
case 32:
|
|
n = 16;
|
|
break; // 32APSK
|
|
default:
|
|
n = 4;
|
|
break;
|
|
}
|
|
}
|
|
|
|
min_freqw = freqw - 65536 / max_omega / n / 2;
|
|
max_freqw = freqw + 65536 / max_omega / n / 2;
|
|
}
|
|
|
|
void run()
|
|
{
|
|
if (!cstln)
|
|
fail("constellation not set");
|
|
|
|
// Magic constants that work with the qa recordings.
|
|
float freq_alpha = 0.04;
|
|
float freq_beta = 0.0012 / omega * pll_adjustment;
|
|
float gain_mu = 0.02 / (cstln_amp * cstln_amp) * 2;
|
|
|
|
int max_meas = chunk_size / meas_decimation + 1;
|
|
// Large margin on output_size because mu adjustments
|
|
// can lead to more than chunk_size/min_omega symbols.
|
|
while (in.readable() >= chunk_size + sampler->readahead() && out.writable() >= chunk_size && (!freq_out || freq_out->writable() >= max_meas) && (!ss_out || ss_out->writable() >= max_meas) && (!mer_out || mer_out->writable() >= max_meas) && (!cstln_out || cstln_out->writable() >= max_meas))
|
|
{
|
|
sampler->update_freq(freqw, chunk_size);
|
|
|
|
complex<T> *pin = in.rd(), *pin0 = pin, *pend = pin + chunk_size;
|
|
SOFTSYMB *pout = out.wr(), *pout0 = pout;
|
|
|
|
// These are scoped outside the loop for SS and MER estimation.
|
|
complex<float> sg{0.0f, 0.0f}; // Symbol before AGC;
|
|
complex<float> s; // For MER estimation and constellation viewer
|
|
complex<signed char> *cstln_point = NULL;
|
|
|
|
while (pin < pend)
|
|
{
|
|
// Here mu is the time of the next symbol counted from 0 at pin.
|
|
if (mu < 1)
|
|
{
|
|
// Here 0<=mu<1 is the fractional time of the next symbol
|
|
// between pin and pin+1.
|
|
sg = sampler->interp(pin, mu, phase + mu * freqw);
|
|
s = sg * agc_gain;
|
|
|
|
// Constellation look-up
|
|
typename cstln_lut<SOFTSYMB, 256>::result *cr =
|
|
cstln->lookup(s.re, s.im);
|
|
*pout = cr->ss;
|
|
++pout;
|
|
|
|
// PLL
|
|
phase += cr->phase_error * freq_alpha;
|
|
freqw += cr->phase_error * freq_beta;
|
|
|
|
// Modified Mueller and Müller
|
|
// mu[k]=real((c[k]-c[k-2])*conj(p[k-1])-(p[k]-p[k-2])*conj(c[k-1]))
|
|
// =dot(c[k]-c[k-2],p[k-1]) - dot(p[k]-p[k-2],c[k-1])
|
|
// p = received signals
|
|
// c = decisions (constellation points)
|
|
hist[2] = hist[1];
|
|
hist[1] = hist[0];
|
|
hist[0].p.re = s.re;
|
|
hist[0].p.im = s.im;
|
|
cstln_point = &cstln->symbols[cr->symbol];
|
|
hist[0].c.re = cstln_point->re;
|
|
hist[0].c.im = cstln_point->im;
|
|
float muerr = ((hist[0].p.re - hist[2].p.re) * hist[1].c.re + (hist[0].p.im - hist[2].p.im) * hist[1].c.im) - ((hist[0].c.re - hist[2].c.re) * hist[1].p.re + (hist[0].c.im - hist[2].c.im) * hist[1].p.im);
|
|
float mucorr = muerr * gain_mu;
|
|
const float max_mucorr = 0.1;
|
|
// TBD Optimize out statically
|
|
if (mucorr < -max_mucorr)
|
|
mucorr = -max_mucorr;
|
|
if (mucorr > max_mucorr)
|
|
mucorr = max_mucorr;
|
|
mu += mucorr;
|
|
mu += omega; // Next symbol time;
|
|
} // mu<1
|
|
|
|
// Next sample
|
|
++pin;
|
|
--mu;
|
|
phase += freqw;
|
|
} // chunk_size
|
|
|
|
in.read(pin - pin0);
|
|
out.written(pout - pout0);
|
|
|
|
// Normalize phase so that it never exceeds 32 bits.
|
|
// Max freqw is 2^31/65536/chunk_size = 256 Hz
|
|
// (this may happen with leandvb --drift --decim).
|
|
phase = fmodf(phase, 65536); // Rounding direction irrelevant
|
|
|
|
if (cstln_point)
|
|
{
|
|
// Output the last interpolated PSK symbol, max once per chunk_size
|
|
if (cstln_out)
|
|
cstln_out->write(s);
|
|
|
|
// AGC
|
|
// For APSK we must do AGC on the symbols, not the whole signal.
|
|
// TODO Use a better estimator at low SNR.
|
|
float insp = sg.re * sg.re + sg.im * sg.im;
|
|
est_insp = insp * kest + est_insp * (1 - kest);
|
|
|
|
if (est_insp)
|
|
agc_gain = cstln_amp / gen_sqrt(est_insp);
|
|
|
|
// SS and MER
|
|
complex<float> ev(s.re - cstln_point->re,
|
|
s.im - cstln_point->im);
|
|
float sig_power, ev_power;
|
|
|
|
if (cstln->nsymbols == 2)
|
|
{
|
|
// Special case for BPSK: Ignore quadrature component of noise.
|
|
// TBD Projection on I axis assumes BPSK at 45°
|
|
float sig_real = (cstln_point->re + cstln_point->im) * 0.707;
|
|
float ev_real = (ev.re + ev.im) * 0.707;
|
|
sig_power = sig_real * sig_real;
|
|
ev_power = ev_real * ev_real;
|
|
}
|
|
else
|
|
{
|
|
sig_power = (int)cstln_point->re * cstln_point->re + (int)cstln_point->im * cstln_point->im;
|
|
ev_power = ev.re * ev.re + ev.im * ev.im;
|
|
}
|
|
|
|
est_sp = sig_power * kest + est_sp * (1 - kest);
|
|
est_ep = ev_power * kest + est_ep * (1 - kest);
|
|
}
|
|
|
|
// This is best done periodically ouside the inner loop,
|
|
// but will cause non-deterministic output.
|
|
|
|
if (!allow_drift)
|
|
{
|
|
if (freqw < min_freqw || freqw > max_freqw)
|
|
freqw = (max_freqw + min_freqw) / 2;
|
|
}
|
|
|
|
// Output measurements
|
|
|
|
refresh_freq_tap();
|
|
|
|
meas_count += pin - pin0;
|
|
|
|
while (meas_count >= meas_decimation)
|
|
{
|
|
meas_count -= meas_decimation;
|
|
if (freq_out)
|
|
freq_out->write(freq_tap);
|
|
if (ss_out)
|
|
ss_out->write(sqrtf(est_insp));
|
|
if (mer_out)
|
|
mer_out->write(
|
|
est_ep ? 10 * logf(est_sp / est_ep) / logf(10) : 0);
|
|
}
|
|
|
|
} // Work to do
|
|
}
|
|
|
|
float freq_tap;
|
|
|
|
void refresh_freq_tap()
|
|
{
|
|
freq_tap = freqw / 65536;
|
|
}
|
|
|
|
private:
|
|
struct
|
|
{
|
|
complex<float> p; // Received symbol
|
|
complex<float> c; // Matched constellation point
|
|
} hist[3];
|
|
|
|
pipereader<complex<T>> in;
|
|
pipewriter<SOFTSYMB> out;
|
|
float est_insp, agc_gain;
|
|
float mu; // PSK time expressed in clock ticks
|
|
float phase; // 65536=2pi
|
|
// Signal estimation
|
|
float est_sp; // Estimated RMS signal power
|
|
float est_ep; // Estimated RMS error vector power
|
|
unsigned long meas_count;
|
|
pipewriter<float> *freq_out, *ss_out, *mer_out;
|
|
pipewriter<cf32> *cstln_out;
|
|
};
|
|
|
|
// FAST QPSK RECEIVER
|
|
|
|
// Optimized for u8 input, no AGC, uses phase information only.
|
|
// Outputs hard symbols.
|
|
|
|
template <typename T>
|
|
struct fast_qpsk_receiver : runnable
|
|
{
|
|
typedef u8 hardsymbol;
|
|
unsigned long meas_decimation; // Measurement rate
|
|
float omega, min_omega, max_omega; // Samples per symbol
|
|
signed long freqw, min_freqw, max_freqw; // Freq offs (angle per sample)
|
|
float pll_adjustment;
|
|
bool allow_drift; // Follow carrier beyond safe limits
|
|
static const unsigned int chunk_size = 128;
|
|
|
|
fast_qpsk_receiver(scheduler *sch,
|
|
pipebuf<complex<T>> &_in,
|
|
pipebuf<hardsymbol> &_out,
|
|
pipebuf<float> *_freq_out = NULL,
|
|
pipebuf<complex<T>> *_cstln_out = NULL) : runnable(sch, "Fast QPSK receiver"),
|
|
meas_decimation(1048576),
|
|
pll_adjustment(1.0),
|
|
allow_drift(false),
|
|
in(_in),
|
|
out(_out, chunk_size),
|
|
mu(0),
|
|
phase(0),
|
|
meas_count(0)
|
|
{
|
|
set_omega(1);
|
|
set_freq(0);
|
|
freq_out = _freq_out ? new pipewriter<float>(*_freq_out) : NULL;
|
|
cstln_out = _cstln_out ? new pipewriter<complex<T>>(*_cstln_out) : NULL;
|
|
memset(hist, 0, sizeof(hist));
|
|
init_lookup_tables();
|
|
}
|
|
|
|
void set_omega(float _omega, float tol = 10e-6)
|
|
{
|
|
omega = _omega;
|
|
min_omega = omega * (1 - tol);
|
|
max_omega = omega * (1 + tol);
|
|
update_freq_limits();
|
|
}
|
|
|
|
void set_freq(float freq)
|
|
{
|
|
freqw = freq * 65536;
|
|
update_freq_limits();
|
|
}
|
|
|
|
void update_freq_limits()
|
|
{
|
|
// Prevent PLL from locking at +-symbolrate/4.
|
|
// TODO The +-SR/8 limit is suitable for QPSK only.
|
|
min_freqw = freqw - 65536 / max_omega / 8;
|
|
max_freqw = freqw + 65536 / max_omega / 8;
|
|
}
|
|
|
|
static const int RLUT_BITS = 8;
|
|
static const int RLUT_ANGLES = 1 << RLUT_BITS;
|
|
|
|
void run()
|
|
{
|
|
// Magic constants that work with the qa recordings.
|
|
signed long freq_alpha = 0.04 * 65536;
|
|
signed long freq_beta = 0.0012 * 256 * 65536 / omega * pll_adjustment;
|
|
|
|
if (!freq_beta)
|
|
fail("Excessive oversampling");
|
|
|
|
float gain_mu = 0.02 / (cstln_amp * cstln_amp) * 2;
|
|
|
|
int max_meas = chunk_size / meas_decimation + 1;
|
|
// Largin margin on output_size because mu adjustments
|
|
// can lead to more than chunk_size/min_omega symbols.
|
|
|
|
while (in.readable() >= chunk_size + 1 && // +1 for interpolation
|
|
out.writable() >= chunk_size && (!freq_out || freq_out->writable() >= max_meas) && (!cstln_out || cstln_out->writable() >= max_meas))
|
|
{
|
|
complex<T> *pin = in.rd(), *pin0 = pin, *pend = pin + chunk_size;
|
|
hardsymbol *pout = out.wr(), *pout0 = pout;
|
|
|
|
cu8 s;
|
|
u_angle symbol_arg = 0; // Exported for constellation viewer
|
|
|
|
while (pin < pend)
|
|
{
|
|
// Here mu is the time of the next symbol counted from 0 at pin.
|
|
if (mu < 1)
|
|
{
|
|
// Here 0<=mu<1 is the fractional time of the next symbol
|
|
// between pin and pin+1.
|
|
|
|
// Derotate and interpolate
|
|
#if 0 /* Phase only (does not work)
|
|
Careful with the float/signed/unsigned casts */
|
|
u_angle a0 = fast_arg(pin[0]) - phase;
|
|
u_angle a1 = fast_arg(pin[1]) - (phase+freqw);
|
|
s_angle da = a1 - a0;
|
|
symbol_arg = a0 + (s_angle)(da*mu);
|
|
s = arg_to_symbol(symbol_arg);
|
|
#elif 1 // Linear by lookup-table. 1.2M on bench3bishs
|
|
polar *p0 = &lut_polar[pin[0].re][pin[0].im];
|
|
u_angle a0 = (u_angle)(p0->a - phase) >> (16 - RLUT_BITS);
|
|
cu8 *p0r = &lut_rect[a0][p0->r >> 1];
|
|
polar *p1 = &lut_polar[pin[1].re][pin[1].im];
|
|
u_angle a1 = (u_angle)(p1->a - (phase + freqw)) >> (16 - RLUT_BITS);
|
|
cu8 *p1r = &lut_rect[a1][p1->r >> 1];
|
|
s.re = (int)(p0r->re + (p1r->re - p0r->re) * mu);
|
|
s.im = (int)(p0r->im + (p1r->im - p0r->im) * mu);
|
|
symbol_arg = fast_arg(s);
|
|
#else // Linear floating-point, for reference
|
|
float a0 = -(int)phase * M_PI / 32768;
|
|
float cosa0 = cosf(a0), sina0 = sinf(a0);
|
|
complex<float>
|
|
p0r(((float)pin[0].re - 128) * cosa0 - ((float)pin[0].im - 128) * sina0,
|
|
((float)pin[0].re - 128) * sina0 + ((float)pin[0].im - 128) * cosa0);
|
|
float a1 = -(int)(phase + freqw) * M_PI / 32768;
|
|
float cosa1 = cosf(a1), sina1 = sinf(a1);
|
|
complex<float>
|
|
p1r(((float)pin[1].re - 128) * cosa1 - ((float)pin[1].im - 128) * sina1,
|
|
((float)pin[1].re - 128) * sina1 + ((float)pin[1].im - 128) * cosa1);
|
|
s.re = (int)(128 + p0r.re + (p1r.re - p0r.re) * mu);
|
|
s.im = (int)(128 + p0r.im + (p1r.im - p0r.im) * mu);
|
|
symbol_arg = fast_arg(s);
|
|
#endif
|
|
|
|
int quadrant = symbol_arg >> 14;
|
|
static unsigned char quadrant_to_symbol[4] = {0, 2, 3, 1};
|
|
*pout = quadrant_to_symbol[quadrant];
|
|
++pout;
|
|
|
|
// PLL
|
|
s_angle phase_error = (s_angle)(symbol_arg & 16383) - 8192;
|
|
phase += (phase_error * freq_alpha + 32768) >> 16;
|
|
freqw += (phase_error * freq_beta + 32768 * 256) >> 24;
|
|
|
|
// Modified Mueller and Müller
|
|
// mu[k]=real((c[k]-c[k-2])*conj(p[k-1])-(p[k]-p[k-2])*conj(c[k-1]))
|
|
// =dot(c[k]-c[k-2],p[k-1]) - dot(p[k]-p[k-2],c[k-1])
|
|
// p = received signals
|
|
// c = decisions (constellation points)
|
|
hist[2] = hist[1];
|
|
hist[1] = hist[0];
|
|
#define HIST_FLOAT 0
|
|
#if HIST_FLOAT
|
|
hist[0].p.re = (float)s.re - 128;
|
|
hist[0].p.im = (float)s.im - 128;
|
|
|
|
cu8 cp = arg_to_symbol((symbol_arg & 49152) + 8192);
|
|
hist[0].c.re = (float)cp.re - 128;
|
|
hist[0].c.im = (float)cp.im - 128;
|
|
|
|
float muerr =
|
|
((hist[0].p.re - hist[2].p.re) * hist[1].c.re +
|
|
(hist[0].p.im - hist[2].p.im) * hist[1].c.im) -
|
|
((hist[0].c.re - hist[2].c.re) * hist[1].p.re +
|
|
(hist[0].c.im - hist[2].c.im) * hist[1].p.im);
|
|
#else
|
|
hist[0].p = s;
|
|
hist[0].c = arg_to_symbol((symbol_arg & 49152) + 8192);
|
|
|
|
int muerr =
|
|
((signed char)(hist[0].p.re - hist[2].p.re) * ((int)hist[1].c.re - 128) + (signed char)(hist[0].p.im - hist[2].p.im) * ((int)hist[1].c.im - 128)) - ((signed char)(hist[0].c.re - hist[2].c.re) * ((int)hist[1].p.re - 128) + (signed char)(hist[0].c.im - hist[2].c.im) * ((int)hist[1].p.im - 128));
|
|
#endif
|
|
float mucorr = muerr * gain_mu;
|
|
const float max_mucorr = 0.1;
|
|
// TBD Optimize out statically
|
|
if (mucorr < -max_mucorr)
|
|
mucorr = -max_mucorr;
|
|
if (mucorr > max_mucorr)
|
|
mucorr = max_mucorr;
|
|
mu += mucorr;
|
|
mu += omega; // Next symbol time;
|
|
} // mu<1
|
|
|
|
// Next sample
|
|
++pin;
|
|
--mu;
|
|
phase += freqw;
|
|
} // chunk_size
|
|
|
|
in.read(pin - pin0);
|
|
out.written(pout - pout0);
|
|
|
|
if (symbol_arg && cstln_out)
|
|
// Output the last interpolated PSK symbol, max once per chunk_size
|
|
cstln_out->write(s);
|
|
|
|
// This is best done periodically ouside the inner loop,
|
|
// but will cause non-deterministic output.
|
|
|
|
if (!allow_drift)
|
|
{
|
|
if (freqw < min_freqw || freqw > max_freqw)
|
|
freqw = (max_freqw + min_freqw) / 2;
|
|
}
|
|
|
|
// Output measurements
|
|
|
|
meas_count += pin - pin0;
|
|
|
|
while (meas_count >= meas_decimation)
|
|
{
|
|
meas_count -= meas_decimation;
|
|
|
|
if (freq_out)
|
|
freq_out->write((float)freqw / 65536);
|
|
}
|
|
|
|
} // Work to do
|
|
}
|
|
|
|
private:
|
|
struct polar
|
|
{
|
|
u_angle a;
|
|
unsigned char r;
|
|
} lut_polar[256][256];
|
|
|
|
u_angle fast_arg(const cu8 &c)
|
|
{
|
|
// TBD read cu8 as u16 index, same endianness as in init()
|
|
return lut_polar[c.re][c.im].a;
|
|
}
|
|
|
|
cu8 lut_rect[RLUT_ANGLES][256];
|
|
cu8 lut_sincos[65536];
|
|
|
|
cu8 arg_to_symbol(u_angle a)
|
|
{
|
|
return lut_sincos[a];
|
|
}
|
|
|
|
void init_lookup_tables()
|
|
{
|
|
for (int i = 0; i < 256; ++i)
|
|
{
|
|
for (int q = 0; q < 256; ++q)
|
|
{
|
|
// Don't cast float to unsigned directly
|
|
lut_polar[i][q].a = (s_angle)(atan2f(q - 128, i - 128) * 65536 / (2 * M_PI));
|
|
lut_polar[i][q].r = (int)hypotf(i - 128, q - 128);
|
|
}
|
|
}
|
|
|
|
for (unsigned long a = 0; a < 65536; ++a)
|
|
{
|
|
float f = 2 * M_PI * a / 65536;
|
|
lut_sincos[a].re = 128 + cstln_amp * cosf(f);
|
|
lut_sincos[a].im = 128 + cstln_amp * sinf(f);
|
|
}
|
|
|
|
for (int a = 0; a < RLUT_ANGLES; ++a)
|
|
{
|
|
for (int r = 0; r < 256; ++r)
|
|
{
|
|
lut_rect[a][r].re = (int)(128 + r * cos(2 * M_PI * a / RLUT_ANGLES));
|
|
lut_rect[a][r].im = (int)(128 + r * sin(2 * M_PI * a / RLUT_ANGLES));
|
|
}
|
|
}
|
|
}
|
|
|
|
struct
|
|
{
|
|
#if HIST_FLOAT
|
|
complex<float> p; // Received symbol
|
|
complex<float> c; // Matched constellation point
|
|
#else
|
|
cu8 p; // Received symbol
|
|
cu8 c; // Matched constellation point
|
|
#endif
|
|
} hist[3];
|
|
|
|
pipereader<cu8> in;
|
|
pipewriter<hardsymbol> out;
|
|
float mu; // PSK time expressed in clock ticks. TBD fixed point.
|
|
u_angle phase;
|
|
unsigned long meas_count;
|
|
pipewriter<float> *freq_out, *mer_out;
|
|
pipewriter<cu8> *cstln_out;
|
|
};
|
|
// fast_qpsk_receiver
|
|
|
|
// CONSTELLATION TRANSMITTER
|
|
|
|
// Maps symbols to I/Q points.
|
|
|
|
template <typename Tout, int Zout>
|
|
struct cstln_transmitter : runnable
|
|
{
|
|
cstln_lut<hard_ss, 256> *cstln;
|
|
|
|
cstln_transmitter(scheduler *sch,
|
|
pipebuf<u8> &_in,
|
|
pipebuf<complex<Tout>> &_out) : runnable(sch, "cstln_transmitter"),
|
|
in(_in),
|
|
out(_out),
|
|
cstln(0)
|
|
{
|
|
}
|
|
|
|
void run()
|
|
{
|
|
if (!cstln)
|
|
fail("constellation not set");
|
|
|
|
int count = min(in.readable(), out.writable());
|
|
u8 *pin = in.rd(), *pend = pin + count;
|
|
complex<Tout> *pout = out.wr();
|
|
|
|
for (; pin < pend; ++pin, ++pout)
|
|
{
|
|
complex<signed char> *cp = &cstln->symbols[*pin];
|
|
pout->re = Zout + cp->re;
|
|
pout->im = Zout + cp->im;
|
|
}
|
|
|
|
in.read(count);
|
|
out.written(count);
|
|
}
|
|
|
|
private:
|
|
pipereader<u8> in;
|
|
pipewriter<complex<Tout>> out;
|
|
};
|
|
// cstln_transmitter
|
|
|
|
// FREQUENCY SHIFTER
|
|
|
|
// Resolution is sample_freq/65536.
|
|
|
|
template <typename T>
|
|
struct rotator : runnable
|
|
{
|
|
rotator(scheduler *sch,
|
|
pipebuf<complex<T>> &_in,
|
|
pipebuf<complex<T>> &_out,
|
|
float freq) : runnable(sch, "rotator"),
|
|
in(_in),
|
|
out(_out),
|
|
index(0)
|
|
{
|
|
int ifreq = freq * 65536;
|
|
if (sch->debug)
|
|
fprintf(stderr, "Rotate: req=%f real=%f\n", freq, ifreq / 65536.0);
|
|
|
|
for (int i = 0; i < 65536; ++i)
|
|
{
|
|
lut_cos[i] = cosf(2 * M_PI * i * ifreq / 65536);
|
|
lut_sin[i] = sinf(2 * M_PI * i * ifreq / 65536);
|
|
}
|
|
}
|
|
|
|
void run()
|
|
{
|
|
unsigned long count = min(in.readable(), out.writable());
|
|
complex<T> *pin = in.rd(), *pend = pin + count;
|
|
complex<T> *pout = out.wr();
|
|
|
|
for (; pin < pend; ++pin, ++pout, ++index)
|
|
{
|
|
float c = lut_cos[index];
|
|
float s = lut_sin[index];
|
|
pout->re = pin->re * c - pin->im * s;
|
|
pout->im = pin->re * s + pin->im * c;
|
|
}
|
|
|
|
in.read(count);
|
|
out.written(count);
|
|
}
|
|
|
|
private:
|
|
pipereader<complex<T>> in;
|
|
pipewriter<complex<T>> out;
|
|
float lut_cos[65536];
|
|
float lut_sin[65536];
|
|
unsigned short index; // Current phase
|
|
};
|
|
// rotator
|
|
|
|
// SPECTRUM-BASED CNR ESTIMATOR
|
|
|
|
// Assumes that the spectrum is as follows:
|
|
//
|
|
// ---|--noise---|-roll-off-|---carrier+noise----|-roll-off-|---noise--|---
|
|
// | (bw/2) | (bw) | (bw/2) | (bw) | (bw/2) |
|
|
//
|
|
// Maximum roll-off 0.5
|
|
|
|
template <typename T>
|
|
struct cnr_fft : runnable
|
|
{
|
|
cnr_fft(scheduler *sch,
|
|
pipebuf<complex<T>> &_in,
|
|
pipebuf<float> &_out,
|
|
float _bandwidth, int nfft = 4096) : runnable(sch, "cnr_fft"),
|
|
bandwidth(_bandwidth),
|
|
freq_tap(NULL),
|
|
tap_multiplier(1),
|
|
decimation(1048576),
|
|
kavg(0.1),
|
|
in(_in),
|
|
out(_out),
|
|
fft(nfft),
|
|
avgpower(NULL),
|
|
phase(0)
|
|
{
|
|
if (bandwidth > 0.25)
|
|
fail("CNR estimator requires Fsampling > 4x Fsignal");
|
|
}
|
|
|
|
float bandwidth;
|
|
float *freq_tap, tap_multiplier;
|
|
int decimation;
|
|
float kavg;
|
|
|
|
void run()
|
|
{
|
|
while (in.readable() >= fft.n && out.writable() >= 1)
|
|
{
|
|
phase += fft.n;
|
|
|
|
if (phase >= decimation)
|
|
{
|
|
phase -= decimation;
|
|
do_cnr();
|
|
}
|
|
|
|
in.read(fft.n);
|
|
}
|
|
}
|
|
|
|
private:
|
|
void do_cnr()
|
|
{
|
|
float center_freq = freq_tap ? *freq_tap * tap_multiplier : 0;
|
|
int icf = floor(center_freq * fft.n + 0.5);
|
|
complex<T> *data = new complex<T>[fft.n];
|
|
memcpy(data, in.rd(), fft.n * sizeof(data[0]));
|
|
fft.inplace(data, true);
|
|
T *power = new T[fft.n];
|
|
|
|
for (int i = 0; i < fft.n; ++i)
|
|
power[i] = data[i].re * data[i].re + data[i].im * data[i].im;
|
|
|
|
if (!avgpower)
|
|
{
|
|
// Initialize with first spectrum
|
|
avgpower = new T[fft.n];
|
|
memcpy(avgpower, power, fft.n * sizeof(avgpower[0]));
|
|
}
|
|
|
|
// Accumulate and low-pass filter
|
|
for (int i = 0; i < fft.n; ++i)
|
|
avgpower[i] = avgpower[i] * (1 - kavg) + power[i] * kavg;
|
|
|
|
int bwslots = (bandwidth / 4) * fft.n;
|
|
|
|
if (!bwslots)
|
|
{
|
|
delete[] power;
|
|
delete[] data;
|
|
return;
|
|
}
|
|
|
|
// Measure carrier+noise in center band
|
|
float c2plusn2 = avgslots(icf - bwslots, icf + bwslots);
|
|
// Measure noise left and right of roll-off zones
|
|
float n2 = (avgslots(icf - bwslots * 4, icf - bwslots * 3) + avgslots(icf + bwslots * 3, icf + bwslots * 4)) / 2;
|
|
float c2 = c2plusn2 - n2;
|
|
float cnr = (c2 > 0 && n2 > 0) ? 10 * logf(c2 / n2) / logf(10) : -50;
|
|
out.write(cnr);
|
|
delete[] power;
|
|
delete[] data;
|
|
}
|
|
|
|
float avgslots(int i0, int i1)
|
|
{ // i0 <= i1
|
|
T s = 0;
|
|
|
|
for (int i = i0; i <= i1; ++i)
|
|
s += avgpower[i & (fft.n - 1)];
|
|
|
|
return s / (i1 - i0 + 1);
|
|
}
|
|
|
|
pipereader<complex<T>> in;
|
|
pipewriter<float> out;
|
|
cfft_engine<T> fft;
|
|
T *avgpower;
|
|
int phase;
|
|
};
|
|
// cnr_fft
|
|
|
|
template <typename T, int NFFT>
|
|
struct spectrum : runnable
|
|
{
|
|
spectrum(scheduler *sch,
|
|
pipebuf<complex<T>> &_in,
|
|
pipebuf<float[NFFT]> &_out) : runnable(sch, "spectrum"),
|
|
decimation(1048576),
|
|
kavg(0.1),
|
|
decim(1), in(in),
|
|
out(_out),
|
|
fft(NFFT),
|
|
avgpower(NULL),
|
|
phase(0)
|
|
{
|
|
}
|
|
|
|
int decimation;
|
|
float kavg;
|
|
int decim;
|
|
|
|
void run()
|
|
{
|
|
while (in.readable() >= fft.n * decim && out.writable() >= 1)
|
|
{
|
|
phase += fft.n * decim;
|
|
|
|
if (phase >= decimation)
|
|
{
|
|
phase -= decimation;
|
|
do_spectrum();
|
|
}
|
|
|
|
in.read(fft.n * decim);
|
|
}
|
|
}
|
|
|
|
private:
|
|
void do_spectrum()
|
|
{
|
|
complex<T> data[fft.n];
|
|
|
|
if (decim == 1)
|
|
{
|
|
memcpy(data, in.rd(), fft.n * sizeof(data[0]));
|
|
}
|
|
else
|
|
{
|
|
complex<T> *pin = in.rd();
|
|
|
|
for (int i = 0; i < fft.n; ++i, pin += decim)
|
|
data[i] = *pin;
|
|
}
|
|
|
|
fft.inplace(data, true);
|
|
float power[NFFT];
|
|
|
|
for (int i = 0; i < fft.n; ++i)
|
|
power[i] = (float)data[i].re * data[i].re + (float)data[i].im * data[i].im;
|
|
|
|
if (!avgpower)
|
|
{
|
|
// Initialize with first spectrum
|
|
avgpower = new float[fft.n];
|
|
memcpy(avgpower, power, fft.n * sizeof(avgpower[0]));
|
|
}
|
|
|
|
// Accumulate and low-pass filter
|
|
for (int i = 0; i < fft.n; ++i)
|
|
avgpower[i] = avgpower[i] * (1 - kavg) + power[i] * kavg;
|
|
|
|
// Reuse power[]
|
|
for (int i = 0; i < fft.n / 2; ++i)
|
|
{
|
|
power[i] = 10 * log10f(avgpower[NFFT / 2 + i]);
|
|
power[NFFT / 2 + i] = 10 * log10f(avgpower[i]);
|
|
}
|
|
|
|
memcpy(out.wr(), power, sizeof(power[0]) * NFFT);
|
|
out.written(1);
|
|
}
|
|
|
|
pipereader<complex<T>> in;
|
|
pipewriter<float[NFFT]> out;
|
|
cfft_engine<T> fft;
|
|
T *avgpower;
|
|
int phase;
|
|
};
|
|
// spectrum
|
|
|
|
} // namespace leansdr
|
|
|
|
#endif // LEANSDR_SDR_H
|