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 "agc/agc_core.h"
  • #include "boxcar/boxcar_core.h"
  • #include "clib_common.h"
  • #include "dp_state.h"
  • #include "jm_perf.h"
  • #include "lo/lo_core.h"
  • #include "loop_filter/loop_filter_core.h"
  • #include <math.h>

Classes

Type Name
struct carrier_nda_state_t
NDA M-th-power carrier loop state.

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.
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 scale, double * pe, double * lock)
The M-th-power discriminator on an arm sample (raw, no per-dump limit).
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_m (const carrier_nda_state_t * state)
int carrier_nda_get_n (const carrier_nda_state_t * state)
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.
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)
size_t carrier_nda_steps_max_out (carrier_nda_state_t * state)
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_AGC_ALPHA 0.01
define CARRIER_NDA_AGC_BW_RATIO 0.01
define CARRIER_NDA_AGC_CLIP_DB 10.0
define CARRIER_NDA_AGC_REF_DB 0.0
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_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc)('C', 'N', 'D', 'A')
define CARRIER_NDA_STATE_VERSION 2u /\* v2: moving-average arm (ring + sum) \*/

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 (unit-normalized) 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⁸). Each level yields a phase error and a lock signal: * phase_error = Im(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^M) (× a per-M lock_scale) for M ≤ 4, and a faithful monotone lock detector for M = 8 — ~1 when phase-locked, ~0 with no carrier. Its EMA (lock) is the carrier lock metric. See docs/design/mpsk.md §2.3 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:

Input average power must be at or below unity. The arm sample feeding the M-th-power detector is normalized to unit average power by an internal AGC (bandwidth = 0.01*bn) with a 10 dB square clip, so the loop gain is amplitude-invariant. This is the normal convention for captured/scaled baseband (and holds for the DSSS despreader's correlation gain). A cold input more than ~10 dB above unity is out of spec: the deliberately slow AGC cannot normalize it before the discriminator reacts, and the loop can false-lock. The AGC absorbs residual/slow level variation, not a large cold offset.

// 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 AGC-normalized 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_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 (raw, no per-dump limit).

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

Runs the repeated-squaring recursion z⁴z⁸ directly on the arm sample z (the "conventional Costas" / linear-arm form) and writes the phase error (= scaled Im(z^M)) and lock signal. The arm sample is expected to be AGC-normalized to unit average power upstream (carrier_nda_arm_step runs the window average through an embedded agc_core AGC) — so a clean window is |z|≈1 and a transition-straddling window is |z|<1 and is down-weighted naturally. This is deliberate: a per-sample unit-magnitude normalization is Yuen's "polarity-type" hard limiter, the worst nonlinearity (≈2.5–4 dB extra squaring loss, and non-monotonic in SNR — see docs/design/mpsk.md §2.3). On a unit-magnitude z the raw and normalized forms coincide, so the S-curve and lock-scale calibration are unchanged.

Parameters:

  • z Arm moving-average sample (AGC-normalized, ~unit at lock).
  • m Constellation order (2, 4, 8).
  • scale Per-M lock scale (1 / 0.619 / 0.412).
  • 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_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_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
) 

Parameters:

  • state Must be non-NULL.

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_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

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
) 

function carrier_nda_steps_max_out

size_t carrier_nda_steps_max_out (
    carrier_nda_state_t * state
) 

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_AGC_ALPHA

#define CARRIER_NDA_AGC_ALPHA `0.01`

define CARRIER_NDA_AGC_BW_RATIO

#define CARRIER_NDA_AGC_BW_RATIO `0.01`

define CARRIER_NDA_AGC_CLIP_DB

#define CARRIER_NDA_AGC_CLIP_DB `10.0`

define CARRIER_NDA_AGC_REF_DB

#define CARRIER_NDA_AGC_REF_DB `0.0`

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_STATE_MAGIC

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

define CARRIER_NDA_STATE_VERSION

#define CARRIER_NDA_STATE_VERSION `2u /* v2: moving-average arm (ring + sum) */`


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