File carrier_mpsk_core.h¶
FileList > carrier_mpsk > carrier_mpsk_core.h
Go to the source code of this file
M-PSK carrier-tracking loop (integer-NCO de-rotation + decision PLL). More...
#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 "mpsk/mpsk_core.h"#include <math.h>
Classes¶
| Type | Name |
|---|---|
| struct | carrier_mpsk_state_t M-PSK carrier loop state. |
Public Functions¶
| Type | Name |
|---|---|
| void | carrier_mpsk_configure (carrier_mpsk_state_t * state, double bn, double zeta) Recompute the loop gains for a new (bn, zeta); keep the estimate. |
| carrier_mpsk_state_t * | carrier_mpsk_create (double bn, double zeta, double init_norm_freq, size_t tsamps, double bn_fll, int m) Create an M-PSK carrier loop instance. |
| void | carrier_mpsk_destroy (carrier_mpsk_state_t * state) Destroy an M-PSK carrier loop instance and release all memory. |
| double | carrier_mpsk_get_bn (const carrier_mpsk_state_t * state) |
| double | carrier_mpsk_get_bn_fll (const carrier_mpsk_state_t * state) |
| double | carrier_mpsk_get_last_error (const carrier_mpsk_state_t * state) |
| double | carrier_mpsk_get_lock_metric (const carrier_mpsk_state_t * state) |
| int | carrier_mpsk_get_m (const carrier_mpsk_state_t * state) |
| double | carrier_mpsk_get_norm_freq (const carrier_mpsk_state_t * state) |
| void | carrier_mpsk_get_state (const carrier_mpsk_state_t * state, void * blob) Serialize the full loop state into blob . |
| void | carrier_mpsk_init (carrier_mpsk_state_t * s, double bn, double zeta, double init_norm_freq, size_t tsamps, double bn_fll, int m) Initialise an M-PSK carrier loop in place (no allocation). |
| void | carrier_mpsk_reset (carrier_mpsk_state_t * state) Re-seed the loop to its create-time frequency/phase; keep config. |
| void | carrier_mpsk_set_bn (carrier_mpsk_state_t * state, double val) |
| void | carrier_mpsk_set_bn_fll (carrier_mpsk_state_t * state, double val) |
| void | carrier_mpsk_set_norm_freq (carrier_mpsk_state_t * state, double val) |
| int | carrier_mpsk_set_state (carrier_mpsk_state_t * state, const void * blob) Restore state; DP_OK, or DP_ERR_INVALID if the envelope rejects. |
| size_t | carrier_mpsk_state_bytes (const carrier_mpsk_state_t * state) Serialized-state byte size. |
| size_t | carrier_mpsk_steps (carrier_mpsk_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out) Track the residual carrier over a block, one prompt per symbol. |
| size_t | carrier_mpsk_steps_max_out (carrier_mpsk_state_t * state) |
| JM_FORCEINLINE JM_HOT void | carrier_mpsk_update (carrier_mpsk_state_t * s, float _Complex P) Per-symbol carrier update: decision discriminator -> loop -> NCO. |
| JM_FORCEINLINE JM_HOT float _Complex | carrier_mpsk_wipeoff (carrier_mpsk_state_t * s, float _Complex x) Per-sample carrier wipe-off: de-rotate x by the NCO, advance it. |
Macros¶
| Type | Name |
|---|---|
| define | CARRIER_MPSK_EPS 1e-12f |
| define | CARRIER_MPSK_LOCK_ALPHA 0.1 |
| define | CARRIER_MPSK_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc)('C', 'M', 'P', 'K') |
| define | CARRIER_MPSK_STATE_VERSION 1u |
Detailed Description¶
The M-ary generalization of the Costas loop (costas_core.h): per sample it de-rotates the input with the integer-phase lo NCO (carrier wipe-off); every tsamps samples it dumps the coherent integrate-and-dump prompt, runs a decision-directed M-PSK phase discriminator, filters the error through an embedded 2nd-order loop_filter_state_t, and steers the NCO frequency + phase. It tracks a small residual carrier (bulk Doppler is removed upstream by acquisition); the steering NCO is lo, so the phase is bounded and exactly reproducible.
The discriminator slices the prompt to the nearest constellation point ahat = mpsk_slice(P, m) and uses e = Im(P * conj(ahat)) / |P| (= sin of the phase error near lock). At m = 2 this reduces exactly to the BPSK Costas discriminator. An optional decision-directed cross-product FLL assist (bn_fll > 0) widens the frequency pull-in.
The loop locks to one of m phases — an M-fold ambiguity on absolute phase. Resolve it downstream with differential demapping (mpsk_diff_demap) or a sync word; this loop only recovers the carrier and emits the prompts.
The block API (carrier_mpsk_steps) is the Python face; the JM_FORCEINLINE carrier_mpsk_wipeoff()/carrier_mpsk_update() are the C composition API a receiver inlines into its own sample loop.
// QPSK carrier loop, 64 samples/symbol, FLL-assisted
carrier_mpsk_state_t *c = carrier_mpsk_create(0.05, 0.707, 0.0, 64, 0.01, 4);
float _Complex sym[16];
size_t k = carrier_mpsk_steps(c, rx, rx_len, sym, 16);
double f = c->nco.norm_freq; // tracked residual carrier
carrier_mpsk_destroy(c);
Public Functions Documentation¶
function carrier_mpsk_configure¶
Recompute the loop gains for a new (bn, zeta); keep the estimate.
Re-derives the proportional/integral gains of the embedded 2nd-order loop filter for the new noise bandwidth and damping, leaving the running frequency and phase estimate (the NCO and the loop integrator) untouched — a live lock survives a re-tune. Use it to widen the loop for fast pull-in and then narrow it for low-jitter tracking, mid-stream.
Parameters:
stateMust be non-NULL.bnLoop noise bandwidth, normalised to the symbol rate.zetaDamping factor (0.707 = critically damped).>>> from doppler.track import CarrierMpsk >>> c = CarrierMpsk(bn=0.02, zeta=0.707, init_norm_freq=0.01, ... tsamps=16, bn_fll=0.0, m=4) >>> round(c.bn, 3) 0.02 >>> c.configure(bn=0.05, zeta=1.0) # widen the loop mid-stream >>> round(c.bn, 3) 0.05 >>> round(c.norm_freq, 3) # frequency estimate preserved 0.01
function carrier_mpsk_create¶
Create an M-PSK carrier loop instance.
carrier_mpsk_state_t * carrier_mpsk_create (
double bn,
double zeta,
double init_norm_freq,
size_t tsamps,
double bn_fll,
int m
)
Parameters:
bnLoop noise bandwidth (default 0.05).zetaDamping factor (default 0.707).init_norm_freqSeed carrier frequency, cycles/sample (default 0.0).tsampsSamples per symbol (default 64).bn_fllFLL-assist bandwidth (default 0.0 = pure PLL).mConstellation order M, 2/4/8 (default 4 = QPSK).
Returns:
Heap-allocated state, or NULL on allocation failure.
Note:
Caller must call carrier_mpsk_destroy() when done.
function carrier_mpsk_destroy¶
Destroy an M-PSK carrier loop instance and release all memory.
Parameters:
stateMay be NULL.
function carrier_mpsk_get_bn¶
function carrier_mpsk_get_bn_fll¶
function carrier_mpsk_get_last_error¶
function carrier_mpsk_get_lock_metric¶
function carrier_mpsk_get_m¶
function carrier_mpsk_get_norm_freq¶
function carrier_mpsk_get_state¶
Serialize the full loop state into blob .
function carrier_mpsk_init¶
Initialise an M-PSK carrier loop in place (no allocation).
void carrier_mpsk_init (
carrier_mpsk_state_t * s,
double bn,
double zeta,
double init_norm_freq,
size_t tsamps,
double bn_fll,
int m
)
Seeds the NCO at init_norm_freq and the loop integrator to the matching per-symbol frequency so de-rotation is correct from the first sample.
Parameters:
sState to initialise. Must be non-NULL.bnLoop noise bandwidth, normalised to the symbol rate.zetaDamping factor (0.707 = critically damped).init_norm_freqSeed carrier frequency, cycles/sample.tsampsSamples per symbol (the integrate-and-dump period).bn_fllFLL-assist bandwidth (0 = pure PLL).mConstellation order M (2, 4, 8).
function carrier_mpsk_reset¶
Re-seed the loop to its create-time frequency/phase; keep config.
Returns the NCO to the seed carrier passed at construction, zeroes the integrate-and-dump accumulator, the FLL history, and the lock/error diagnostics, and re-primes the loop integrator to the matching per-symbol frequency — the exact state a fresh carrier_mpsk_create() leaves. The tuning (bn, zeta, bn_fll, tsamps, m) is untouched. Call it at a capture boundary so a lock reached on one segment does not bias an unrelated next one.
Parameters:
stateMust be non-NULL.>>> import numpy as np >>> from doppler.mpsk import mpsk_map >>> from doppler.track import CarrierMpsk >>> rng = np.random.default_rng(1) >>> sig = np.repeat( ... mpsk_map(rng.integers(0, 4, 100).astype(np.uint8), 4), ... 16).astype(np.complex64) >>> rx = (sig * np.exp(2j * np.pi * 0.003 * np.arange(len(sig))) ... ).astype(np.complex64) >>> c = CarrierMpsk(bn=0.04, zeta=0.707, init_norm_freq=0.0, ... tsamps=16, bn_fll=0.02, m=4) >>> _ = c.steps(rx) >>> round(c.norm_freq, 3) # loop pulled onto the residual carrier 0.003 >>> c.reset() # back to the create-time seed >>> round(c.norm_freq, 3) 0.0
function carrier_mpsk_set_bn¶
function carrier_mpsk_set_bn_fll¶
function carrier_mpsk_set_norm_freq¶
function carrier_mpsk_set_state¶
Restore state; DP_OK, or DP_ERR_INVALID if the envelope rejects.
function carrier_mpsk_state_bytes¶
Serialized-state byte size.
function carrier_mpsk_steps¶
Track the residual carrier over a block, one prompt per symbol.
size_t carrier_mpsk_steps (
carrier_mpsk_state_t * state,
const float _Complex * x,
size_t x_len,
float _Complex * out,
size_t max_out
)
The block form of the inline wipeoff/update pair: for each input sample it de-rotates by the carrier NCO and accumulates the coherent integrate-and-dump; every tsamps samples it dumps the prompt, runs the decision-directed M-PSK discriminator (slice to the nearest constellation point, error Im(P conj(ahat))/|P|, plus the optional cross-product FLL assist), filters the error, and steers the NCO frequency and phase. Exactly one de-rotated prompt is emitted per completed symbol; a trailing partial symbol is carried in the accumulator to the next call, so a stream can be fed in blocks of any length with no seam.
The loop locks to one of m carrier phases — an M-fold ambiguity on the absolute constellation orientation. Resolve it downstream (differential demapping or a sync word); this call only recovers the carrier and returns the prompts. At m = 2 it is exactly the BPSK Costas loop.
Parameters:
stateCarrier loop state (mutated). Must be non-NULL.xInput block, one complex baseband sample per element.x_lenNumber of input samples.outPrompt output buffer written by the binding.max_outCapacity ofout, in symbols.
Returns:
One de-rotated prompt symbol per completed integrate-and-dump period; the count is x_len / tsamps.
>>> import numpy as np
>>> from doppler.mpsk import mpsk_map
>>> from doppler.track import CarrierMpsk
>>> rng = np.random.default_rng(0)
>>> sps = 16
>>> labels = rng.integers(0, 4, 400).astype(np.uint8)
>>> sig = np.repeat(mpsk_map(labels, 4), sps).astype(np.complex64)
>>> k = np.arange(len(sig))
>>> rx = (sig * np.exp(2j * np.pi * 0.002 * k)).astype(np.complex64)
>>> c = CarrierMpsk(bn=0.04, zeta=0.707, init_norm_freq=0.0,
... tsamps=sps, bn_fll=0.02, m=4)
>>> prompts = c.steps(rx) # one prompt per symbol
>>> prompts.shape
(400,)
>>> round(c.norm_freq, 4) # tracked the residual carrier 0.002
0.002
>>> round(c.lock_metric, 2) # decision-aligned lock metric -> 1
1.0
function carrier_mpsk_steps_max_out¶
function carrier_mpsk_update¶
Per-symbol carrier update: decision discriminator -> loop -> NCO.
Slices the prompt P to the nearest M-PSK point ahat, forms the decision-directed phase error e = Im(P conj(ahat)) / |P|, optionally runs a decision-directed cross-product FLL on the data-wiped prompts, filters, and steers the NCO frequency + a proportional phase nudge. Updates the lock metric (decision-aligned Re(P conj(ahat))/|P|) and last_error. Inline.
Parameters:
sCarrier loop state. Must be non-NULL.PThe dumped integrate-and-dump prompt for this symbol.
function carrier_mpsk_wipeoff¶
Per-sample carrier wipe-off: de-rotate x by the NCO, advance it.
JM_FORCEINLINE JM_HOT float _Complex carrier_mpsk_wipeoff (
carrier_mpsk_state_t * s,
float _Complex x
)
x * conj(lo_step(nco)) — strips the (tracked) carrier ahead of the matched-filter integrate-and-dump. Inline, zero call overhead.
Parameters:
sCarrier loop state. Must be non-NULL.xOne input sample.
Returns:
The de-rotated sample to feed the integrator.
Macro Definition Documentation¶
define CARRIER_MPSK_EPS¶
define CARRIER_MPSK_LOCK_ALPHA¶
define CARRIER_MPSK_STATE_MAGIC¶
define CARRIER_MPSK_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/carrier_mpsk/carrier_mpsk_core.h