Skip to content

File mpsk_receiver_core.h

FileList > inc > mpsk_receiver > mpsk_receiver_core.h

Go to the source code of this file

Pulse-shaped M-PSK receiver: a tuned matched front end and two loops. More...

  • #include "clib_common.h"
  • #include "ddc/ddc_core.h"
  • #include "ddcr/ddcr_core.h"
  • #include "dp_state.h"
  • #include "jm_perf.h"
  • #include "mpsk_receiver/mpsk_rx_loops.h"
  • #include <complex.h>
  • #include "ratesync/ratesync_core.h"
  • #include "RateConverter/RateConverter_core.h"
  • #include "resamp/resamp_core.h"
  • #include "hbdecim/hbdecim_core.h"
  • #include "cic/cic_core.h"
  • #include "fir/fir_core.h"
  • #include "resample/resample_core.h"
  • #include "lo/lo_core.h"
  • #include "nco/nco_core.h"
  • #include "loop_filter/loop_filter_core.h"
  • #include "lockdet/lockdet_core.h"
  • #include "symsync/symsync_core.h"
  • #include "agc/agc_core.h"
  • #include "dp_tlm/dp_tlm_core.h"
  • #include "ber/ber_core.h"
  • #include "telemetry/telemetry_core.h"
  • #include "boxcar/boxcar_core.h"

Classes

Type Name
struct mpsk_receiver_state_t
M-PSK receiver state.

Public Functions

Type Name
size_t mpsk_receiver_bits (mpsk_receiver_state_t * state, const float _Complex * x, size_t x_len, uint8_t * out, size_t max_out)
Demodulate a cf32 block and emit hard Gray-coded bits.
size_t mpsk_receiver_bits_max_out (mpsk_receiver_state_t * state)
size_t mpsk_receiver_bits_real (mpsk_receiver_state_t * state, const float * x, size_t x_len, uint8_t * out, size_t max_out)
Demodulate a real f32 block and emit hard Gray-coded bits.
size_t mpsk_receiver_bits_real_max_out (mpsk_receiver_state_t * state)
mpsk_receiver_state_t * mpsk_receiver_create (int m, double sps, size_t m_out, int pulse, double rrc_beta, int rrc_span, double bn_carrier, double zeta, double bn_timing, double lock_thresh, double init_norm_freq, int differential, size_t num_phases, int agc, double bn_agc_ratio)
Create an M-PSK receiver.
mpsk_receiver_state_t * mpsk_receiver_create_bpsk (double sample_rate_hz, double symbol_rate_hz, double carrier_freq_hz, int pulse, double rrc_beta, int rrc_span, double bn_carrier, double bn_timing, int differential, int agc)
A BPSK receiver stated in the units a caller actually holds: Hz.
mpsk_receiver_state_t * mpsk_receiver_create_real (int m, double sps, size_t m_out, int pulse, double rrc_beta, int rrc_span, double bn_carrier, double zeta, double bn_timing, double lock_thresh, double init_norm_freq, int differential, size_t num_phases, int agc, double bn_agc_ratio)
Create the same receiver behind an R2C halfband: a real IF in.
void mpsk_receiver_destroy (mpsk_receiver_state_t * state)
Destroy an M-PSK receiver and release all memory.
double mpsk_receiver_get_agc_gain_db (const mpsk_receiver_state_t * state)
Gain the front end's AGC is applying, in dB; 0.0 when agc = 0.
double mpsk_receiver_get_bn_agc_ratio (const mpsk_receiver_state_t * state)
AGC bandwidth ratio in use — derived unless pinned (§8.1).
int mpsk_receiver_get_clipped (const mpsk_receiver_state_t * state)
Has the cascade's CIC stage clipped its input since the last reset? A CIC bounds its input to +-1.0 and clips silently past that, which costs ~25 dB of EVM behind a perfectly healthy lock.
double mpsk_receiver_get_last_error (const mpsk_receiver_state_t * state)
Carrier loop phase discriminator (rad) — the residual phase the loop is trying to null; loop stress.
double mpsk_receiver_get_lock (const mpsk_receiver_state_t * state)
double mpsk_receiver_get_lock_drop_thresh (const mpsk_receiver_state_t * state)
Carrier DROP threshold in use — MPSK_RX_LOCK_DOWN x the declare threshold, the level hysteresis the pair is stated with.
double mpsk_receiver_get_lock_thresh (const mpsk_receiver_state_t * state)
Carrier lock DECLARE threshold in use — derived unless pinned (§8.1). It gates no loop and no output; see mpsk_rx_loops.h .
int64_t mpsk_receiver_get_lock_time (const mpsk_receiver_state_t * state)
Symbols from reset to the FIRST carrier-lock declaration, or -1 if the receiver has not locked yet.
int mpsk_receiver_get_locked (const mpsk_receiver_state_t * state)
Binary carrier-lock flag from the loop's hysteretic (up/down verify-counted) lock detector — de-chattered, unlike the raw metric.
int mpsk_receiver_get_m (const mpsk_receiver_state_t * state)
size_t mpsk_receiver_get_m_out (const mpsk_receiver_state_t * state)
Terminal outputs per symbol (the old n , now the cascade's).
double mpsk_receiver_get_nco_freq (const mpsk_receiver_state_t * state)
Instantaneous NCO frequency command (carrier loop filter output, cycles/sample): mean tracks a ramp with no lag, variance is loop stress.
double mpsk_receiver_get_norm_freq (const mpsk_receiver_state_t * state)
Carrier frequency the receiver is tracking, cycles/sample at the input rate: the create-time centre plus the loop's own estimate.
size_t mpsk_receiver_get_num_phases (const mpsk_receiver_state_t * state)
Matched-filter bank arms in use — derived unless pinned (§8.1).
double mpsk_receiver_get_sps (const mpsk_receiver_state_t * state)
void mpsk_receiver_get_state (const mpsk_receiver_state_t * state, void * blob)
double mpsk_receiver_get_sync_lock_drop_thresh (const mpsk_receiver_state_t * state)
Timing DROP threshold on sync.lock . Equal to the declare threshold when the timing loop carries no level hysteresis.
double mpsk_receiver_get_sync_lock_thresh (const mpsk_receiver_state_t * state)
Timing DECLARE threshold on sync.lock , derived by symsync's own (rolloff, esno_min, pfa, pd) geometry rather than pinned.
double mpsk_receiver_get_timing_rate (const mpsk_receiver_state_t * state)
Smoothed tracked samples per symbol — departs from the nominal sps by exactly the sample-clock offset the timing loop is tracking.
double mpsk_receiver_get_zeta (const mpsk_receiver_state_t * state)
Loop damping in use — derived 1/sqrt(2) unless pinned (§8.1).
void mpsk_receiver_reset (mpsk_receiver_state_t * state)
Re-seed the front end and both loops to their create-time state.
void mpsk_receiver_set_norm_freq (mpsk_receiver_state_t * state, double val)
Retune to val cycles/sample: moves the LO centre there and zeroes the loop's residual estimate, so norm_freq reads back exactly.
int mpsk_receiver_set_state (mpsk_receiver_state_t * state, const void * blob)
int mpsk_receiver_set_telemetry (mpsk_receiver_state_t * state, dp_tlm_t * tlm, const char * prefix, uint32_t decim)
Attach (or detach) a telemetry context across the receiver. Registers the receiver's own "<prefix>.lock" probe (the carrier lock EMA), then the carrier loop's "<prefix>.car.e" / ".freq" / ".locked" and the symbol-timing loop's "<prefix>.sync.e" / ".ctrl" / ".rate" / ".lock" / ".locked" / ".mu" ten probes emitted once per recovered symbol then the front end's AGC under "<prefix>.agc" ("<prefix>.agc.gain_db" and "<prefix>.agc.level_db"; seeagc_set_telemetry() ). Twelve probes total, all thinned bydecim . Passing NULL detaches everything.
size_t mpsk_receiver_state_bytes (const mpsk_receiver_state_t * state)
JM_FORCEINLINE JM_HOT int mpsk_receiver_step_real_ted (mpsk_receiver_state_t * s, float x, float _Complex * y_out, int ted)
Push one REAL input sample; emit a symbol if it completed one.
JM_FORCEINLINE JM_HOT int mpsk_receiver_step_ted (mpsk_receiver_state_t * s, float _Complex x, float _Complex * y_out, int ted)
Push one input sample; emit a symbol if it completed one.
size_t mpsk_receiver_steps (mpsk_receiver_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out)
Demodulate a cf32 block and emit the recovered symbols.
size_t mpsk_receiver_steps_max_out (mpsk_receiver_state_t * state)
size_t mpsk_receiver_steps_real (mpsk_receiver_state_t * state, const float * x, size_t x_len, float _Complex * out, size_t max_out)
Demodulate a real f32 block and emit the recovered symbols.
size_t mpsk_receiver_steps_real_max_out (mpsk_receiver_state_t * state)

Macros

Type Name
define MPSK_RECEIVER_R_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('M', 'P', 'S', 'R')
define MPSK_RECEIVER_R_STATE_VERSION 2u
define MPSK_RECEIVER_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('M', 'P', 'S', 'K')
define MPSK_RECEIVER_STATE_VERSION 6u /\* v5: rebuilt on the matched DDC \*/

Detailed Description

A complete inline modem for a continuous (unspread) M-PSK signal at any input rate. It is the top of the polyphase family, and it is composition rather than machinery — it owns no filter, no NCO and no interpolator of its own:

x ──> MatchedDDC ──────────────────────────────> y ──> loops ──> symbols
       LO mix · CIC/HB cascade · matched filter        
         ^                            ^                
         └── freq_ctrl ── carrier ────┴── rate_ctrl ───┘

One object, two front ends

A real IF — the usual output of a single-ended ADC — is the same receiver behind an R2C halfband, and it is a real flag on this state rather than a second type:

f32 in ──> MatchedDdcr ────────────────────────> y ──> the SAME loops
            halfband R2C (2:1) · LO mix · cascade · MF

Every loop, discriminator and demapper decision is one implementation over one mpsk_rx_loops_t. What the front end changes is exactly three things, and each is a rate convention rather than an algorithm:

  • The LO runs at half the input rate. The R2C halfband decimates 2:1 (with the fs/4 shift baked in) before the mix, so the LO sees sps/2 samples per symbol — which is why mpsk_rx_loops_t takes lo_sps separately from sps. norm_freq stays caller-facing in cycles/sample at the input rate, so the real face halves it on the way in and doubles it on the way out. Ddcr's tuning law is norm_freq = -(2*f_c + 0.5).
  • **sps must exceed 2 * m_out,** strictly, against sps >= m_out for the complex face: the cascade behind the halfband runs at twice the overall rate and Ddcr requires that below 0.5.
  • **init_norm_freq means the real IF centre** rather than a baseband residual.

The hot path is not tagged. There are two step entry points, each force- inlined onto mpsk_rx_fold, so the front end is a compile-time fact inside the sample loop and real is read only on cold paths (destroy, reset, telemetry, the frequency accessors and the state triplet).

  • ddc_state_t (the matched flavor) mixes, decimates and matched-filters in the dot products it was already doing. Its terminal polyphase stage IS the matched filter, and the arm that stage selects IS the fractional symbol-timing delay.
  • mpsk_rx_loops_t closes a symbol-timing loop on the cascade's rate_ctrl port and a carrier loop on the LO's freq_ctrl port. The timing half is ratesync_loop_t — literally RateSync's loop, not a copy of it.

Carrier recovery follows the project rule, now structurally rather than by convention: predetection de-rotation happens in the LO at the front of the chain, and postdetection discrimination on the matched-filtered symbols at the end of it. ONE discriminator steers the one LO: the NDA M-th-power error on the on-time strobe, needing no data and no symbol timing, running from the first symbol to the last.

There is no acquisition/tracking handover to a decision-directed error. There was one, opt-in, until doppler#877 measured what it bought: across the ten paired cells where it engaged it moved 99% of the recovered symbols and changed the symbol error rate by a mean factor of 0.9999 (t = 0.28). See mpsk_rx_loops.h for why the strobe is the only sample either discriminator could have read.

What the cascade buys

sps is a double, and the front end plans itself. At sps = 8 the plan is a halfband or two and a terminal stage; at sps = 256 it is a CIC followed by the same terminal stage, so the matched filter costs the same bank either way (~34 taps/arm at both ends of a 64x span of input rates, against the 4225 taps/arm a single-stage design would need). An irrational sps — a free-running ADC clock against the symbol clock — is no harder than an integer one, because the terminal accumulator is a double and the loop only has to steer the strobe.

The M-fold phase ambiguity is unchanged: resolve it with differential demapping (bits(..., differential=1)) or a sync word downstream. A DSSS-MPSK receiver is still Dll(segments) -> MpskReceiver.

Warning:

This object's outputs are not bit-identical to releases before the cascade rebuild. The matched filter became a polyphase bank instead of a dense FIR and the interpolator became a bank arm instead of a Farrow, so symbols move at the float level. bn_carrier also changed units: it is now normalised to the symbol rate, like bn_timing, rather than to the input sample rate — at the old default sps = 8 the same number is now an 8x wider loop. Detection performance is unchanged (the fused matched filter measures on the Es/N0 bound); exact-output pins are not.

Lifecycle: mpsk_receiver_create -> (steps / bits / reset)* -> _destroy.

Both examples name every argument, in the constructor's own parameter order, so a reader can check a call against the signature without counting commas. Fifteen positional numbers cannot be read, and they were not being read: the complex example below carried SIXTEEN arguments to a fifteen-parameter function one left behind by a retired parameter and said so to nobody, because nothing in the tree compiles a header's example block.

// QPSK, 8 samples/symbol, I&D matched filter, NDA acquisition.
const int    m              = 4;     // QPSK
const double sps            = 8.0;   // samples per symbol
const size_t m_out          = 4;     // terminal outputs per symbol
const int    pulse          = MPSK_RX_PULSE_IANDD;
const double rrc_beta       = 0.35;  // pulse=rrc only; inert for I&D
const int    rrc_span       = 8;     //   "
const double bn_carrier     = 0.01;  // x symbol rate
const double zeta           = 0.707; // critically damped
const double bn_timing      = 0.01;  // x symbol rate
const double lock_thresh    = 0.0;   // 0 derives; MPSK_RX_LOCK_THRESH_DEFAULT
const double init_norm_freq = 0.0;   // no prior carrier estimate
const int    differential   = 0;     // coherent demap
const size_t num_phases     = 0;     // 0 derives 64, the saturation point
const int    agc            = 1;     // one AGC, inside the cascade
const double bn_agc_ratio   = MPSK_RX_AGC_BW_RATIO;

mpsk_receiver_state_t *rx = mpsk_receiver_create (
    m, sps, m_out, pulse, rrc_beta, rrc_span, bn_carrier, zeta,
    bn_timing, lock_thresh, init_norm_freq, differential, num_phases,
    agc, bn_agc_ratio);
float _Complex sym[256];
size_t k = mpsk_receiver_steps (rx, rx_in, rx_len, sym, 256);
double f = mpsk_receiver_get_norm_freq (rx);  // tracked residual carrier
mpsk_receiver_destroy (rx);

The real-IF face is the same call with _real on both ends. It lives here rather than on mpsk_receiver_create_real() because a constructor's own example block is what jm renders as that class's Python Examples, so a C block THERE replaces the Python doctest instead of adding to it. Here it reaches a C reader and costs the Python face nothing.

// QPSK on a real IF at 0.2*fs, 32 samples/symbol, I&D matched filter.
// Same parameters as above; only the three noted in this block differ.
const int    m              = 4;
const double sps            = 32.0;  // > 2 * m_out: the cascade behind
const size_t m_out          = 0;     //   the R2C halfband runs at 2x, and
                                     //   0 derives m_out = 8
const int    pulse          = MPSK_RX_PULSE_IANDD;
const double rrc_beta       = 0.35;
const int    rrc_span       = 8;
const double bn_carrier     = 0.01;
const double zeta           = 0.0;   // 0 derives
const double bn_timing      = 0.01;
const double lock_thresh    = 0.0;   // 0 derives
const double init_norm_freq = 0.2;   // the real IF CENTRE, not a residual
const int    differential   = 0;
const size_t num_phases     = 0;     // 0 derives
const int    agc            = 1;
const double bn_agc_ratio   = 0.0;   // 0 derives

mpsk_receiver_state_t *rx = mpsk_receiver_create_real (
    m, sps, m_out, pulse, rrc_beta, rrc_span, bn_carrier, zeta,
    bn_timing, lock_thresh, init_norm_freq, differential, num_phases,
    agc, bn_agc_ratio);
float _Complex sym[256];
size_t k = mpsk_receiver_steps_real (rx, rx_in, rx_len, sym, 256);
mpsk_receiver_destroy (rx);

Public Functions Documentation

function mpsk_receiver_bits

Demodulate a cf32 block and emit hard Gray-coded bits.

size_t mpsk_receiver_bits (
    mpsk_receiver_state_t * state,
    const float _Complex * x,
    size_t x_len,
    uint8_t * out,
    size_t max_out
) 

Like mpsk_receiver_steps(), but each recovered symbol is sliced to its nearest M-PSK point and unpacked to log2(M) hard bits (LSB-first). With the differential option set at create time, the Gray label is taken from the phase difference between consecutive symbols (rotation-invariant — it resolves the M-fold carrier ambiguity), else from the absolute (coherent) decision.

Parameters:

  • state Receiver state. Must be non-NULL.
  • x Input cf32 samples.
  • x_len Number of input samples.
  • out Output bytes (0/1); caller provides max_out capacity.
  • max_out Output capacity.

Returns:

Number of bits written.

>>> import numpy as np
>>> from doppler.track import MpskReceiver
>>> rng = np.random.default_rng(3)
>>> idx = rng.integers(0, 2, 3000)                  # BPSK payload bits
>>> tx = np.repeat(np.exp(1j * np.pi * idx), 8).astype(np.complex64)
>>> rx = MpskReceiver(m=2, sps=8, m_out=4, bn_carrier=0.005)
>>> b = rx.bits(tx)                                 # 1 hard bit/symbol
>>> b.size
2998
>>> # settled tail matches the payload, up to the BPSK
>>> # inversion ambiguity and the pipeline's one-symbol lead
>>> tail = np.mean(b[1001:2001] != idx[1000:2000])
>>> round(float(min(tail, 1 - tail)), 3)
0.0


function mpsk_receiver_bits_max_out

size_t mpsk_receiver_bits_max_out (
    mpsk_receiver_state_t * state
) 

function mpsk_receiver_bits_real

Demodulate a real f32 block and emit hard Gray-coded bits.

size_t mpsk_receiver_bits_real (
    mpsk_receiver_state_t * state,
    const float * x,
    size_t x_len,
    uint8_t * out,
    size_t max_out
) 

mpsk_receiver_bits() taking real samples. Requires a state built by mpsk_receiver_create_real().

Parameters:

  • state Must be non-NULL.
  • x Real f32 input samples.
  • x_len Number of input samples.
  • out Output bytes (0/1); caller provides max_out capacity.
  • max_out Output capacity.

Returns:

Number of bits written.

>>> import numpy as np
>>> from doppler.track import MpskReceiverR
>>> rng = np.random.default_rng(3)
>>> idx = rng.integers(0, 2, 2400)                  # BPSK payload bits
>>> bb = np.repeat(np.exp(1j * np.pi * idx), 32)
>>> n = np.arange(bb.size)
>>> x = (0.4 * bb * np.exp(2j * np.pi * 0.25 * n)).real  # IF at fs/4
>>> x = np.ascontiguousarray(x.astype(np.float32))
>>> rx = MpskReceiverR(m=2, sps=32, m_out=8, init_norm_freq=0.25,
...                    bn_carrier=0.005)
>>> b = rx.bits(x)                                  # 1 hard bit/symbol
>>> b.size
2398
>>> # settled tail matches the payload, up to the BPSK
>>> # inversion ambiguity
>>> tail = np.mean(b[1500:2300] != idx[1500:2300])
>>> round(float(min(tail, 1 - tail)), 3)
0.0


function mpsk_receiver_bits_real_max_out

size_t mpsk_receiver_bits_real_max_out (
    mpsk_receiver_state_t * state
) 

function mpsk_receiver_create

Create an M-PSK receiver.

mpsk_receiver_state_t * mpsk_receiver_create (
    int m,
    double sps,
    size_t m_out,
    int pulse,
    double rrc_beta,
    int rrc_span,
    double bn_carrier,
    double zeta,
    double bn_timing,
    double lock_thresh,
    double init_norm_freq,
    int differential,
    size_t num_phases,
    int agc,
    double bn_agc_ratio
) 

Parameters:

  • m Constellation order M, 2/4/8 (default 4 = QPSK).
  • sps Samples per symbol; any double >= m_out (8.0 by default, but 17.33389 is equally valid).
  • m_out Terminal outputs per symbol: even, 2..8. 0 (the default) derives it — the largest even count in 2..8 the rate allows, via mpsk_rx_derive_m_out, which is 8 at the default sps = 8; pass a value only to pin one. Read it back with mpsk_receiver_get_m_out(). Gardner needs the half-symbol gate. The derived answer reaches 8 for two reasons. The matched filter: the rectangle is one symbol wide, so its filter is an m_out-tap sum spanning it, and a smaller m_out samples the same integral more coarsely. Measured on QPSK at sps = 8 against EVM_dB = -(Es/N0)_dB, at 18 dB Es/N0: 0.41 dB off the bound at 8, 3.11 dB at 4. And the M-th-power discriminator: z^M auto-convolves the spectrum M times, spreading energy over ~M*Rs, and whatever exceeds the update rate folds back onto itself. A clean strobe raises to a constant with nothing to fold, but every departure from clean (ISI, timing error, noise) is splattered M-fold and aliased — so the nonlinearity's tolerance for a coarse matched filter COLLAPSES as M grows. The first reason is M-independent; the second is not. Measured (halving m_out from 8 to 4, each M at its own SER=1e-3 anchor): BPSK 1.7 dB, QPSK 1.6 dB, 8PSK 3.0 dB — the last also sitting 0.87 dB from the fully-scattered EVM floor, i.e. barely distinguishable from noise. So m_out = 8 is not optional at M = 8. Never pair 2 with MPSK_RX_PULSE_IANDD — the filter degenerates to a two-tap sum, the eye barely opens and acquisition itself fails about half the time. Replaces the old n (NDA arm dumps/symbol), which the cascade's own outputs now serve.
  • pulse Matched-filter shape (default MPSK_RX_PULSE_IANDD).
  • rrc_beta RRC roll-off in [0, 1] (default 0.35; RRC only).
  • rrc_span RRC one-sided span in symbols (default 8; RRC only).
  • bn_carrier Carrier loop noise bandwidth, normalised to the symbol rate (default 0.01). A carrier loop here closes around the matched filter, so its dead time is that filter's group delay — keep it a small fraction of the symbol rate, as a real receiver does.
  • zeta Damping factor for both loops. 0 (the default) derives it as 1/sqrt(2) (MPSK_RX_ZETA_DEFAULT) — a constant rather than a computation, since nothing in this receiver moves the optimal damping and both loops already share one value. Read it back with mpsk_receiver_get_zeta().
  • bn_timing Symbol-timing loop noise bandwidth, normalised to the symbol rate (default 0.01).
  • lock_thresh Declare threshold for the carrier lock indicator, on the carrier lock metric. 0 (the default) derives it as sigma_H0 * eta(Pfa) = 0.1132 * 4.4159 = 0.4999 (MPSK_RX_LOCK_THRESH_DEFAULT), which is the 0.5 that used to be hand-picked — so the derivation changed no behaviour, and is here because a number that was picked and a number that was derived look identical until one has to move. Read it back with mpsk_receiver_get_lock_thresh(). The drop threshold sits at 0.8x for level hysteresis, and both directions are verify-counted (8 symbols up / 32 down). The metric is Re((z/|z|)^M) smoothed by an EMA, whose noise-only sd is 0.1132 for every M, so the threshold is 4.42 noise sigmas — a per-look false-alarm probability of 5e-6. To pin your own, divide your Pfa's z-score into 0.1132 rather than picking by feel; see carrier_nda_core.h for the derivation and the measured verification.
  • init_norm_freq Seed carrier frequency, cycles/sample at the input rate (default 0.0). This is the centre the LO is tuned to; the loop tracks the residual around it.
  • differential bits(): differential (rotation-invariant) demap (default 0 = coherent).
  • num_phases Terminal-stage bank arms; a power of two. 0 (the default) derives it as 64 (MPSK_RX_NUM_PHASES_DEFAULT), the measured saturation point — against the 1024 that used to be the default, a 16x bank for no measurable gain. Read it back with mpsk_receiver_get_num_phases(). Sets the timing resolution to 1/num_phases of an output period.
  • agc Non-zero (default) puts the receiver's ONE AGC in the front-end cascade, immediately before the terminal matched stage. It serves BOTH loops — carrier and timing both run on its output, so it is a dynamic element inside both, which is why mpsk_rx_agc_bn sizes it against the SLOWER of the two rather than against timing alone. What differs is only why the level matters to each: the timing detector normalises by a slope computed at construction for a unit-amplitude stream (symsync_ted_slope), so a level error is a loop-gain error there directly; the carrier detector normalises by its own |z|^M (carrier_nda_disc), so it is immune to the level itself but still sees the AGC's transient. Pass 0 and the receiver is un-levelled: the timing loop is under-driven by A^2, which at an input amplitude of 0.25 is 16x. The reference is derived from the bank's own pulse energy, not chosen.
  • bn_agc_ratio That AGC's bandwidth as a fraction of the SLOWEST loop it feeds, min(bn_carrier, bn_timing) — see mpsk_rx_agc_bn. Must be in (0, 1); construction refuses 1 or above rather than warning, because at 1 the AGC is exactly as fast as a loop it feeds and past that it is faster, and two level-correcting loops at the same speed integrate against each other. 0 (the default) derives it as MPSK_RX_AGC_BW_RATIO = 0.05, 20x slower than the slowest loop it feeds (MPSK_RX_AGC_RATIO_DEFAULT); 0 is the one value below 1 that is a request rather than a rejection. Read it back with mpsk_receiver_get_bn_agc_ratio().

Returns:

Heap-allocated state, or NULL on invalid args / allocation failure.

Note:

Zero means derive, for m_out, zeta, lock_thresh, num_phases and bn_agc_ratio (gh-644). Every one of those validators previously REJECTED zero, so no working call site can be relying on it, which is what makes the derivation additive rather than a break. The derivation runs BEFORE the validation, so a derived answer faces the same guards a supplied one does. Each is reported back by a getter — without that, 0 would be an instruction whose result nobody can see. See docs/design/mpsk.md §8.1.

Note:

Caller must call mpsk_receiver_destroy() when done.


function mpsk_receiver_create_bpsk

A BPSK receiver stated in the units a caller actually holds: Hz.

mpsk_receiver_state_t * mpsk_receiver_create_bpsk (
    double sample_rate_hz,
    double symbol_rate_hz,
    double carrier_freq_hz,
    int pulse,
    double rrc_beta,
    int rrc_span,
    double bn_carrier,
    double bn_timing,
    int differential,
    int agc
) 

Same core, same loops, same methods — this differs from mpsk_receiver_create() only in what it ASKS FOR, and that is the point. A caller with a capture holds a sample rate, a symbol rate and a carrier frequency, all in Hz. They do not hold sps: that is fs / Rs, a ratio this library computes for its own use in selecting a cascade and in costing it. Requiring it makes the caller derive an internal quantity, and then it spreads — because sps is in the constructor, init_norm_freq has to be cycles per SAMPLE, so stating a carrier offset needs sps and fs both, while the loop bandwidth on the next line is normalised to the SYMBOL rate. One constructor, two normalisations, and the conversion between them is the caller's problem.

So the conversion happens here, once: sps = sample_rate_hz / symbol_rate_hz and the LO centre is carrier_freq_hz / sample_rate_hz. Nothing on this signature is normalised to anything.

**m is absent because the type says it.** That is the cheapest parameter to remove and the easiest to miss: a fact carried by the class name is not a parameter on that class.

Every argument this does not take is a derive-request in the delegate below — m_out, zeta, lock_thresh, num_phases, bn_agc_ratio all ask create() for the derived answer, and the NDA tap is the one measured to work at every battery point. They are absent because nobody has a use for them here, not because they are unavailable: MpskReceiver still takes every one.

Parameters:

  • sample_rate_hz ADC sample rate, Hz. Must be > 0.
  • symbol_rate_hz Symbol rate, Hz. Must be > 0, and must leave sample_rate_hz / symbol_rate_hz at or above the derived m_out — a rate that cannot be strobed is refused rather than approximated.
  • carrier_freq_hz Carrier centre, Hz (default 0 — complex baseband). |carrier_freq_hz| must be under sample_rate_hz / 2; a centre outside Nyquist is a mis-stated capture, not a tuning request.
  • pulse Matched-filter shape (default MPSK_RX_PULSE_IANDD).
  • rrc_beta RRC roll-off in [0, 1] (default 0.35; RRC only).
  • rrc_span RRC one-sided span in symbols (default 8; RRC only).
  • bn_carrier Carrier loop noise bandwidth, normalised to the symbol rate (default 0.01).
  • bn_timing Symbol-timing loop noise bandwidth, normalised to the symbol rate (default 0.01).
  • differential bits(): differential (rotation-invariant) demap (default 0, coherent).
  • agc Front-end AGC (default 1).

Returns:

Heap-allocated state, or NULL on invalid args / allocation failure. Destroy with mpsk_receiver_destroy() like any other.

>>> from doppler.track import BpskReceiver
>>> rx = BpskReceiver(sample_rate_hz=8e6, symbol_rate_hz=1e6)
>>> rx.m                 # the type says it
2
>>> rx.sps               # derived from the two rates, not asked for
8.0

function mpsk_receiver_create_real

Create the same receiver behind an R2C halfband: a real IF in.

mpsk_receiver_state_t * mpsk_receiver_create_real (
    int m,
    double sps,
    size_t m_out,
    int pulse,
    double rrc_beta,
    int rrc_span,
    double bn_carrier,
    double zeta,
    double bn_timing,
    double lock_thresh,
    double init_norm_freq,
    int differential,
    size_t num_phases,
    int agc,
    double bn_agc_ratio
) 

The real-input face. Every parameter means what it means on mpsk_receiver_create() — same names, same order, same types, same derivations, the same "zero means derive" rule — because this is the same object and not a twin of it. Only the three rate conventions in this file's header block differ, and each is named against the parameter it touches below.

A real-valued IF is the usual output of a single-ended ADC, so this is the face that takes a digitiser's samples directly. Everything downstream — symbols, bits, telemetry, serialization — is one implementation shared with the complex face.

Parameters:

  • m As mpsk_receiver_create().
  • sps Samples per symbol at the REAL input; any double strictly greater than 2 * m_out. The cascade behind the halfband runs at twice the overall rate, and Ddcr requires that rate below 0.5 — so where the complex face accepts sps >= m_out, this one needs twice the headroom. Derived m_out honours the same bound (mpsk_rx_derive_m_out takes the constraint, not the rate), so a caller cannot pair an sps and an m_out that will not construct.
  • m_out As mpsk_receiver_create(); 0 derives it against the strict sps/2 cap above rather than sps.
  • pulse As mpsk_receiver_create().
  • rrc_beta As mpsk_receiver_create().
  • rrc_span As mpsk_receiver_create().
  • bn_carrier As mpsk_receiver_create(). Still normalised to the SYMBOL rate: the halfband moves the LO's clock, not the loop's units.
  • zeta As mpsk_receiver_create(); 0 derives.
  • bn_timing As mpsk_receiver_create().
  • lock_thresh As mpsk_receiver_create(); 0 derives.
  • init_norm_freq The real IF centre, cycles/sample at the real input rate. An IF at 0.2 * fs is 0.2; the halved value the LO actually uses is this object's business, not the caller's. A real IF must be tuned near — this face does not acquire from a cold zero the way the complex one does, so the centre is where the tap's pull-in range sits around.
  • differential As mpsk_receiver_create().
  • num_phases As mpsk_receiver_create(); 0 derives.
  • agc As mpsk_receiver_create(). The AGC sits inside the cascade BEHIND the halfband, so it levels the analytic signal at the intermediate rate, which is also where the noise has already been filtered.
  • bn_agc_ratio As mpsk_receiver_create(); 0 derives.

Returns:

Heap-allocated state, or NULL on invalid args / allocation failure. Destroy with mpsk_receiver_destroy() like any other.

Deliberately carries NO example block, exactly like mpsk_receiver_create() above the C usage MOVED to this file's header comment, it was not dropped. A constructor's own example block is what jm renders as the class's Python Examples, so a C block here does not add an example, it REPLACES the generated Python one. That is how MpskReceiverR came to be the one public class with no runnable example: it had looked covered only because a view used to inherit its parent's docstring, which just-makeit 0.70.1 correctly stopped doing.

(Written without naming the doxygen command itself: the command is recognised inside backticks too, so spelling it here would open a block that never closes and swallow every declaration below.)


function mpsk_receiver_destroy

Destroy an M-PSK receiver and release all memory.

void mpsk_receiver_destroy (
    mpsk_receiver_state_t * state
) 

Parameters:

  • state May be NULL.

function mpsk_receiver_get_agc_gain_db

Gain the front end's AGC is applying, in dB; 0.0 when agc = 0.

double mpsk_receiver_get_agc_gain_db (
    const mpsk_receiver_state_t * state
) 

The cascade's own level correction, read back rather than inferred. This is the diagnostic for a level problem: a receiver that will not lock with a healthy lock statistic, or one whose timing loop behaves differently at two input levels, is asking about this number. It settles at -10*log10(P_in / P_ref) where P_ref is the power a unit-amplitude symbol stream has where the AGC sits, so a reading far from 0 dB says the input is far from the level the cascade was built for which is fine, and is exactly what the AGC is for, but is worth knowing.

Separate from the cascade's filter response (RateConverter_gain()), which is computed from coefficients and stays 1.0; the two multiply.


function mpsk_receiver_get_bn_agc_ratio

AGC bandwidth ratio in use — derived unless pinned (§8.1).

double mpsk_receiver_get_bn_agc_ratio (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_clipped

Has the cascade's CIC stage clipped its input since the last reset? A CIC bounds its input to +-1.0 and clips silently past that, which costs ~25 dB of EVM behind a perfectly healthy lock.

int mpsk_receiver_get_clipped (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_last_error

Carrier loop phase discriminator (rad) — the residual phase the loop is trying to null; loop stress.

double mpsk_receiver_get_last_error (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_lock

double mpsk_receiver_get_lock (
    const mpsk_receiver_state_t * state
) 

function mpsk_receiver_get_lock_drop_thresh

Carrier DROP threshold in use — MPSK_RX_LOCK_DOWN x the declare threshold, the level hysteresis the pair is stated with.

double mpsk_receiver_get_lock_drop_thresh (
    const mpsk_receiver_state_t * state
) 

Readable for the same reason the declare side is: a caller plotting the lock statistic needs both edges to know what the decision was reading, and deriving 0.8 * in a plotting script puts a second copy of the hysteresis rule outside the object that owns it.


function mpsk_receiver_get_lock_thresh

Carrier lock DECLARE threshold in use — derived unless pinned (§8.1). It gates no loop and no output; see mpsk_rx_loops.h .

double mpsk_receiver_get_lock_thresh (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_lock_time

Symbols from reset to the FIRST carrier-lock declaration, or -1 if the receiver has not locked yet.

int64_t mpsk_receiver_get_lock_time (
    const mpsk_receiver_state_t * state
) 

The acquisition time, as a number a caller can read rather than infer by polling locked in a loop. Dated by the same hysteretic detector mpsk_receiver_get_locked() reports, so the two cannot disagree.

In SYMBOLS, not seconds: bn_carrier and bn_timing are both normalised to the symbol rate, so a settling budget quoted in symbols is comparable across every input rate, and a caller with Rs divides once. Only the first declaration is dated — a drop and re-acquire does not restamp it, because the question this answers is "how long did this receiver take to lock", not "when did it last hold". mpsk_receiver_reset() clears it to -1.


function mpsk_receiver_get_locked

Binary carrier-lock flag from the loop's hysteretic (up/down verify-counted) lock detector — de-chattered, unlike the raw metric.

int mpsk_receiver_get_locked (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_m

int mpsk_receiver_get_m (
    const mpsk_receiver_state_t * state
) 

function mpsk_receiver_get_m_out

Terminal outputs per symbol (the old n , now the cascade's).

size_t mpsk_receiver_get_m_out (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_nco_freq

Instantaneous NCO frequency command (carrier loop filter output, cycles/sample): mean tracks a ramp with no lag, variance is loop stress.

double mpsk_receiver_get_nco_freq (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_norm_freq

Carrier frequency the receiver is tracking, cycles/sample at the input rate: the create-time centre plus the loop's own estimate.

double mpsk_receiver_get_norm_freq (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_num_phases

Matched-filter bank arms in use — derived unless pinned (§8.1).

size_t mpsk_receiver_get_num_phases (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_sps

double mpsk_receiver_get_sps (
    const mpsk_receiver_state_t * state
) 

function mpsk_receiver_get_state

void mpsk_receiver_get_state (
    const mpsk_receiver_state_t * state,
    void * blob
) 

function mpsk_receiver_get_sync_lock_drop_thresh

Timing DROP threshold on sync.lock . Equal to the declare threshold when the timing loop carries no level hysteresis.

double mpsk_receiver_get_sync_lock_drop_thresh (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_sync_lock_thresh

Timing DECLARE threshold on sync.lock , derived by symsync's own (rolloff, esno_min, pfa, pd) geometry rather than pinned.

double mpsk_receiver_get_sync_lock_thresh (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_timing_rate

Smoothed tracked samples per symbol — departs from the nominal sps by exactly the sample-clock offset the timing loop is tracking.

double mpsk_receiver_get_timing_rate (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_get_zeta

Loop damping in use — derived 1/sqrt(2) unless pinned (§8.1).

double mpsk_receiver_get_zeta (
    const mpsk_receiver_state_t * state
) 


function mpsk_receiver_reset

Re-seed the front end and both loops to their create-time state.

void mpsk_receiver_reset (
    mpsk_receiver_state_t * state
) 

Clears the cascade's filter memory, the carrier and timing NCOs, the loop-filter integrators and the lock detectors, and returns the carrier estimate to init_norm_freq. The configuration (order, rate, pulse, bandwidths) is untouched, so the same input fed twice around a reset reproduces the same output bit-for-bit.

Parameters:

  • state Must be non-NULL.
    >>> import numpy as np
    >>> from doppler.track import MpskReceiver
    >>> rng = np.random.default_rng(0)
    >>> idx = rng.integers(0, 4, 300)
    >>> tx = np.repeat(np.exp(1j * (2 * np.pi * idx / 4 + np.pi / 4)), 8)
    >>> tx = tx.astype(np.complex64)
    >>> rx = MpskReceiver(m=4, sps=8, m_out=4)
    >>> first = rx.steps(tx)
    >>> rx.reset()                                # back to the cold state
    >>> np.array_equal(first, rx.steps(tx))       # same input, same output
    True
    

function mpsk_receiver_set_norm_freq

Retune to val cycles/sample: moves the LO centre there and zeroes the loop's residual estimate, so norm_freq reads back exactly.

void mpsk_receiver_set_norm_freq (
    mpsk_receiver_state_t * state,
    double val
) 


function mpsk_receiver_set_state

int mpsk_receiver_set_state (
    mpsk_receiver_state_t * state,
    const void * blob
) 

function mpsk_receiver_set_telemetry

Attach (or detach) a telemetry context across the receiver. Registers the receiver's own "<prefix>.lock" probe (the carrier lock EMA), then the carrier loop's "<prefix>.car.e" / ".freq" / ".locked" and the symbol-timing loop's "<prefix>.sync.e" / ".ctrl" / ".rate" / ".lock" / ".locked" / ".mu" ten probes emitted once per recovered symbol then the front end's AGC under "<prefix>.agc" ("<prefix>.agc.gain_db" and "<prefix>.agc.level_db"; seeagc_set_telemetry() ). Twelve probes total, all thinned bydecim . Passing NULL detaches everything.

int mpsk_receiver_set_telemetry (
    mpsk_receiver_state_t * state,
    dp_tlm_t * tlm,
    const char * prefix,
    uint32_t decim
) 

Warning:

The two AGC probes are NOT at the symbol rate the other ten are. That AGC sits pre-terminal in the cascade (RateConverter's tap, ahead of the stage the timing loop steers) and emits once per gain-update event, i.e. every AGC_DECIM_DEFAULT samples of that fixed-rate stream so it reports on a grid that depends on the planned cascade, not on recovered symbols, and a run yields a different number of AGC records than carrier records. Compare the two by TIME, never by record index. This is deliberate: the AGC's bandwidth is quoted in the pre-terminal stream's units precisely so it is not coupled to the loop that is stretching the symbol grid (see RateConverter_enable_agc()).

Instrumenting it matters because it is FIRST in the chain, and a level error is the one kind no downstream loop can correct for itself: a TED normalises by its own construct-time slope, so it reads a level error as a loop-gain error (A^2 Gardner, A DTTL) with no other reference to catch it. This receiver also makes the AGC the slowest of its three loops by construction mpsk_rx_agc_bn() derives its bandwidth as a fraction of the slowest loop it feeds, and bn_agc_ratio is validated to (0, 1) but that is a choice of THIS composition, and slowest does not by itself mean longest: settling is set by the bandwidth AND by how far the level starts from the reference, which is unknown at construction. Which is exactly why it has to be measured rather than inferred; the zero-referenced "<prefix>.agc.level_db" is what makes that possible.

With agc = 0 at construction there is no AGC to attach and the two probes are simply absent (fourteen, not sixteen); this still returns DP_OK.

Setup path, never hot; the context is borrowed and must outlive the attachment (SPSC rules in dp_tlm/dp_tlm_core.h).

Parameters:

  • state Must be non-NULL.
  • tlm Telemetry context to attach, or NULL to detach.
  • prefix Probe-name prefix, e.g. "rx".
  • decim Emit every decim-th symbol (every decim-th gain update for the two AGC probes); >= 1.

Returns:

DP_OK, or DP_ERR_INVALID when the probe table cannot take the probes (the attach fails whole; everything detached).

>>> import numpy as np
>>> from doppler.track import MpskReceiver
>>> from doppler.telemetry import Telemetry
>>> tlm = Telemetry(1 << 14)   # 15 probes x ~512 syms + headroom
>>> rx = MpskReceiver(m=4, sps=4, m_out=2)
>>> rx.set_telemetry(tlm, "rx")
>>> len(tlm.probe_names)
15
>>> rng = np.random.default_rng(7)
>>> syms = (1 - 2 * rng.integers(0, 2, 512)).astype(np.complex64)
>>> x = np.repeat(syms, 4)
>>> _ = rx.steps(x)
>>> recs = tlm.read()
>>> tlm.dropped        # size the ring, or the counts below diverge
0
>>> n_sync = len(recs[recs["probe"] == tlm.probe_id("rx.sync.e")])
>>> n_car = len(recs[recs["probe"] == tlm.probe_id("rx.car.e")])
>>> n_sync > 0 and n_sync == n_car
True
>>> n_agc = len(recs[recs["probe"] == tlm.probe_id("rx.agc.gain_db")])
>>> n_agc > 0 and n_agc != n_sync   # cascade grid, not symbol grid
True


function mpsk_receiver_state_bytes

size_t mpsk_receiver_state_bytes (
    const mpsk_receiver_state_t * state
) 

function mpsk_receiver_step_real_ted

Push one REAL input sample; emit a symbol if it completed one.

JM_FORCEINLINE  JM_HOT int mpsk_receiver_step_real_ted (
    mpsk_receiver_state_t * s,
    float x,
    float _Complex * y_out,
    int ted
) 

The real face's composition API — mpsk_receiver_step_ted() behind an R2C halfband. Only the front end and the input type differ; everything after the front end is mpsk_rx_fold, shared verbatim, which is what makes "the loops behave identically regardless of front end" a claim about one body of code rather than about two.

Parameters:

  • s State, built by mpsk_receiver_create_real(). Non-NULL.
  • x One real input sample.
  • y_out Receives the symbol when the return is 1.
  • ted RATESYNC_TED_GARDNER or RATESYNC_TED_DTTL — pass a literal for a specialised (branch-free) instantiation.

Returns:

1 if a symbol was emitted (into y_out), 0 otherwise.


function mpsk_receiver_step_ted

Push one input sample; emit a symbol if it completed one.

JM_FORCEINLINE  JM_HOT int mpsk_receiver_step_ted (
    mpsk_receiver_state_t * s,
    float _Complex x,
    float _Complex * y_out,
    int ted
) 

The composition API: mixes, decimates and matched-filters x through the front end at the loops' current control values, then folds every output it produced into both loops. The cascade rate is m_out/sps <= 1, so one input can complete at most two output periods and therefore at most one on-time strobe.

Parameters:

  • s State. Must be non-NULL.
  • x One input sample.
  • y_out Receives the symbol when the return is 1.
  • ted RATESYNC_TED_GARDNER or RATESYNC_TED_DTTL — pass a literal for a specialised (branch-free) instantiation.

Returns:

1 if a symbol was emitted (into y_out), 0 otherwise.


function mpsk_receiver_steps

Demodulate a cf32 block and emit the recovered symbols.

size_t mpsk_receiver_steps (
    mpsk_receiver_state_t * state,
    const float _Complex * x,
    size_t x_len,
    float _Complex * out,
    size_t max_out
) 

Runs the per-sample loop (mix + cascade + matched filter, then the carrier and timing loops) over x and writes one cf32 symbol per recovered symbol period — roughly x_len / sps outputs. Read norm_freq for the tracked carrier and lock for the carrier lock metric.

Parameters:

  • state Receiver state. Must be non-NULL.
  • x Input cf32 samples.
  • x_len Number of input samples.
  • out Output symbols; caller provides max_out capacity.
  • max_out Output capacity.

Returns:

Number of symbols written.

>>> import numpy as np
>>> from doppler.track import MpskReceiver
>>> rng = np.random.default_rng(0)
>>> idx = rng.integers(0, 4, 3000)                  # QPSK symbols
>>> tx = np.repeat(np.exp(1j * (2 * np.pi * idx / 4 + np.pi / 4)), 8)
>>> tx = tx.astype(np.complex64)                    # 8 samples/symbol
>>> rx = MpskReceiver(m=4, sps=8, m_out=4, bn_carrier=0.02)
>>> sym = rx.steps(tx)                              # blind NDA acquire
>>> sym.size                                        # ~ x_len / sps
2998
>>> rx.lock > 0.8                                   # carrier locked
True


function mpsk_receiver_steps_max_out

size_t mpsk_receiver_steps_max_out (
    mpsk_receiver_state_t * state
) 

function mpsk_receiver_steps_real

Demodulate a real f32 block and emit the recovered symbols.

size_t mpsk_receiver_steps_real (
    mpsk_receiver_state_t * state,
    const float * x,
    size_t x_len,
    float _Complex * out,
    size_t max_out
) 

mpsk_receiver_steps() taking real samples: the R2C halfband makes them complex before anything else touches them, and the per-sample body is the same one. Requires a state built by mpsk_receiver_create_real().

Parameters:

  • state Must be non-NULL.
  • x Real f32 input samples.
  • x_len Number of input samples.
  • out Output symbols; caller provides max_out capacity.
  • max_out Output capacity.

Returns:

Number of symbols written.

>>> import numpy as np
>>> from doppler.track import MpskReceiverR
>>> rng = np.random.default_rng(3)
>>> idx = rng.integers(0, 4, 2400)                  # QPSK symbols
>>> bb = np.repeat(np.exp(2j * np.pi * idx / 4), 32)  # 32 sps
>>> n = np.arange(bb.size)
>>> x = (0.4 * bb * np.exp(2j * np.pi * 0.25 * n)).real  # IF at fs/4
>>> x = np.ascontiguousarray(x.astype(np.float32))
>>> rx = MpskReceiverR(m=4, sps=32, m_out=8, init_norm_freq=0.25)
>>> sym = rx.steps(x)
>>> sym.size                                        # ~ x_len / sps
2398
>>> rx.lock > 0.8                                   # carrier locked
True


function mpsk_receiver_steps_real_max_out

size_t mpsk_receiver_steps_real_max_out (
    mpsk_receiver_state_t * state
) 

Macro Definition Documentation

define MPSK_RECEIVER_R_STATE_MAGIC

#define MPSK_RECEIVER_R_STATE_MAGIC `DP_FOURCC ('M', 'P', 'S', 'R')`

define MPSK_RECEIVER_R_STATE_VERSION

#define MPSK_RECEIVER_R_STATE_VERSION `2u`

define MPSK_RECEIVER_STATE_MAGIC

#define MPSK_RECEIVER_STATE_MAGIC `DP_FOURCC ('M', 'P', 'S', 'K')`

define MPSK_RECEIVER_STATE_VERSION

#define MPSK_RECEIVER_STATE_VERSION `6u /* v5: rebuilt on the matched DDC */`


The documentation for this class was generated from the following file native/inc/mpsk_receiver/mpsk_receiver_core.h