Skip to content

File carrier_nda_core.h

FileList > carrier_nda > carrier_nda_core.h

Go to the source code of this file

Non-data-aided (NDA) M-th-power carrier-tracking loop. More...

  • #include "boxcar/boxcar_core.h"
  • #include "clib_common.h"
  • #include "dp_state.h"
  • #include "jm_perf.h"
  • #include "lo/lo_core.h"
  • #include "lockdet/lockdet_core.h"
  • #include "loop_filter/loop_filter_core.h"
  • #include "dp_tlm/dp_tlm_core.h"
  • #include <math.h>
  • #include "telemetry/telemetry_core.h"

Classes

Type Name
struct carrier_nda_state_t
NDA M-th-power carrier loop state.
struct carrier_nda_tlm_t
Telemetry attachment: a borrowed context + this object's probe ids. NULL ctx (the default) means detached — the probe site is then a single predicted-not-taken branch per block loop. Zeroed in state blobs and preserved across set_state (DP_DEFINE_POD_STATE_TLM).

Public Functions

Type Name
JM_FORCEINLINE JM_HOT int carrier_nda_arm_step (carrier_nda_state_t * s, float _Complex d, double * pe, double * lock)
Slide the moving-average arm by one sample; discriminate the output.
void carrier_nda_configure_lock (carrier_nda_state_t * state, double up_thresh, double down_thresh, uint32_t n_up, uint32_t n_down)
Re-tune the carrier lock detector's geometry directly.
carrier_nda_state_t * carrier_nda_create (double bn, double zeta, double init_norm_freq, size_t sps, int n, int m)
Create an NDA carrier loop instance.
void carrier_nda_destroy (carrier_nda_state_t * state)
Destroy an NDA carrier loop instance and release all memory.
JM_FORCEINLINE void carrier_nda_disc (float _Complex z, int m, double * pe, double * lock)
The M-th-power discriminator on an arm sample, normalized by its own amplitude law.
double carrier_nda_get_bn (const carrier_nda_state_t * state)
double carrier_nda_get_last_error (const carrier_nda_state_t * state)
double carrier_nda_get_lock (const carrier_nda_state_t * state)
int carrier_nda_get_locked (const carrier_nda_state_t * state)
Current lock decision (1 = locked, 0 = not), with the configured verify-count / hysteresis rule applied (see carrier_nda_configure_lock).
int carrier_nda_get_m (const carrier_nda_state_t * state)
int carrier_nda_get_n (const carrier_nda_state_t * state)
double carrier_nda_get_nco_freq (const carrier_nda_state_t * state)
Instantaneous NCO frequency command = centre + full loop-filter output (integ + kp*e), cycles/sample. Mean rides a ramp with no lag; variance is the loop stress. See the impl for the estimator-vs-command distinction.
double carrier_nda_get_norm_freq (const carrier_nda_state_t * state)
size_t carrier_nda_get_sps (const carrier_nda_state_t * state)
void carrier_nda_get_state (const carrier_nda_state_t * state, void * blob)
Serialize the full loop state into blob .
void carrier_nda_init (carrier_nda_state_t * s, double bn, double zeta, double init_norm_freq, size_t sps, int n, int m)
Initialise an NDA carrier loop in place (no allocation).
void carrier_nda_reset (carrier_nda_state_t * state)
Re-seed the loop to its create-time frequency/phase; keep config.
void carrier_nda_set_bn (carrier_nda_state_t * state, double val)
void carrier_nda_set_norm_freq (carrier_nda_state_t * state, double val)
int carrier_nda_set_state (carrier_nda_state_t * state, const void * blob)
Restore state; DP_OK, or DP_ERR_INVALID if the envelope rejects.
int carrier_nda_set_telemetry (carrier_nda_state_t * state, dp_tlm_t * tlm, const char * prefix, uint32_t decim)
Attach (or detach) a telemetry context and register the carrier loop's probes on it. Registers four probes, emitted once per input sample (this is a sample-rate loop — use decim to thin the stream): "<prefix>.lock" (the lock-signal EMA, ~1 when phase-locked), "<prefix>.e" (the M-th-power phase discriminator — the loop stress), "<prefix>.freq" (the tracked carrier frequency, cycles/sample) and "<prefix>.locked" (the verify-counted lockdet decision, 0/1). Passing NULL detaches. Setup path, never hot: call before the producer thread starts stepping; the context is borrowed and must outlive the attachment (SPSC rules indp_tlm/dp_tlm_core.h ).
size_t carrier_nda_state_bytes (const carrier_nda_state_t * state)
Serialized-state byte size.
JM_FORCEINLINE JM_HOT void carrier_nda_steer (carrier_nda_state_t * s, double pe)
Steer the shared NCO with a phase error through the loop filter.
size_t carrier_nda_steps (carrier_nda_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out)
De-rotate a cf32 block with the recovered carrier and return the de-rotated stream (one output per input sample).
size_t carrier_nda_steps_max_out (carrier_nda_state_t * state)
void carrier_nda_tlm_flush (const carrier_nda_state_t * s)
Emit the carrier loop's telemetry records for the current sample.
JM_FORCEINLINE JM_HOT float _Complex carrier_nda_wipeoff (carrier_nda_state_t * s, float _Complex x)
Per-sample carrier wipe-off: de-rotate x by the NCO, advance it.

Macros

Type Name
define CARRIER_NDA_EPS 1e-12
define CARRIER_NDA_INV_2PI 0.15915494309189535 /\* 1 / (2\*pi) \*/
define CARRIER_NDA_LOCK_ALPHA 0.05
define CARRIER_NDA_LOCK_NORM_SD 0.11322770341445956
define CARRIER_NDA_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('C', 'N', 'D', 'A')
define CARRIER_NDA_STATE_VERSION /* multi line expression */

Detailed Description

A carrier-recovery loop that locks without data and without symbol timing — the cold-start / acquisition counterpart to the decision-directed carrier_mpsk_state_t loop. Per sample it de-rotates the input with the integer-phase lo_state_t NCO (carrier wipe-off); it filters the de-rotated samples through a free-running I/Q boxcar moving average of sps/n samples (one output per input sample — no rate change), and on every sample runs the M-th-power phase discriminator, filters the error through an embedded loop_filter_state_t, and steers the NCO frequency + phase.

Raising the arm sample z to the Mth power strips the M-PSK data modulation, leaving M times the carrier phase — so the discriminator is independent of the data symbols and of symbol timing. That is what lets it acquire a bare/unmodulated carrier, or a modulated carrier before timing lock. It is the M-fold-ambiguous acquisition aid; a decision-directed loop gives the low-jitter steady state (resolve the M-fold ambiguity downstream).

The M-th power is computed by repeated complex squaring (z⁴z⁸) of the unit-magnitude sample z/|z|. Each level yields a phase error and a lock signal: * phase_error = Im((z/|z|)^M) scaled by 1, ½, ¼ for M = 2, 4, 8 — the scale normalizes the phase-detector gain so the S-curve slope at lock is 2 for every M (one bn behaves identically across M). * lock_signal = Re((z/|z|)^M) — the M-th power of a limited sample, so it is bounded in ±1 and its H0 variance is 1/2 for every M. ~1 when phase-locked, zero-mean with no carrier. That M-independence is what makes one lock_thresh mean one Pfa at every order; the threshold chain is derived above CARRIER_NDA_LOCK_ALPHA. Its EMA (lock) is the carrier lock metric. See docs/design/mpsk.md §4.2, "Limiting — what makes the threshold a Pfa", for the derivation.

The block API (carrier_nda_steps) is the Python face and emits the de-rotated sample stream; the JM_FORCEINLINE carrier_nda_wipeoff()/_arm_step()/_steer() are the C composition API a receiver inlines into its own sample loop (it can also steer the shared NCO with its own decision-directed error on handover).

Note:

The input SCALE does not matter, and there is no AGC here. The discriminator divides out its own amplitude law (|z|^M) exactly, so both outputs — and with them the loop gain — are invariant to input scale over the whole float range: measured identical to 6.5e-7 relative from an amplitude of 1e-5 to 1e15, at every M a few float eps, which is the rounding floor a float detector has and not a level dependence. The measurement is test_carrier_nda_core.c section 9; this file carried a tighter 5e-7 that the test itself had already corrected, so the number here now comes from the thing that runs it. This loop used to embed a slow arm AGC whose only job was to manufacture |z| = 1 so a raw Im(z^M) would behave; that condition no longer has to be manufactured, and the AGC is gone. A receiver needs exactly one AGC, for its own signal path, and not one per detector (docs/design/mpsk.md §3.2, "The NDA discriminator + lock signal").

SCALE is not Es/N0, and only the first of them is invariant here. Section 9 scales a clean phasor, so it holds signal and noise in the same ratio; that is homogeneity of degree zero, plus — the reason the test earns its place — a float-RANGE gate, since forming |z|^M at the end instead returns 0 below |z| = 0.032 and NaN above 1e4 at M = 8. It says nothing about level relative to NOISE, and it should not be read as though it did: per-sample division by the instantaneous |s+n| is a hard limiter, so the S-curve slope genuinely does depend on Es/N0. That dependence is measured, as loop SNR against the un-normalised form across 0–20 dB and every M, in docs/design/mpsk.md §3.2 — the penalty is real at 0–3 dB, where the link cannot be closed anyway, and from ~6 dB up normalising is equal or better. This distinction was read the other way once (gh-795), which is why it is spelled out rather than implied.

** **

A caller must start the residual carrier inside a window, and there are two separate limits on that window. Which one binds decides whether a violation is obvious or silent, so both belong here rather than only in the design of whatever composes this.

Loop capture, roughly k * bn / M. Where pull-in is prompt and predictable. This is the tighter bound at every shipped setting and it is the one a caller normally meets. Violating it is LOUD: the tracked frequency walks and locked never asserts, which is unmistakable in telemetry.

The aliasing ceiling, 1/(2M) cycles per sample. The discriminator runs once per input sample, so an M-th power folds when the residual advances more than half a cycle per update i.e. at a residual of 1/(2M) cyc/sample, with stable false locks spaced 1/M apart. The M-fold ambiguity is therefore a FREQUENCY ambiguity as well as a phase one. Violating this is SILENT and it is this object's worst failure mode: the loop sits still at the wrong frequency and reports a healthy lock statistic measured on QPSK at a residual of one quarter the update rate, tracked frequency 2e-6 against a true 0.03125 and a lock statistic of +0.83 against the ~1.0 a real lock reads. Nothing self-referenced detects it: the constellation is stationary, so EVM and blind M2M4 both look clean. It takes an external frequency reference, a sync word, or a coarse estimate seeded through init_norm_freq.

Both scale as 1/M, so an 8PSK caller needs four times tighter tuning than BPSK at the same bandwidth. See docs/design/mpsk.md §3.4 and §3.5.

** **

The ceiling is linear in the rate the discriminator updates at, and nothing else in the loop depends on that rate so it is free margin for whoever picks it. Stepping this object per input sample, which is what carrier_nda_steps does, is the widest setting available and puts the ceiling tens of times beyond loop capture. A composer that instead taps a DECIMATED stream inherits a proportionally tighter ceiling: at two samples per symbol it falls to one symbol rate over M, which is the regime the QPSK false lock above was measured in. The rule for a composition is to choose the tap so the ceiling stays clear of loop capture at every M it supports, which makes the silent failure structurally unreachable and leaves only the loud one.

** **

n sets the arm window to sps / n samples, and the window is not free: the arm averages across data transitions, so its coherent gain after the M-th power falls as the window widens. For the half-symbol arm (n = 2) the gain is 1/2 + 1/(M+1) 5/6 at BPSK, 7/10 at QPSK, 11/18 at 8PSK against unity for a constant-modulus input. A caller choosing n is choosing a sensitivity loss, and the validation report is the evidence for what each choice costs.

// QPSK NDA carrier loop, 8 samples/symbol, 2-sample moving-average arm
carrier_nda_state_t *c = carrier_nda_create(0.01, 0.707, 0.0, 8, 4, 4);
float _Complex derot[1024];
size_t k = carrier_nda_steps(c, rx, rx_len, derot, 1024);
double f = carrier_nda_get_norm_freq(c); // tracked carrier (cyc/sample)
carrier_nda_destroy(c);

Public Functions Documentation

function carrier_nda_arm_step

Slide the moving-average arm by one sample; discriminate the output.

JM_FORCEINLINE  JM_HOT int carrier_nda_arm_step (
    carrier_nda_state_t * s,
    float _Complex d,
    double * pe,
    double * lock
) 

The arm is a free-running boxcar moving average of the last arm_len de-rotated samples — one output per input sample, no rate change (not a decimating integrate-and-dump). It updates the running window sum in O(1) (add d, subtract the sample leaving the window), runs the M-th-power discriminator on the window average, writes pe and lock, and returns 1 every call.

Parameters:

  • s Carrier loop state. Must be non-NULL.
  • d One de-rotated sample (from carrier_nda_wipeoff).
  • pe Receives the phase error.
  • lock Receives the lock signal.

Returns:

Always 1 (one discriminator output per input sample).


function carrier_nda_configure_lock

Re-tune the carrier lock detector's geometry directly.

void carrier_nda_configure_lock (
    carrier_nda_state_t * state,
    double up_thresh,
    double down_thresh,
    uint32_t n_up,
    uint32_t n_down
) 

Full lockdet control, mirroring costas_configure_lock(): a split declare/drop threshold pair on the lock-signal EMA (level hysteresis) and both verify counts (time hysteresis). Defaults (0.5/0.4, 64 up / 32 down) start from MpskReceiver's own pre-existing acquisition<-> tracking handover thresholds, but size n_up independently: lock is a fast per-sample EMA, so consecutive looks are highly autocorrelated and MpskReceiver's own n_up=8 does not compound the false-declare rate the way it would for independent looks (direct Monte Carlo against a noise-only, no-carrier input found real false locks at n_up=8; n_up=64 was the smallest verify count that reliably eliminated them see carrier_nda_core.c's CARRIER_NDA_LOCK_DEFAULT_* comment for the exact trial data). A live lock survives the re-tune; the in-flight verify run restarts.

Parameters:

  • state Must be non-NULL.
  • up_thresh Declare threshold on the lock-signal EMA.
  • down_thresh Drop threshold; choose <= up_thresh for level hysteresis.
  • n_up Consecutive above-threshold samples to declare; clamped >= 1.
  • n_down Consecutive below-threshold samples to drop; clamped >= 1.
    >>> from doppler.track import CarrierNda
    >>> c = CarrierNda(bn=0.01, sps=8, n=4, m=4)
    >>> c.locked
    False
    >>> c.configure_lock(0.6, 0.5, 16, 64)   # tighter declare, slower drop
    

function carrier_nda_create

Create an NDA carrier loop instance.

carrier_nda_state_t * carrier_nda_create (
    double bn,
    double zeta,
    double init_norm_freq,
    size_t sps,
    int n,
    int m
) 

Parameters:

  • bn Loop noise bandwidth (default 0.01).
  • zeta Damping factor (default 0.707).
  • init_norm_freq Seed carrier frequency, cycles/sample (default 0.0).
  • sps Samples per symbol (default 8).
  • n MA window divisor: window = sps/n (default 4; spsn==0).
  • m Constellation order M, 2/4/8 (default 4 = QPSK).

Returns:

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

Note:

Caller must call carrier_nda_destroy() when done.


function carrier_nda_destroy

Destroy an NDA carrier loop instance and release all memory.

void carrier_nda_destroy (
    carrier_nda_state_t * state
) 

Parameters:

  • state May be NULL.

function carrier_nda_disc

The M-th-power discriminator on an arm sample, normalized by its own amplitude law.

JM_FORCEINLINE void carrier_nda_disc (
    float _Complex z,
    int m,
    double * pe,
    double * lock
) 

Runs the repeated-squaring recursion z⁴z⁸ on the unit- magnitude sample z/|z| and writes the phase error (= scaled Im((z/|z|)^M)) and the lock signal (Re((z/|z|)^M)). Both outputs are therefore invariant to the input's scale, which is what lets this loop run with no AGC in front of it — see the amplitude note at the top of this file, and docs/design/mpsk.md §3.2, "The NDA discriminator + lock signal", for the squaring-loss measurement that says normalizing is equal or better from ~6 dB Es/N0 up: loop SNR against the raw form, 4e5 samples per point, tabulated over 0–20 dB and M = 2/4/8.

Parameters:

  • z Arm moving-average sample (any scale; only its phase is used).
  • m Constellation order (2, 4, 8).
  • pe Receives the phase error.
  • lock Receives the lock signal.

function carrier_nda_get_bn

double carrier_nda_get_bn (
    const carrier_nda_state_t * state
) 

function carrier_nda_get_last_error

double carrier_nda_get_last_error (
    const carrier_nda_state_t * state
) 

function carrier_nda_get_lock

double carrier_nda_get_lock (
    const carrier_nda_state_t * state
) 

function carrier_nda_get_locked

Current lock decision (1 = locked, 0 = not), with the configured verify-count / hysteresis rule applied (see carrier_nda_configure_lock).

int carrier_nda_get_locked (
    const carrier_nda_state_t * state
) 


function carrier_nda_get_m

int carrier_nda_get_m (
    const carrier_nda_state_t * state
) 

function carrier_nda_get_n

int carrier_nda_get_n (
    const carrier_nda_state_t * state
) 

function carrier_nda_get_nco_freq

Instantaneous NCO frequency command = centre + full loop-filter output (integ + kp*e), cycles/sample. Mean rides a ramp with no lag; variance is the loop stress. See the impl for the estimator-vs-command distinction.

double carrier_nda_get_nco_freq (
    const carrier_nda_state_t * state
) 


function carrier_nda_get_norm_freq

double carrier_nda_get_norm_freq (
    const carrier_nda_state_t * state
) 

function carrier_nda_get_sps

size_t carrier_nda_get_sps (
    const carrier_nda_state_t * state
) 

function carrier_nda_get_state

Serialize the full loop state into blob .

void carrier_nda_get_state (
    const carrier_nda_state_t * state,
    void * blob
) 


function carrier_nda_init

Initialise an NDA carrier loop in place (no allocation).

void carrier_nda_init (
    carrier_nda_state_t * s,
    double bn,
    double zeta,
    double init_norm_freq,
    size_t sps,
    int n,
    int m
) 

Parameters:

  • s State to initialise. Must be non-NULL.
  • bn Loop noise bandwidth, cycles/sample (per-sample loop).
  • zeta Damping factor (0.707 = critically damped).
  • init_norm_freq Seed carrier frequency, cycles/sample.
  • sps Samples per symbol.
  • n MA window divisor: window = sps/n samples (sps % n == 0, sps/n <= BOXCAR_MAX_LEN).
  • m Constellation order M (2, 4, 8).

function carrier_nda_reset

Re-seed the loop to its create-time frequency/phase; keep config.

void carrier_nda_reset (
    carrier_nda_state_t * state
) 

Restores the object to its post-create state: the carrier NCO is reset to the seed frequency it was constructed with (init_norm_freq) with zero phase, the moving-average arm, the loop-filter integrator and the lock EMA are cleared, and the lock detector is dropped. The configured (bn, zeta), the arm geometry (sps, n) and the constellation order m are preserved, so the same object can re-acquire a fresh capture.

Parameters:

  • state Must be non-NULL.
    >>> import numpy as np
    >>> from doppler.track import CarrierNda
    >>> c = CarrierNda(bn=0.01, zeta=0.707, init_norm_freq=0.0,
    ...                sps=8, n=4, m=4)
    >>> rng = np.random.default_rng(0)
    >>> k = np.arange(40000)
    >>> x = (np.exp(2j * np.pi * 0.001 * k) + 0.05 * (
    ...      rng.standard_normal(k.size)
    ...      + 1j * rng.standard_normal(k.size))).astype(np.complex64)
    >>> _ = c.steps(x)
    >>> round(c.norm_freq, 4), round(c.lock, 2)   # acquired the carrier
    (0.001, 0.99)
    >>> c.reset()
    >>> round(c.norm_freq, 4), round(c.lock, 2)   # back to seed, unlocked
    (0.0, 0.0)
    

function carrier_nda_set_bn

void carrier_nda_set_bn (
    carrier_nda_state_t * state,
    double val
) 

function carrier_nda_set_norm_freq

void carrier_nda_set_norm_freq (
    carrier_nda_state_t * state,
    double val
) 

function carrier_nda_set_state

Restore state; DP_OK, or DP_ERR_INVALID if the envelope rejects.

int carrier_nda_set_state (
    carrier_nda_state_t * state,
    const void * blob
) 


function carrier_nda_set_telemetry

Attach (or detach) a telemetry context and register the carrier loop's probes on it. Registers four probes, emitted once per input sample (this is a sample-rate loop — use decim to thin the stream): "<prefix>.lock" (the lock-signal EMA, ~1 when phase-locked), "<prefix>.e" (the M-th-power phase discriminator — the loop stress), "<prefix>.freq" (the tracked carrier frequency, cycles/sample) and "<prefix>.locked" (the verify-counted lockdet decision, 0/1). Passing NULL detaches. Setup path, never hot: call before the producer thread starts stepping; the context is borrowed and must outlive the attachment (SPSC rules indp_tlm/dp_tlm_core.h ).

int carrier_nda_set_telemetry (
    carrier_nda_state_t * state,
    dp_tlm_t * tlm,
    const char * prefix,
    uint32_t decim
) 

Parameters:

  • state Must be non-NULL.
  • tlm Telemetry context to attach, or NULL to detach.
  • prefix Probe-name prefix, e.g. "car" or "rx.car".
  • decim Emit every decim-th sample; >= 1.

Returns:

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

>>> import numpy as np
>>> from doppler.track import CarrierNda
>>> from doppler.telemetry import Telemetry
>>> tlm = Telemetry(1 << 14)
>>> c = CarrierNda(bn=0.01, sps=8, n=4, m=4)
>>> c.set_telemetry(tlm, "car", decim=8)
>>> sorted(tlm.probe_names)
['car.e', 'car.freq', 'car.lock', 'car.locked']
>>> x = np.exp(2j * np.pi * 0.005 * np.arange(4096)).astype(
...     np.complex64)
>>> _ = c.steps(x)
>>> recs = tlm.read()
>>> len(recs[recs["probe"] == tlm.probe_id("car.e")]) == 4096 // 8
True


function carrier_nda_state_bytes

Serialized-state byte size.

size_t carrier_nda_state_bytes (
    const carrier_nda_state_t * state
) 


function carrier_nda_steer

Steer the shared NCO with a phase error through the loop filter.

JM_FORCEINLINE  JM_HOT void carrier_nda_steer (
    carrier_nda_state_t * s,
    double pe
) 

Filters pe and updates the NCO frequency (per sample) + a proportional phase nudge. Shared by the NDA acquisition path and a composing receiver's decision-directed tracking path (handover writes the same NCO).

Parameters:

  • s Carrier loop state. Must be non-NULL.
  • pe Phase error (NDA discriminator, or a decision-directed error).

function carrier_nda_steps

De-rotate a cf32 block with the recovered carrier and return the de-rotated stream (one output per input sample).

size_t carrier_nda_steps (
    carrier_nda_state_t * state,
    const float _Complex * x,
    size_t x_len,
    float _Complex * out,
    size_t max_out
) 

Runs the non-data-aided carrier loop over the block: each sample is wiped off by the integer-phase NCO, the de-rotated sample slides the I/Q moving-average arm, and the M-th-power discriminator (which strips the M-PSK data modulation) steers the NCO frequency and phase. Because the discriminator is data- and timing-independent, this acquires the carrier with no symbol timing and no data present — a bare carrier, or a modulated carrier before timing lock. It resolves to one of m carrier phases (M-fold ambiguity, resolved downstream). Read norm_freq for the tracked carrier (cycles/sample) and lock for the carrier lock metric.

Parameters:

  • state Must be non-NULL.
  • x Input samples (average power at or below unity).
  • x_len Number of input samples.
  • out De-rotated samples, one per input.
  • max_out Capacity of out.

Returns:

Number of de-rotated samples written to out (equals x_len).

>>> import numpy as np
>>> from doppler.track import CarrierNda
>>> c = CarrierNda(bn=0.01, zeta=0.707, init_norm_freq=0.0,
...                sps=8, n=4, m=4)
>>> rng = np.random.default_rng(0)
>>> k = np.arange(40000)
>>> x = (np.exp(2j * np.pi * 0.001 * k) + 0.05 * (
...      rng.standard_normal(k.size)
...      + 1j * rng.standard_normal(k.size))).astype(np.complex64)
>>> y = c.steps(x)                 # de-rotated toward DC
>>> y.shape[0]
40000
>>> round(c.norm_freq, 4)          # tracked carrier, cycles/sample
0.001
>>> c.lock > 0.5                    # carrier lock metric, ~1 at lock
True


function carrier_nda_steps_max_out

size_t carrier_nda_steps_max_out (
    carrier_nda_state_t * state
) 

function carrier_nda_tlm_flush

Emit the carrier loop's telemetry records for the current sample.

void carrier_nda_tlm_flush (
    const carrier_nda_state_t * s
) 

Out-of-line on purpose: the emit machinery must not inline into the per-sample hot loop (inlined ring-write expansions bloat the loop body and an extern call site forces per-iteration state reloads — both measured ~20% slower detached on other loops). Callers gate on s->tlm.ctx. This loop updates every sample, so the natural call rate is per sample — decim (set at attach) is the throttle. Records "<prefix>.lock" (the lock-signal EMA), "<prefix>.e" (the M-th-power phase discriminator — the loop stress), "<prefix>.freq" (the tracked carrier, NCO centre + integrated correction, cycles/sample) and "<prefix>.locked" (the verify-counted lockdet decision, 0/1). A composing receiver (the MPSK receiver) calls this once per recovered symbol instead.

Parameters:

  • s State with a non-NULL tlm.ctx (caller-checked).

function carrier_nda_wipeoff

Per-sample carrier wipe-off: de-rotate x by the NCO, advance it.

JM_FORCEINLINE  JM_HOT float _Complex carrier_nda_wipeoff (
    carrier_nda_state_t * s,
    float _Complex x
) 

Parameters:

  • s Carrier loop state. Must be non-NULL.
  • x One input sample.

Returns:

The de-rotated sample to feed the moving-average arm.


Macro Definition Documentation

define CARRIER_NDA_EPS

#define CARRIER_NDA_EPS `1e-12`

define CARRIER_NDA_INV_2PI

#define CARRIER_NDA_INV_2PI `0.15915494309189535 /* 1 / (2*pi) */`

define CARRIER_NDA_LOCK_ALPHA

#define CARRIER_NDA_LOCK_ALPHA `0.05`

define CARRIER_NDA_LOCK_NORM_SD

#define CARRIER_NDA_LOCK_NORM_SD `0.11322770341445956`

define CARRIER_NDA_STATE_MAGIC

#define CARRIER_NDA_STATE_MAGIC `DP_FOURCC ('C', 'N', 'D', 'A')`

define CARRIER_NDA_STATE_VERSION

#define CARRIER_NDA_STATE_VERSION `/* multi line expression */`


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