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⁴→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:
sCarrier loop state. Must be non-NULL.dOne de-rotated sample (from carrier_nda_wipeoff).peReceives the phase error.lockReceives 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:
bnLoop noise bandwidth (default 0.01).zetaDamping factor (default 0.707).init_norm_freqSeed carrier frequency, cycles/sample (default 0.0).spsSamples per symbol (default 8).nMA window divisor: window = sps/n (default 4; spsn==0).mConstellation 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.
Parameters:
stateMay 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⁴→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:
zArm moving-average sample (AGC-normalized, ~unit at lock).mConstellation order (2, 4, 8).scalePer-M lock scale (1 / 0.619 / 0.412).peReceives the phase error.lockReceives the lock signal.
function carrier_nda_get_bn¶
function carrier_nda_get_last_error¶
function carrier_nda_get_lock¶
function carrier_nda_get_m¶
function carrier_nda_get_n¶
function carrier_nda_get_norm_freq¶
function carrier_nda_get_sps¶
function carrier_nda_get_state¶
Serialize the full loop state into 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:
sState to initialise. Must be non-NULL.bnLoop noise bandwidth, cycles/sample (per-sample loop).zetaDamping factor (0.707 = critically damped).init_norm_freqSeed carrier frequency, cycles/sample.spsSamples per symbol.nMA window divisor: window = sps/n samples (sps % n == 0, sps/n <= BOXCAR_MAX_LEN).mConstellation order M (2, 4, 8).
function carrier_nda_reset¶
Re-seed the loop to its create-time frequency/phase; keep config.
Parameters:
stateMust be non-NULL.
function carrier_nda_set_bn¶
function carrier_nda_set_norm_freq¶
function carrier_nda_set_state¶
Restore state; DP_OK, or DP_ERR_INVALID if the envelope rejects.
function carrier_nda_state_bytes¶
Serialized-state byte size.
function carrier_nda_steer¶
Steer the shared NCO with a phase error through the loop filter.
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:
sCarrier loop state. Must be non-NULL.pePhase 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¶
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:
sCarrier loop state. Must be non-NULL.xOne 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_BW_RATIO¶
define CARRIER_NDA_AGC_CLIP_DB¶
define CARRIER_NDA_AGC_REF_DB¶
define CARRIER_NDA_EPS¶
define CARRIER_NDA_INV_2PI¶
define CARRIER_NDA_LOCK_ALPHA¶
define CARRIER_NDA_STATE_MAGIC¶
define CARRIER_NDA_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/carrier_nda/carrier_nda_core.h