diff --git a/src/psk31.cc b/src/psk31.cc index 7f7aea4..c565854 100644 --- a/src/psk31.cc +++ b/src/psk31.cc @@ -7,6 +7,7 @@ using namespace sdr; Varicode::Varicode() : Sink(), Source() { + // Fill code table _code_table[1023] = '!'; _code_table[87] = '.'; _code_table[895] = '\''; _code_table[367] = '*'; _code_table[495] = '\\'; _code_table[687] = '?'; _code_table[475] = '$'; _code_table[701] = '@'; _code_table[365] = '_'; @@ -17,6 +18,8 @@ Varicode::Varicode() _code_table[693] = '}'; _code_table[503] = ')'; _code_table[1749] = '%'; _code_table[471] = '>'; _code_table[991] = '+'; _code_table[251] = '['; _code_table[85] = '='; _code_table[943] = '/'; _code_table[29] = '\n'; + _code_table[31] = '\r'; _code_table[747] = '\n'; + // ^- Encode EOT as LN _code_table[443] = '|'; _code_table[1] = ' '; _code_table[125] = 'A'; _code_table[235] = 'B'; _code_table[173] = 'C'; _code_table[181] = 'D'; _code_table[119] = 'E'; _code_table[219] = 'F'; _code_table[253] = 'G'; @@ -35,8 +38,7 @@ Varicode::Varicode() _code_table[21] = 'r'; _code_table[23] = 's'; _code_table[5] = 't'; _code_table[55] = 'u'; _code_table[123] = 'v'; _code_table[107] = 'w'; _code_table[223] = 'x'; _code_table[93] = 'y'; _code_table[469] = 'z'; - _code_table[183] = '0'; _code_table[445] = '1'; _code_table[237] = '2'; - // ^- Collides with ';'! + _code_table[183] = '0'; _code_table[189] = '1'; _code_table[237] = '2'; _code_table[511] = '3'; _code_table[375] = '4'; _code_table[859] = '5'; _code_table[363] = '6'; _code_table[941] = '7'; _code_table[427] = '8'; _code_table[951] = '9'; diff --git a/src/psk31.hh b/src/psk31.hh index e838867..150338e 100644 --- a/src/psk31.hh +++ b/src/psk31.hh @@ -8,33 +8,55 @@ namespace sdr { -/** A simple BPSK31 "demodulator". */ +/** A simple BPSK31 "demodulator". This node consumes a complex input stream with a sample-rate of + * at least 2000Hz and produces a bitstream with 31.25 Hz "sample-rate". Use the @c Varicode node + * to decode this bitstream to ASCII chars. The BPSK31 signal should be centered around 0Hz, this + * node uses a simple PLL to adjust for small detunings. */ template class BPSK31: public Sink< std::complex >, public Source { public: + /** Constructs a new BPSK31 demodulator. + * + * This node first subsamples the input signal to a multiple of 31.25 Hz (by default to + * 2000Hz = 64*31.25) using an interpolating sub-sampler. Therefore, the input signal must be + * filtered sufficiently well to avoid artifacts of the interpolating sub-sampler. + * Then, the phase-constellation of the signal is determined as either -pi or pi while the + * frequency of the carrier is tracked using a simple PLL. Finally, the BPSK31 bit stream is + * decoded by detecting a phase-change (0) or not (1). + * + * @note This node uses floating point arithmetic, hence it should not be used on streams with + * a high sample rate! Which is not neccessary as it only decodes a BPSK signal with approx. + * 31 baud. + * + * @param dF Specfies the (relative anglular) frequency range of the PLL to adjust for small + * deviations of the BPSK31 signal from 0Hz. */ BPSK31(double dF=0.1) : Sink< std::complex >(), Source(), _P(0), _F(0), _Fmin(-dF), _Fmax(dF) { + // Assemble carrier PLL gains: double damping = std::sqrt(2)/2; double bw = M_PI/100; double tmp = 1. + 2*damping*bw + bw*bw; _alpha = 4*damping*bw/tmp; _beta = 4*bw*bw/tmp; - // Init Delay line + // Init delay line for the interpolating sub-sampler _dl = Buffer< std::complex >(2*8); _dl_idx = 0; for (size_t i=0; i<16; i++) { _dl[i] = 0; } + _mu = 0.25; _gain_mu = 0.01; - // Inner PLL: - _theta = 0; _mu = 0.25; _gain_mu = 0.01; + // constant phase shift of the signal + _theta = 0; + + // Phase detection: _omega = _min_omega = _max_omega = 0; _omega_rel = 0.001; _gain_omega = 0.001; _p_0T = _p_1T = _p_2T = 0; _c_0T = _c_1T = _c_2T = 0; - // Super-sample (256 samples per symbol) + // Super-sample (64 phase samples per symbol) _superSample = 64; } @@ -42,9 +64,11 @@ public: // pass... } - virtual void config(const Config &src_cfg) { + virtual void config(const Config &src_cfg) + { // Requires type, sample rate & buffer size if (!src_cfg.hasType() || !src_cfg.hasSampleRate() || !src_cfg.hasBufferSize()) { return; } + // Check buffer type if (Config::typeId< std::complex >() != src_cfg.type()) { ConfigError err; @@ -53,18 +77,29 @@ public: throw err; } + // Check input sample rate double Fs = src_cfg.sampleRate(); + if (2000 > Fs) { + ConfigError err; + err << "Can not configure BPSK31: Input sample rate too low! The BPSK31 node requires at " + << "least a sample rate of 2000Hz, got " << Fs << "Hz"; + throw err; + } + // Compute samples per symbol _omega = Fs/(_superSample*31.25); + // Obtain limits for the sub-sample rate _min_omega = _omega*(1.0 - _omega_rel); _max_omega = _omega*(1.0 + _omega_rel); - _omega_mid = 0.5*(_min_omega+_max_omega); + // Decoding history, contains up to 64 phases (if in sync) _hist = Buffer(_superSample); _hist_idx = 0; _last_constellation = 1; - _buffer = Buffer(src_cfg.bufferSize()); + // Output buffer + size_t bsize = 1 + int(Fs/31.25); + _buffer = Buffer(bsize); // This node sends a bit-stream with 31.25 baud. this->setConfig(Config(Traits::scalarId, 31.25, src_cfg.bufferSize(), 1)); @@ -80,10 +115,15 @@ public: } // Then, ... if (i sample = interpolate(_dl.sub(_dl_idx, 8), _mu); + // Update error tracking _errorTracking(sample); + // Update carrier PLL _updatePLL(sample); // done. real(sample) constains phase of symbol + + // Now fill history with phase constellation and try to decode a bit _hist[_hist_idx] = std::real(sample); // Check if there is a constellation transition at the current time if ((_hist_idx>1) && (_hasTransition())) { @@ -109,8 +149,8 @@ public: } } } - - this->send(_buffer.head(o)); + // If at least 1 bit was decoded -> send result + if (o>0) { this->send(_buffer.head(o)); } } @@ -145,77 +185,113 @@ protected: } inline void _updateSampler(const std::complex &value) { - _mu-=1; _P += _F; + // decrease fractional sub-sample counter + _mu-=1; + // Update phase + _P += _F; + // Limit phase while (_P>( 2*M_PI)) { _P -= 2*M_PI; } while (_P<(-2*M_PI)) { _P += 2*M_PI; } + // Calc down-conversion factor (consider look-up table) std::complex fac = std::exp(std::complex(0, _P+_theta)); + // Down-convert singal std::complex sample = fac * std::complex(value.real(), value.imag()); + // store sample into delay line _dl[_dl_idx] = sample; _dl[_dl_idx+8] = sample; _dl_idx = (_dl_idx + 1) % 8; } inline void _errorTracking(const std::complex &sample) { + // Update last 2 constellation and phases _p_2T = _p_1T; _p_1T = _p_0T; _p_0T = sample; _c_2T = _c_1T; _c_1T = _c_0T; _c_0T = (sample.real() > 0) ? -1 : 1; - + // Compute difference between phase and constellation std::complex x = (_c_0T - _c_2T) * std::conj(_p_1T); std::complex y = (_p_0T - _p_2T) * std::conj(_c_1T); + // Get phase error float err = std::real(y-x); if (err > 1.0) { err = 1.0; } if (err < -1.0) { err = -1.0; } + // Update symbol rate (approx 31.25*64) _omega += _gain_omega * err; - if ( (_omega-_omega_mid) > _omega_rel) { _omega = _omega_mid + _omega_rel; } - else if ( (_omega-_omega_mid) < -_omega_rel) { _omega = _omega_mid - _omega_rel; } - else { _omega = _omega_mid + _omega-_omega_mid; } + // Limit omega on _omega_mid +/- _omega*_omega_rel + _omega = std::max(_min_omega, std::min(_max_omega, _omega)); + // Update mu as +_omega (samples per symbol) and a small correction term _mu += _omega + _gain_mu * err; } protected: - // Carrier PLL stuff - float _P, _F, _Fmin, _Fmax; - float _alpha, _beta; - // Delay line - Buffer< std::complex > _dl; - size_t _dl_idx; - // Second PLL - float _theta; - float _mu, _gain_mu; - float _omega, _min_omega, _max_omega; - float _omega_mid, _omega_rel, _gain_omega; - std::complex _p_0T, _p_1T, _p_2T; - std::complex _c_0T, _c_1T, _c_2T; - // Third "PLL" -> bit decoder + /** Holds the number of phase constellations per bit. */ size_t _superSample; + /** Phase of the carrier PLL. */ + float _P; + /** Frequency of the carrier PLL. */ + float _F; + /** Upper and lower frequency limit of the carrier PLL. */ + float _Fmin, _Fmax; + /** Gain factors of the carrier PLL. */ + float _alpha, _beta; + /** The delay line for the interpolating sub-sampler. */ + Buffer< std::complex > _dl; + /** The current index of the delay line. */ + size_t _dl_idx; + /** Holds the fractional sub-sampling counter. */ + float _mu; + /** Gain factor of the sub-sampler. */ + float _gain_mu; + /** Constant phase shift between real axis and first constellation. (Currently unused). */ + float _theta; + /** Current sub-sample rate. */ + float _omega; + /** Relative error of the subsample rate. */ + float _omega_rel; + /** Limits of the sub-sample rate. */ + float _min_omega, _max_omega; + /** Gain of the sub-sample rate correction. */ + float _gain_omega; + /** Last 3 phases. */ + std::complex _p_0T, _p_1T, _p_2T; + /** Last 3 constellations. */ + std::complex _c_0T, _c_1T, _c_2T; + /** The last @c _superSample phases. */ Buffer _hist; + /** Current phase history index. */ size_t _hist_idx; + /** The last output constellation. */ int _last_constellation; - // Output buffer + /** Output buffer. */ Buffer _buffer; }; +/** Simple varicode (Huffman code) decoder node. It consumes a bit-stream (uint8_t) + * and produces a uint8_t stream of ascii chars. Non-printable chars (except for new-line) are + * ignored. The output stream has no samplerate! */ class Varicode: public Sink, public Source { public: + /** Constructor. */ Varicode(); + /** Destructor. */ virtual ~Varicode(); - + /** Configures the node. */ virtual void config(const Config &src_cfg); - + /** Converts the input bit stream to ASCII chars. */ virtual void process(const Buffer &buffer, bool allow_overwrite); protected: + /** The shift register of the last received bits. */ uint16_t _value; + /** The output buffer. */ Buffer _buffer; + /** The conversion table. */ std::map _code_table; }; - - }