Skip to content

File dll_core.h

FileList > dll > dll_core.h

Go to the source code of this file

Delay-lock loop (DLL) — non-coherent early/prompt/late code tracking. More...

  • #include "doppler/clib_common.h"
  • #include "doppler/dp_state.h"
  • #include "doppler/jm_perf.h"
  • #include "doppler/lockdet/lockdet_core.h"
  • #include "doppler/loop_filter/loop_filter_core.h"
  • #include "doppler/nco/nco_core.h"
  • #include "doppler/dp_tlm/dp_tlm_core.h"
  • #include "doppler/dp_complex.h"
  • #include <math.h>
  • #include "doppler/detection/detection_core.h"
  • #include "doppler/telemetry/telemetry_core.h"

Classes

Type Name
struct dll_tlm_t
Telemetry attachment: a borrowed context + this object's probe ids. NULL ctx (the default) means detached — every probe site is then a single predicted-not-taken branch per code epoch. Zeroed in state blobs and preserved across set_state (the hand-written triplet treats it like the borrowed code ).
struct dp_dll_state_t
DLL state.

Public Functions

Type Name
JM_FORCEINLINE JM_HOT int dll_accumulate (dp_dll_state_t * s, float _Complex d)
Per-sample early/prompt/late correlate + fixed-point code-phase advance.
JM_FORCEINLINE float dll_chip_sign (uint8_t c)
JM_FORCEINLINE JM_HOT void dll_lock_accumulate (dp_dll_state_t * s, float _Complex d)
Per-sample offset (noise) tap for the always-on lock detector.
JM_FORCEINLINE float dll_replica_word (const dp_dll_state_t * s, uint32_t w)
Sub-chip code replica at one tap's phase word.
JM_FORCEINLINE JM_HOT void dll_steer (dp_dll_state_t * s, double ep, double lp, double pp)
The code discriminator, its filter and the NCO steer the ONE steer both correlation paths call (doppler#1280).
JM_FORCEINLINE uint32_t dll_tap_base (const dp_dll_state_t * s)
This sample's prompt tap as a phase word: its dwell CENTER ( nco_dwell_center() , the zero-order-hold rule), on the replica grid's origin.
JM_FORCEINLINE JM_HOT void dll_update (dp_dll_state_t * s)
Per-period code discriminator + loop update + NCO steer on the dumped accumulators (the coherent full-epoch path).
int dp_dll_configure (dp_dll_state_t * state, double bn, double zeta)
Recompute the loop gains for a new (bn, zeta); keep the code state.
int dp_dll_configure_lock (dp_dll_state_t * state, double pfa, size_t n_looks, double ref_snr_db)
Tune the always-on code-lock detector to a target (pfa, n_looks).
void dp_dll_configure_lock_raw (dp_dll_state_t * state, double up_thresh, double down_thresh, size_t n_looks, double alpha, uint32_t n_up, uint32_t n_down)
Set the lock detector's raw geometry directly.
dp_dll_state_t * dp_dll_create (const uint8_t * code, size_t code_len, size_t sps, double init_chip, double bn, double zeta, double spacing, size_t segments)
Create a code/timing delay-locked loop over a spreading code.
void dp_dll_destroy (dp_dll_state_t * state)
Destroy a DLL instance and release all memory (incl. the code copy).
double dp_dll_get_bn (const dp_dll_state_t * state)
double dp_dll_get_code_phase (const dp_dll_state_t * state)
double dp_dll_get_code_rate (const dp_dll_state_t * state)
double dp_dll_get_last_error (const dp_dll_state_t * state)
double dp_dll_get_lock_stat (const dp_dll_state_t * state)
Last lock test statistic R (compare against the configured eta).
int dp_dll_get_locked (const dp_dll_state_t * state)
Current lock decision (1 = locked, 0 = not), with the configured verify-count / hysteresis rule applied (see dp_dll_configure_lock).
double dp_dll_get_noise_est (const dp_dll_state_t * state)
Current CFAR noise-power estimate E|O|^2 (offset-tap EMA).
size_t dp_dll_get_segments (const dp_dll_state_t * state)
void dp_dll_get_state (const dp_dll_state_t * state, void * blob)
size_t dp_dll_get_symbol_window (const dp_dll_state_t * state)
The lock detector's coherent window, in partials (0 when the symbol-period aid is off). Size n_looks from it.
void dp_dll_hold_here (dp_dll_state_t * state)
Mark the point a coast returns to: this rate and this filter.
void dp_dll_init (dp_dll_state_t * s, const uint8_t * code, size_t code_len, size_t sps, double init_chip, double bn, double zeta, double spacing)
Initialise a DLL in place (no allocation); BORROWS code .
void dp_dll_lock_epoch (dp_dll_state_t * s)
Per-epoch lock-detector housekeeping: re-draw the noise offset.
void dp_dll_lock_look (dp_dll_state_t * s, double norm)
Fold one look into the lock detector; clear the offset tap.
size_t dp_dll_lookback_segments (size_t tsamps, double max_error_db)
Derive a principled segments count from a max tolerable async-lookback correlation-power loss, instead of hand-picking one.
int dp_dll_params_ok (size_t code_len, double init_chip, double bn, double zeta, double spacing, size_t segments)
The DLL's one domain predicate: the arguments dp_dll_create() builds a loop from.
void dp_dll_reset (dp_dll_state_t * state)
Re-seed the loop to its create-time code phase; keep config.
int dp_dll_set_bn (dp_dll_state_t * state, double val)
Set the loop bandwidth, keeping zeta.
void dp_dll_set_coast (dp_dll_state_t * state, int coast)
Hold the loop (1) or run it (0, the default).
void dp_dll_set_code_phase (dp_dll_state_t * state, double chips)
Set the prompt code phase, in chips: the correction a holder applies to a coasting loop.
int dp_dll_set_lock_verify (dp_dll_state_t * state, uint32_t n_up, uint32_t n_down)
Set the lock detector's verify counts, keeping its thresholds.
void dp_dll_set_rate_aid (dp_dll_state_t * state, double rate_aid)
Set the carrier-aiding code-rate deviation (ratio; 0 = off).
int dp_dll_set_state (dp_dll_state_t * state, const void * blob)
Restore from blob .
int dp_dll_set_symbol_period (dp_dll_state_t * state, double partials_per_symbol)
Give the loop the data-symbol period, so the lock detector's looks and the code discriminator's windows are coherent over a symbol instead of a quarter-epoch partial.
int dp_dll_set_telemetry (dp_dll_state_t * state, dp_tlm_t * tlm, const char * prefix, uint32_t decim)
Attach (or detach) a telemetry context and register the code loop's probes on it. Registers five probes, emitted once per code epoch (period) and further thinned by decim: "<prefix>.e" (the early-minus-late envelope discriminator — the loop stress), "<prefix>.rate" (the tracked code rate, chips advanced per nominal chip, ~1.0 at lock — dp_dll_get_code_rate() , the loop's own rate, without the carrier aid; a held loop reports its held integrator's), "<prefix>.lock" (the CFAR lock statistic R; compare against the configured threshold), "<prefix>.locked" (the verify-counted lock decision, 0/1 — the lockdet output, so a consumer sees where the declare/drop rule fired without re-deriving it from the statistic) and "<prefix>.nco" (what the code NCO is commanded: the complete loop-filter output, the integrator, the proportional term and the carrier aid, as chips per nominal chip, at the NCO's resolution of one phase-increment step; a held loop's held command). 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 dp_dll_state_bytes (const dp_dll_state_t * state)
size_t dp_dll_steps (dp_dll_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out)
Correlate a carrier-wiped block against the local code and steer the code NCO once per code period.
size_t dp_dll_steps_max_out (dp_dll_state_t * state)
int dp_dll_symbol_period_ok (size_t segments, double partials_per_symbol)
Whether dp_dll_set_symbol_period() takespartials_per_symbol as a period to turn the symbol aid ON.
size_t dp_dll_take_error (dp_dll_state_t * state, double * sum)
Take the discriminator's running sum: the steers since the last take, their sum, both zeroed.
double dp_dll_take_error_mean (dp_dll_state_t * state)
dp_dll_take_error() as one number: the mean of the steers taken, or NaN when none were the Python face of the primitive.
void dp_dll_tlm_flush (const dp_dll_state_t * s)
Emit the code loop's telemetry records for the epoch just closed.

Macros

Type Name
define DLL_AID_MAX_PERIOD 1048576.0
The longest symbol period, in partials, the symbol aid takes.
define DLL_DISC_CLAMP 1.0
define DLL_EPS 1e-12
define DLL_OUT_LEVEL_PERIODS 32u
define DLL_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('D','L','L',' ')
define DLL_STATE_VERSION /* multi line expression */

Detailed Description

Tracks the code phase of a continuous, repeating spreading code (e.g. a PN / Gold sequence) on a carrier-wiped sample stream. Per sample it correlates the input against three taps of a 2-samples/chip interpolated local-code replica — early (+spacing chips), prompt, late (-spacing chips) — accumulating an integrate-and-dump over one code period; per period it runs the power-domain non-coherent early-minus-late discriminator 0.5 * (|E|^2 - |L|^2) / |P|^2, filters it through an embedded 2nd-order dp_loop_filter_state_t, and steers an embedded fixed-point NCO (dp_nco_state_t) that tracks the code phase — a 32-bit phase accumulator, exact integer wraparound, no open-ended floating-point drift.

It pairs with the carrier loop (costas_core.h): the carrier loop wipes the carrier, the DLL wipes the code. The block API (dp_dll_steps) is the Python face; the JM_FORCEINLINE dll_accumulate()/dll_update() are the C composition API a tracking channel inlines into its own sample loop.

Lifecycle: dp_dll_create -> (steps / configure / reset)* -> dp_dll_destroy, or embed by value with dp_dll_init() (which BORROWS the caller-owned code, and always runs with segments == 1 — there is no by-value counterpart to dp_dll_create()'s segments parameter).

uint8_t code[31] = { ... };  // 0/1 chips, one period
dp_dll_state_t *d = dp_dll_create(code, 31, 2, 0.0, 0.01, 0.707, 0.5);
float _Complex sym[16];
size_t k = dp_dll_steps(d, rx, rx_len, sym, 16);  // one prompt per period
double phase = d->chip_pos;                     // tracked code phase, chips
dp_dll_destroy(d);

Public Functions Documentation

function dll_accumulate

Per-sample early/prompt/late correlate + fixed-point code-phase advance.

JM_FORCEINLINE  JM_HOT int dll_accumulate (
    dp_dll_state_t * s,
    float _Complex d
) 

Correlates the carrier-wiped sample d against the early, prompt and late code taps at this sample's dwell-CENTER phase (dll_tap_base, the taps tap_spacing either side, all wrapping over the periodic code as phase words), then advances the embedded fixed-point NCO by one sample and re-derives chip_pos from its POST-advance (dwell-START-of-next-sample) phase (never independently accumulated — see dp_dll_state_t::chip_pos). Inline, zero call overhead.

Parameters:

  • s DLL state. Must be non-NULL.
  • d One carrier-wiped input sample.

Returns:

1 if this sample's advance wrapped the code period (a period boundary), 0 otherwise. A plain return value, not a persistent struct field — a stored wrap flag that a caller forgets to consume is exactly the class of bug an earlier attempt at this redesign hit (a stale flag causing an infinite loop under segments>1 stress); a local value cannot go stale.


function dll_chip_sign

JM_FORCEINLINE float dll_chip_sign (
    uint8_t c
) 

0/1 chip -> +1/-1 BPSK sign.


function dll_lock_accumulate

Per-sample offset (noise) tap for the always-on lock detector.

JM_FORCEINLINE  JM_HOT void dll_lock_accumulate (
    dp_dll_state_t * s,
    float _Complex d
) 

The composition sibling of dll_accumulate(): correlates the input against the code at this epoch's random off-peak offset (tap_off from the prompt), feeding the CFAR noise reference. Call it on the same sample stream as dll_accumulate() and BEFORE it (both taps evaluate this sample's dwell-CENTER phase, dll_tap_base — dll_accumulate() hasn't advanced the NCO yet, so the two calls see the same phase). A composer that skips this (and dp_dll_lock_look()/dp_dll_lock_epoch()) simply leaves the lock detector idle — locked stays 0, lock_stat/noise_est stay 0.

Parameters:

  • s DLL state. Must be non-NULL.
  • d One carrier-wiped input sample (same sample as dll_accumulate).

function dll_replica_word

Sub-chip code replica at one tap's phase word.

JM_FORCEINLINE float dll_replica_word (
    const dp_dll_state_t * s,
    uint32_t w
) 

The code is treated as held at a fixed 2 samples/chip, sampled at chip-relative positions {0.25, 0.75} (a quarter and three-quarters into each chip) and linearly interpolated at any fractional position — NOT at {0, 0.5}, which would confine the whole linear-interpolation transition zone to one side of each chip boundary (found during the initial port: a real bug, not a cosmetic asymmetry — it broke E/L symmetry around P badly enough to leave a large discriminator offset at perfect lock). Quarter/three-quarter placement centers the transition zone symmetrically on each boundary (spanning [chip-0.25, chip+0.25)), and both quarter-chip flat regions are pure sign(code[chip]) (no table is materialised — the 2x-oversampled index converts straight back to a chip index via >> 1, same as an unshifted grid would, only the phase origin moves). A point-sample interpolation, not a dwell-width-aware blend: this replaces the earlier exact matched-filter integral (which varied its blend width with the sample's chip-phase dwell time) with the simpler, validated design from docs/design/async-dsss-receiver.md §3.6 — the dwell-integral model was mathematically fancier but did not, on its own, fix the long-run false-lock that motivated this redesign; this one does.

The tap's phase is a PHASE WORD: its code phase as a fraction of one period in 32 bits, the code NCO's own unit, already moved to the grid's origin a quarter chip in (dll_tap_base()). nco_word_to_grid() reads it on the 2*sf-point grid the NCO header's grid face, which any table read at an NCO's phase shares so a tap costs no division, no fmod and no double, which is where the loop's time went when a tap was a chip count in double (doppler#2397).

Parameters:

  • s DLL state (for the code and period length).
  • w The tap's phase word, from dll_tap_base() plus a tap offset.

Returns:

Linearly-interpolated ±1 replica value for this tap and sample.


function dll_steer

The code discriminator, its filter and the NCO steer the ONE steer both correlation paths call (doppler#1280).

JM_FORCEINLINE  JM_HOT void dll_steer (
    dp_dll_state_t * s,
    double ep,
    double lp,
    double pp
) 

Runs the power-domain non-coherent early-minus-late discriminator 0.5 * (|E|^2 - |L|^2) / |P|^2 on the given powers (the prompt power is the normalizing "signal + noise power" reference the validated design from docs/design/async-dsss-receiver.md §3.6, not a magnitude-domain (|E|-|L|)/(|E|+|L|) ratio), clamps it, records it (last_error, the .e probe, and the running sum dp_dll_take_error() reads), and then unless the loop is held (dp_dll_set_coast()) filters it and steers phase_inc (sample-and-hold, constant until the next call) from BOTH the integrator and the proportional term, spread smoothly across the whole next update interval rather than kicked directly into phase. Two things were tried and rejected while porting this design: (1) folding the loop filter's full combined control (integ + kp*e) into phase_inc as a single rate (mirroring symsync_core.h) massively over-corrects a rate held for a whole sf*sps-sample period turns a kp*e-chip correction into a kp*e*sf-chip one, unstable at any non-tiny bn; (2) kicking phase directly once per period (mirroring costas_core.c) is unsafe right after a wrap (when phase is near zero) a negative kick pushes phase backward across the just-crossed boundary, and the very next sample's forward step re-crosses it, registering a second, spurious wrap.

How the filter's two terms reach phase_inc and code_rate is the state's gain table (ctrl_i, ctrl_p, rate_i, rate_p, set by segments at configuration and scaled by inv_upd when the symbol aid re-times the update): the coherent full-epoch loop (segments == 1) treats the integrator as a code-rate ratio and the proportional term as chips per epoch; the partial-correlation loop (segments > 1) treats the filter's output as chips over sps. Until this function existed each path carried its own copy of the discriminator and steer, and a fix to one (the coast hold, design §12.22) did not reach the other. The gain tables are the two loops' separately pinned behaviour, kept as data.

The NCO free-runs at its own nominal rate (1/tsamps cycles/sample, set once in seed() and never touched directly); this function only ever computes a pure correction (ctrl, built entirely from the loop filter's terms no "1.0"/nominal anywhere in it) and adds it on top when steering phase_inc, beside the rate aid. Keeping the control path free of the nominal rate is what lets a second correction source sum in cleanly, and is what code_rate (a public, ratio-form observable 1.0 = nominal, unrelated to how phase_inc is actually steered) must never be substituted for internally. Inline; no division.

Parameters:

  • s DLL state. Must be non-NULL.
  • ep Early power |E|^2 over the update's window.
  • lp Late power |L|^2.
  • pp Prompt power |P|^2 (the reference), on the same scale.

function dll_tap_base

This sample's prompt tap as a phase word: its dwell CENTER ( nco_dwell_center() , the zero-order-hold rule), on the replica grid's origin.

JM_FORCEINLINE uint32_t dll_tap_base (
    const dp_dll_state_t * s
) 

The result is the code NCO's own phase word, so it wraps over the period as the word overflows, less tap_grid, the quarter chip that puts it on dll_replica_word()'s grid. Every other tap is this plus a constant word: tap_spacing either side for early and late, tap_off for the noise tap.

Parameters:

  • s DLL state.

Returns:

The prompt tap's phase word.


function dll_update

Per-period code discriminator + loop update + NCO steer on the dumped accumulators (the coherent full-epoch path).

JM_FORCEINLINE  JM_HOT void dll_update (
    dp_dll_state_t * s
) 

Takes the powers of acc_e/acc_l/acc_p and hands them to dll_steer(). Call this at a period boundary (dll_accumulate() returned 1) after reading the prompt, then the caller resets the accumulators. The period wrap itself is NOT handled here it falls out of dll_accumulate()'s own NCO advance (which wraps mod 2^32 on its own). Inline.

Parameters:

  • s DLL state. Must be non-NULL.

function dp_dll_configure

Recompute the loop gains for a new (bn, zeta); keep the code state.

int dp_dll_configure (
    dp_dll_state_t * state,
    double bn,
    double zeta
) 

Re-derives the 2nd-order loop filter's proportional and integral gains for a new noise bandwidth and damping, leaving the tracked code phase, code rate and correlator accumulators untouched — retune the loop mid-run (e.g. narrow the bandwidth once pulled in) without dropping lock.

Parameters:

  • state DLL state. Must be non-NULL.
  • bn Loop noise bandwidth, normalised to the code-period rate.
  • zeta Damping factor (0.707 = critically damped).

Returns:

DP_OK; DP_ERR_INVALID, changing nothing, when (bn, zeta) is outside dp_loop_filter_params_ok() a NaN gave gains that never recover, and a negative bn with zeta >= 1 a zero denominator (doppler#2103).

>>> import numpy as np
>>> from doppler.track import Dll
>>> rng = np.random.default_rng(1)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)
>>> d = Dll(code=code, sps=2, bn=0.01)
>>> d.configure(bn=0.02, zeta=0.707)   # widen the bandwidth mid-run
>>> round(d.bn, 3)
0.02


function dp_dll_configure_lock

Tune the always-on code-lock detector to a target (pfa, n_looks).

int dp_dll_configure_lock (
    dp_dll_state_t * state,
    double pfa,
    size_t n_looks,
    double ref_snr_db
) 

The DLL carries a lock detector that reuses acquisition's non-coherent test statistic. Every emitted look (a partial in segments mode, or the full-epoch prompt when segments == 1) is also correlated at a random off-peak code phase — re-drawn each epoch and kept noise_guard chips clear of the prompt/early/late lobe — to give a signal-free CFAR noise sample (valid for a low-sidelobe code, e.g. Gold). The offset power feeds an EMA reference E|O|^2; the prompt powers of n_looks consecutive looks are summed into S = sum|P_k|^2, and the detector declares lock when

R = sqrt(2 * S / E|O|^2) > det_threshold_noncoherent(pfa, n_looks)

which under H0 has P(R > eta) = marcum_q(n_looks, 0, eta). Size n_looks with det_n_noncoh(snr, ...) for the operating C/N0.

The noise-reference EMA bandwidth is sized probabilistically via dp_det_ema_alpha(): the signal-free |O|^2 samples are exponential (0 dB estimator SNR per sample — a DC level in fluctuation of equal power), and ref_snr_db chooses the EMA output's estimator SNR (mean^2/variance). Passing 0 derives it from n_looks: the reference's relative std is held to an eighth of the statistic's intrinsic H0 spread (1/sqrt(N)), floored at ~33 dB — which reproduces the classic 1/alpha = max(1024, 32*N) sizing exactly, now as a consequence instead of a constant.

The detector needs an off-peak code phase to sample noise from: with a very short code (fewer than ~2*(spacing+2)+1 chips, i.e. sf <= 6 at the default spacing) no offset clears the prompt/early/late lobe, the noise tap aliases the prompt, and the statistic pins below threshold — locked stays 0 (fail-closed) no matter the signal. Use a code of >= 7 chips (real spreading codes are far longer) for a meaningful lock decision.

The decision itself runs through an embedded lock detector (lockdet_core.h) rather than a single-comparison latch: locked flips up only after det_verify_count(pfa, pfa*1e-3) CONSECUTIVE above-threshold decisions (the false-declare budget held three decades under the per-decision pfa — 2 straight for the default 1e-3), and drops only after 2 straight below-threshold decisions, so a statistic grazing the threshold cannot chatter the flag. Full control of the verify counts and a split declare/drop threshold pair is C-only via dp_dll_configure_lock_raw().

Parameters:

  • state DLL state. Must be non-NULL.
  • pfa Per-decision false-alarm probability, in (0, 1).
  • n_looks Non-coherent integration depth N (looks); clamped >= 1.
  • ref_snr_db Noise-reference estimator SNR in dB (> 0), or 0 to derive from n_looks as above.

Returns:

DP_OK, or DP_ERR_INVALID when pfa is outside (0, 1).

>>> import numpy as np
>>> from doppler.track import Dll
>>> d = Dll(code=np.zeros(31, dtype=np.uint8), sps=2)
>>> d.configure_lock(1e-3, 20)
>>> d.locked
False
>>> d.configure_lock(1e-3, 20, ref_snr_db=20.0)   # ~50-look reference
>>> d.configure_lock(2.0, 20)
Traceback (most recent call last):
    ...
ValueError: configure_lock failed (rc=-4)


function dp_dll_configure_lock_raw

Set the lock detector's raw geometry directly.

void dp_dll_configure_lock_raw (
    dp_dll_state_t * state,
    double up_thresh,
    double down_thresh,
    size_t n_looks,
    double alpha,
    uint32_t n_up,
    uint32_t n_down
) 

The escape hatch under dp_dll_configure_lock() for a composing C caller that derives its own threshold/EMA/hysteresis geometry — the full lockdet decision rule is exposed: a split declare/drop threshold pair (level hysteresis) and both verify counts (time hysteresis; size them with dp_det_verify_count()). Re-tuning clears the in-flight statistic and drops the lock so the next decision uses only looks gathered under the new config.

Parameters:

  • state DLL state. Must be non-NULL.
  • up_thresh Declare threshold on the statistic R (e.g. the CFAR eta from dp_det_threshold_noncoherent()).
  • down_thresh Drop threshold on R; choose <= up_thresh for level hysteresis.
  • n_looks Non-coherent integration depth N (looks); clamped >= 1.
  • alpha EMA coefficient for the noise reference, in (0, 1].
  • n_up Consecutive above-threshold decisions to declare lock; clamped to >= 1.
  • n_down Consecutive below-threshold decisions to drop it; clamped to >= 1.
    >>> import numpy as np
    >>> from doppler.track import Dll
    >>> rng = np.random.default_rng(1)
    >>> # >= 7 chips gives a usable lock statistic
    >>> code = rng.integers(0, 2, 63).astype(np.uint8)
    >>> chip = np.where(code & 1, -1.0, 1.0)
    >>> x = np.tile(np.repeat(chip, 4), 400).astype(np.complex64)
    >>> d = Dll(code, sps=4, bn=0.005)
    >>> # raw geometry: declare at R>3, drop at R<2.5, 8-look,
    >>> # 2-of-2 hysteresis
    >>> d.configure_lock_raw(3.0, 2.5, 8, 1.0 / 1024, 2, 2)
    >>> _ = d.steps(x)
    >>> d.locked                       # cleared the declare threshold
    True
    >>> bool(d.lock_stat > 3.0)
    True
    

function dp_dll_create

Create a code/timing delay-locked loop over a spreading code.

dp_dll_state_t * dp_dll_create (
    const uint8_t * code,
    size_t code_len,
    size_t sps,
    double init_chip,
    double bn,
    double zeta,
    double spacing,
    size_t segments
) 

An early/prompt/late DLL that tracks the code phase of a repeating spreading sequence (PN / Gold) on a carrier-wiped sample stream. Each code period it correlates the input against three replica taps — early (+spacing chips), prompt, late (-spacing chips) — runs the power-domain early-minus-late discriminator, filters it through a 2nd-order loop, and steers a fixed-point code-phase NCO. With segments == 1 it emits one prompt symbol per period (a coherent full-epoch integrate-and-dump); with segments > 1 it splits each epoch into that many partial correlations, emits them, and steers on the one-period window its look-back finds free of a data transition, summed coherently: robust to an asynchronous data-symbol clock. An always-on CFAR lock detector (see dp_dll_configure_lock) reports whether the loop is tracking; carrier-aiding (dp_dll_set_rate_aid) lets the code NCO ride a physically-coupled Doppler the discriminator alone cannot pull in at low SNR.

Parameters:

  • code Spreading code (0/1 chips), one period; copied internally.
  • code_len Code length (chips per period).
  • sps Samples per chip (default 2).
  • init_chip Seed code phase, chips (default 0.0).
  • bn Loop noise bandwidth (default 0.01).
  • zeta Damping factor (default 0.707).
  • spacing Early/late tap offset, chips (default 0.5).
  • segments Partial correlations per code epoch (default 1). 1 = a coherent full-epoch integrate-and-dump (one prompt/period). >1 splits each epoch into that many sub-epoch partials: it emits that many partial prompts/period and steers on the transition-free one-period window its look-back finds among them (robust to an asynchronous data-symbol clock). segments/epoch ~ samples/symbol at a downstream SymbolSync when the symbol rate is near the code rate, so choose >= 2 for symbol-timing recovery.

Returns:

Heap-allocated state, or NULL on allocation failure or an argument outside dp_dll_params_ok(): an empty code, segments 0, a non-finite init_chip, bn and zeta outside dp_loop_filter_params_ok(), or a spacing outside (0, code_len/2) (doppler#2103).

Note:

Caller must call dp_dll_destroy() when done.

>>> import numpy as np
>>> from doppler.track import Dll
>>> rng = np.random.default_rng(1)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)  # a 31-chip PN code
>>> chip = np.where(code & 1, -1.0, 1.0)    # BPSK spreading code
>>> x = np.tile(np.repeat(chip, 2), 60).astype(np.complex64)
>>> d = Dll(code=code, sps=2)               # 2 samples/chip loop
>>> sym = d.steps(x)                        # one prompt per period
>>> sym.shape                               # 60 despread symbols
(60,)
>>> round(float(np.mean(sym.real[-10:])), 1)  # despread to a clean +1
1.0
>>> round(d.code_rate, 3)                   # code NCO at nominal rate
1.0


function dp_dll_destroy

Destroy a DLL instance and release all memory (incl. the code copy).

void dp_dll_destroy (
    dp_dll_state_t * state
) 

Parameters:

  • state May be NULL.

function dp_dll_get_bn

double dp_dll_get_bn (
    const dp_dll_state_t * state
) 

function dp_dll_get_code_phase

double dp_dll_get_code_phase (
    const dp_dll_state_t * state
) 

function dp_dll_get_code_rate

double dp_dll_get_code_rate (
    const dp_dll_state_t * state
) 

function dp_dll_get_last_error

double dp_dll_get_last_error (
    const dp_dll_state_t * state
) 

function dp_dll_get_lock_stat

Last lock test statistic R (compare against the configured eta).

double dp_dll_get_lock_stat (
    const dp_dll_state_t * state
) 


function dp_dll_get_locked

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

int dp_dll_get_locked (
    const dp_dll_state_t * state
) 


function dp_dll_get_noise_est

Current CFAR noise-power estimate E|O|^2 (offset-tap EMA).

double dp_dll_get_noise_est (
    const dp_dll_state_t * state
) 


function dp_dll_get_segments

size_t dp_dll_get_segments (
    const dp_dll_state_t * state
) 

function dp_dll_get_state

void dp_dll_get_state (
    const dp_dll_state_t * state,
    void * blob
) 

function dp_dll_get_symbol_window

The lock detector's coherent window, in partials (0 when the symbol-period aid is off). Size n_looks from it.

size_t dp_dll_get_symbol_window (
    const dp_dll_state_t * state
) 

Parameters:

  • state Must be non-NULL.

function dp_dll_hold_here

Mark the point a coast returns to: this rate and this filter.

void dp_dll_hold_here (
    dp_dll_state_t * state
) 

Call while the loop is known to be on its signal; dp_dll_set_coast(1) restores the filter and the integrator's rate from here.

Parameters:

  • state The loop.

function dp_dll_init

Initialise a DLL in place (no allocation); BORROWS code .

void dp_dll_init (
    dp_dll_state_t * s,
    const uint8_t * code,
    size_t code_len,
    size_t sps,
    double init_chip,
    double bn,
    double zeta,
    double spacing
) 

The by-value counterpart to dp_dll_create(): a tracking channel that embeds a dp_dll_state_t initialises it here and retains ownership of code (it is not copied or freed). code must hold code_len chips for the loop's lifetime.

Unguarded, as the by-value path: the caller checks its arguments with dp_dll_params_ok() (one segment) first. Outside it the loop's gains go NaN or its taps read past the code (doppler#2103).

Parameters:

  • s State to initialise. Must be non-NULL.
  • code Spreading code (0/1 chips), one period; borrowed.
  • code_len Code length (chips per period); with the floats, inside dp_dll_params_ok().
  • sps Samples per chip.
  • init_chip Seed code phase, chips.
  • bn Loop noise bandwidth, normalised to the code-period rate.
  • zeta Damping factor (0.707 = critically damped).
  • spacing Early/late tap offset, chips (0.5 = half-chip).

function dp_dll_lock_epoch

Per-epoch lock-detector housekeeping: re-draw the noise offset.

void dp_dll_lock_epoch (
    dp_dll_state_t * s
) 

Call once per code epoch (after the period's dp_dll_lock_look()) so the next epoch's noise tap lands at a fresh random off-peak code phase.

Parameters:

  • s DLL state. Must be non-NULL.

function dp_dll_lock_look

Fold one look into the lock detector; clear the offset tap.

void dp_dll_lock_look (
    dp_dll_state_t * s,
    double norm
) 

Normalises the prompt and offset accumulators by norm (the number of samples integrated into them — one full period for a full-epoch composer), folds the offset power into the CFAR noise reference and the prompt power into the running N-look sum, and — at every n_looks-th look — forms the statistic R and steps the verify-counted lock detector. Call at each look boundary BEFORE zeroing the correlator accumulators (it reads acc_p and acc_o; acc_o is cleared here). Out of line: per-look rate, never hot.

Parameters:

  • s DLL state. Must be non-NULL.
  • norm Samples integrated into acc_p/acc_o this look (> 0).

function dp_dll_lookback_segments

Derive a principled segments count from a max tolerable async-lookback correlation-power loss, instead of hand-picking one.

size_t dp_dll_lookback_segments (
    size_t tsamps,
    double max_error_db
) 

Ports the coupled-despreader prototype's async_lookback_windows() verbatim (itself ported from ~/legacy-commz's asynchronous_correlation_loss): derives an ideal segment size from max_error_db, then snaps to the nearest exact divisor of tsamps so tsamps/segment_size is always an integer count (ties broken toward the smaller/earlier divisor, matching numpy.argmin's own first-occurrence convention). This project found hand-picked segments/windows values go stale silently (a WINDOWS=62 constant was carried for a long time implying an unexamined ~0.07-0.1dB loss tolerance, ~5-7x tighter than this formula's own validated 0.5dB default) — this function is the canonical, derived replacement.

Parameters:

  • tsamps One code period, in samples (code_len * sps).
  • max_error_db Maximum tolerable correlation-power loss from splitting the epoch into this many segments (dB, > 0); 0.5 is this project's own validated default.

Returns:

Segment count in [1, tsamps] that evenly divides tsamps (1 if tsamps == 0).


function dp_dll_params_ok

The DLL's one domain predicate: the arguments dp_dll_create() builds a loop from.

int dp_dll_params_ok (
    size_t code_len,
    double init_chip,
    double bn,
    double zeta,
    double spacing,
    size_t segments
) 

code_len >= 1 and segments >= 1; a finite init_chip (it is folded, so any finite value is a phase); bn and zeta inside dp_loop_filter_params_ok(); and 0 < spacing < code_len/2, where the early and late taps are distinct at 0 they coincide, at half the code they meet round the wrap, and a NaN is a phase word nothing can round to (it read 2^62 chips past the end of the code when a tap was a chip count).

dp_dll_create() refuses outside it. A composer that builds its DLL through an abort-on-NULL wrapper, or embeds one by value through dp_dll_init(), calls it first and refuses with its own error (doppler#2103).

Returns:

1 inside the domain, 0 outside it (a NaN is outside).


function dp_dll_reset

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

void dp_dll_reset (
    dp_dll_state_t * state
) 

Restores the code phase, loop filter, correlator accumulators and lock detector to their post-construction state while preserving the tuned configuration (bn/zeta, spacing, segments, lock geometry). Re-running the same input after a reset therefore reproduces the same tracked state bit-for-bit — the basis of a deterministic replay.

Parameters:

  • state Must be non-NULL.
    >>> import numpy as np
    >>> from doppler.track import Dll
    >>> rng = np.random.default_rng(21)
    >>> code = rng.integers(0, 2, 63).astype(np.uint8)
    >>> idx = (np.arange(63 * 4 * 300) * (1 + 3e-4) / 4).astype(
    ...     np.int64) % 63
    >>> x = np.where(code[idx] & 1, -1.0, 1.0).astype(np.complex64)
    >>> d = Dll(code, sps=4, bn=0.005)
    >>> _ = d.steps(x)
    >>> first = round(d.code_rate, 6)
    >>> d.reset()                     # back to the create-time code phase
    >>> _ = d.steps(x)                # same input -> same tracked rate
    >>> round(d.code_rate, 6) == first
    True
    

function dp_dll_set_bn

Set the loop bandwidth, keeping zeta.

int dp_dll_set_bn (
    dp_dll_state_t * state,
    double val
) 

Returns:

As dp_dll_configure(): DP_ERR_INVALID, changing nothing, outside the loop filter's domain.


function dp_dll_set_coast

Hold the loop (1) or run it (0, the default).

void dp_dll_set_coast (
    dp_dll_state_t * state,
    int coast
) 

Coasting, the loop filter takes no update and the NCO is not steered: on entry the filter is restored to the last dp_dll_hold_here() and the rate to that filter's integrator the loop's frequency memory, without the proportional term of a steer, which is a phase correction for one interval and held as a rate walks the phase off at chips per second and the code phase then advances at that rate, the carrier aid as the caller leaves it, while the lock detector keeps looking at the prompt so a signal that returns at that phase is seen. The caller marks the hold point while it knows the loop is genuinely on its signal (the receiver: both of its flags up), so a flag that blips on a passing neighbour does not move it. The reason it exists: a loop left running on noise after its emitter leaves free-runs at whatever the filter holds, sweeps its phase through every other emitter's, and can capture one whose code phase crosses its own slowly enough measured in the pool's ten-minute soak, a departed receiver followed a neighbour's chips at 44.8 chips per second and its code flag returned sixteen times in fourteen seconds (doppler#1271). The caller decides when: async_dsss_receiver coasts while both of its lock flags are down after a lock it once had.

Parameters:

  • state The loop.
  • coast 1 to hold, 0 to run.

function dp_dll_set_code_phase

Set the prompt code phase, in chips: the correction a holder applies to a coasting loop.

void dp_dll_set_code_phase (
    dp_dll_state_t * state,
    double chips
) 

Moves the code NCO to chips (modulo the code length): the loop filter, the rate aid, the lock detector and the symbol-period aid keep their state. Within a period the accumulators of the period in progress finish on the new phase. Across the period wrap where a holder fed whole periods sits the move is taken the short way round and the epoch follows it: a put forward across the wrap closes the epoch in progress on the next sample (the wrap it was waiting for has happened), and a put back across it closes nothing until the next wrap (those samples are the tail of an epoch already closed) and starts the epoch there. Either way a put never emits a burst of short partials and never folds a second period into one epoch. This is the other half of dp_dll_set_coast(): a coasting loop advances at its held rate, which its 32-bit NCO quantises to a few parts in 10^7 about 0.06 chip per 31 ms block at 5 Mcps (design §12.22) so whoever holds it on another clock (a searcher's cell, a carrier aid) puts it back where that clock says, once per block, and reads the discriminator between. Nominally at a period boundary; called mid-period it costs that one period's read.

Parameters:

  • state DLL state. Must be non-NULL.
  • chips The prompt's code phase, chips; folded into [0, code_len).
    >>> import numpy as np
    >>> from doppler.track import Dll
    >>> rng = np.random.default_rng(3)
    >>> code = rng.integers(0, 2, 63).astype(np.uint8)
    >>> d = Dll(code, sps=4, init_chip=0.0, bn=0.01)
    >>> d.set_code_phase(5.25)
    >>> round(d.code_phase, 2)
    5.25
    >>> d.set_code_phase(63.0 + 1.5)      # folded into the period
    >>> round(d.code_phase, 2)
    1.5
    

function dp_dll_set_lock_verify

Set the lock detector's verify counts, keeping its thresholds.

int dp_dll_set_lock_verify (
    dp_dll_state_t * state,
    uint32_t n_up,
    uint32_t n_down
) 

dp_dll_configure_lock() derives the declare count from pfa and fixes the drop count at 2. A caller that has sized n_looks for a target Pd knows the per-decision miss probability 1 - pd, and det_verify_count(1 - pd, budget) is the drop count that holds the false-drop rate under a budget three consecutive misses for pd = 0.99 at 1e-6 per decision. This sets both counts without touching the thresholds or the noise reference; the running verify counter and the flag restart.

Parameters:

  • state DLL state. Must be non-NULL.
  • n_up Consecutive above-threshold decisions to declare (>= 1).
  • n_down Consecutive below-threshold decisions to drop (>= 1).

Returns:

DP_OK, or DP_ERR_INVALID when either count is 0.

>>> import numpy as np
>>> from doppler.track import Dll
>>> from doppler.detection import det_verify_count
>>> d = Dll(np.zeros(31, dtype=np.uint8), sps=2, segments=4)
>>> d.set_lock_verify(2, det_verify_count(0.01, 1e-6))
>>> d.locked
False


function dp_dll_set_rate_aid

Set the carrier-aiding code-rate deviation (ratio; 0 = off).

void dp_dll_set_rate_aid (
    dp_dll_state_t * state,
    double rate_aid
) 

A fixed fractional rate bias summed into the sample-and-hold phase_inc on top of the loop's own control every epoch for physically-coupled Doppler, carrier_offset_hz / carrier_freq_hz, so the code NCO rides the code-rate dilation the discriminator alone can't pull in at low SNR. Applied continuously across the epoch (via phase_inc), not as a phase pulse. Also nudges the current phase_inc so the aid takes effect before the first period update. code_rate stays the loop's own observable and is unaffected. A HELD loop (dp_dll_set_coast()) takes the new aid at once: nothing steers a coasting loop's phase_inc, so it is recomputed here from the held filter and the new aid a holder that refreshes the Doppler it holds (a searcher-timed receiver's fold) sees the code rate follow. Before this, a coasting loop kept the aid it was held with.

Parameters:

  • state DLL state. Must be non-NULL.
  • rate_aid Fractional code-rate deviation (e.g. 8e-6). 0 disables.
    >>> import numpy as np
    >>> from doppler.track import Dll
    >>> rng = np.random.default_rng(11)
    >>> code = rng.integers(0, 2, 63).astype(np.uint8)
    >>> delta = 5e-4                                   # code-rate Doppler
    >>> idx = (np.arange(63 * 4 * 300) * (1 + delta) / 4).astype(
    ...     np.int64) % 63
    >>> x = np.where(code[idx] & 1, -1.0, 1.0).astype(np.complex64)
    >>> plain = Dll(code, sps=4, bn=0.005)
    >>> _ = plain.steps(x)
    >>> round(plain.code_rate, 4)      # loop had to pull the whole Doppler
    1.0005
    >>> aided = Dll(code, sps=4, bn=0.005)
    >>> aided.set_rate_aid(delta)      # feed the Doppler forward instead
    >>> _ = aided.steps(x)
    >>> round(aided.code_rate, 4)      # loop integrator stays at nominal
    1.0
    

function dp_dll_set_state

Restore from blob .

int dp_dll_set_state (
    dp_dll_state_t * state,
    const void * blob
) 

Returns:

DP_OK or DP_ERR_INVALID.

The blob is decoded and checked whole before any of state is written, so a refusal changes nothing (doppler#2092). Refused: a wrong-object, wrong-version, wrong-size or foreign-endian envelope; a blob whose code length, samples per chip, segment count or symbol-aid geometry (on or off, its ring and hypotheses) is not this instance's; and running fields no live instance carries a chunk index past the segments, a noise offset outside the code, a non-finite or negative noise guard, and on an aided instance a non-finite period, a window as long as its ring, a best hypothesis past its hypotheses or a last look past the partials counted. The code, the buffers and the telemetry attachment stay this instance's.


function dp_dll_set_symbol_period

Give the loop the data-symbol period, so the lock detector's looks and the code discriminator's windows are coherent over a symbol instead of a quarter-epoch partial.

int dp_dll_set_symbol_period (
    dp_dll_state_t * state,
    double partials_per_symbol
) 

In segments > 1 mode every partial is a look for the code-lock detector (dp_dll_configure_lock()) and the discriminator sees one epoch through the per-epoch look-back: the smallest integrations the asynchronous data allows when nothing is known about where its transitions fall, and therefore the weakest. This is the same max-power search the per-epoch look-back already runs, lifted to the symbol scale once the symbol PERIOD is known: with P = partials_per_symbol the transitions recur every P partials, so ceil(P) boundary-phase hypotheses each define a transition-free window of L = min(floor(P) - 1, 4 * segments) partials per symbol. Each hypothesis accumulates the power of its coherently summed windows (an EMA over the last ~32 symbols); the one with the most power IS the symbol timing, and its windows become the detector's looks and the discriminator's windows. A look then integrates L partials coherently at 1.81 epochs per symbol (1,023 chips at 5 Mcps, 2,700 sym/s) and four partials per epoch, six partials instead of one, 7.8 dB more per look; at SPEC's 1.11 epochs, three and 4.8 dB and never straddles a transition. Size n_looks for it with detection.det_n_noncoh() over L * (sf * sps / segments) samples.

The code loop steers once per symbol, on the early/prompt/late sums over the winning window, instead of once per epoch on the look-back's: the same discriminator on a window half again as long and never across a transition. The loop filter is re-timed to the symbol interval, so bn keeps its per-epoch meaning and the tracked rate is continuous across the switch either way a loop that has pulled in a code Doppler keeps it when the aid is turned on or off. Measured against the per-epoch loop at the operating point (docs/design/async-dsss-receiver-measurements.md §12.5, validate_dll_aid_jitter): pull-in about 20% faster, and a code jitter 0.8x the per-epoch loop's above 45 dB-Hz where the look-back's own handling of the data transitions sets it and 1.2-1.4x at the 40 dB-Hz floor, where the noise sets it and the window's unused partials cost more than its coherence buys; hundredths of a chip either way. The emitted partial stream is untouched by the aid. The search is blind to WHICH hypothesis is right on any one symbol it needs no decision and no external timing so it costs nothing at cold start and follows a slowly drifting symbol clock by itself. L is capped at four epochs of partials so a long symbol (a low data rate) does not ask for coherence across more carrier than the wipe-off holds.

Parameters:

  • state DLL state. Must be non-NULL.
  • partials_per_symbol Data-symbol period in emitted partials, segments * chip_rate / (sf * symbol_rate); >= 2. 0 disables (per-partial looks again).

Returns:

DP_OK; DP_ERR_INVALID when segments <= 1, or the period is not finite, is negative, is in (0, 2) or is past 2^20 partials, or when the loop's bandwidth is outside the domain at the period's interval or at t = 1 (the gains would overflow) the rings are sized by the period, and past that they are refused, not allocated (doppler#2103). Exactly 0 turns the aid off; a negative period is refused, not taken as 0.

>>> import numpy as np
>>> from doppler.track import Dll
>>> code = (np.arange(63) * 7 % 2).astype(np.uint8)
>>> d = Dll(code, sps=2, segments=4)
>>> d.set_symbol_period(7.24)      # 1.81 epochs per symbol, 4 partials/epoch
>>> d.symbol_window                # coherent partials per look
6
>>> d.set_symbol_period(0.0)       # back to per-partial looks
>>> d.symbol_window
0


function dp_dll_set_telemetry

Attach (or detach) a telemetry context and register the code loop's probes on it. Registers five probes, emitted once per code epoch (period) and further thinned by decim: "<prefix>.e" (the early-minus-late envelope discriminator — the loop stress), "<prefix>.rate" (the tracked code rate, chips advanced per nominal chip, ~1.0 at lock — dp_dll_get_code_rate() , the loop's own rate, without the carrier aid; a held loop reports its held integrator's), "<prefix>.lock" (the CFAR lock statistic R; compare against the configured threshold), "<prefix>.locked" (the verify-counted lock decision, 0/1 — the lockdet output, so a consumer sees where the declare/drop rule fired without re-deriving it from the statistic) and "<prefix>.nco" (what the code NCO is commanded: the complete loop-filter output, the integrator, the proportional term and the carrier aid, as chips per nominal chip, at the NCO's resolution of one phase-increment step; a held loop's held command). 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 dp_dll_set_telemetry (
    dp_dll_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. "code" or "ch0.code".
  • decim Emit every decim-th epoch; >= 1.

Returns:

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

>>> import numpy as np
>>> from doppler.track import Dll
>>> from doppler.telemetry import Telemetry
>>> tlm = Telemetry(1 << 12)
>>> code = np.zeros(31, dtype=np.uint8)
>>> d = Dll(code=code, sps=2)
>>> d.set_telemetry(tlm, "code")
>>> sorted(tlm.probe_names)
['code.e', 'code.lock', 'code.locked', 'code.nco', 'code.rate']
>>> x = np.ones(31 * 2 * 50, dtype=np.complex64)
>>> _ = d.steps(x)
>>> recs = tlm.read()   # five records per code epoch
>>> len(recs) > 0 and len(recs) % 5 == 0
True


function dp_dll_state_bytes

size_t dp_dll_state_bytes (
    const dp_dll_state_t * state
) 

function dp_dll_steps

Correlate a carrier-wiped block against the local code and steer the code NCO once per code period.

size_t dp_dll_steps (
    dp_dll_state_t * state,
    const float _Complex * x,
    size_t x_len,
    float _Complex * out,
    size_t max_out
) 

The Python face of the loop. Each code period the early/prompt/late correlators dump, the power-domain early-minus-late discriminator runs, and the fixed-point code-phase NCO is re-steered; the prompt correlator value is emitted as one output symbol per period (or segments partial prompts per period when segments > 1). At segments > 1 each emitted partial is its window's prompt sum over the samples it spans, divided by a slowly tracked level a running mean of the period's look-back magnitude over DLL_OUT_LEVEL_PERIODS (32) periods so the output sits near unit amplitude at any input gain, which the timing detectors downstream assume. Never by the period's own magnitude: that divisor carries the period's own noise, and it made the despread error non-circular and cost the error rate (doppler#2495). The level settles to 1% 146 periods after a step in the input's level or a put of the code phase, and the output's scale follows the step until then; over the first periods after create or reset the level is their plain mean, so it is not yet slow. The discriminator's early, late and prompt are coherent sums over its window one code period, or with dp_dll_set_symbol_period() the transition-free window of a symbol and it takes their powers. So the loop is blind to the carrier's phase but not to its frequency: a residual df costs a window of T seconds sinc^2(df * T) of its power, and a whole 1/T cycle nulls it. Wipe the carrier before this call: AsyncDsssReceiver runs its carrier loop upstream of its Dll and hands the same estimate to dp_dll_set_rate_aid(). Symbol-timing recovery runs downstream, on this output. Returned blocks are block-size invariant and safe to keep across calls (a block still referenced is never overwritten, jm gh-437).

Parameters:

  • state DLL state. Must be non-NULL.
  • x Carrier-wiped input samples (one contiguous block).
  • x_len Number of input samples.
  • out Output buffer for the emitted prompt symbols.
  • max_out Capacity of out in elements; emission stops there.

Returns:

Number of prompt symbols written — one per completed code period (segments per period when segments > 1) — up to max_out.

>>> import numpy as np
>>> from doppler.track import Dll
>>> rng = np.random.default_rng(1)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)
>>> chip = np.where(code & 1, -1.0, 1.0)    # BPSK spreading code
>>> x = np.tile(np.repeat(chip, 2), 40).astype(np.complex64)
>>> d = Dll(code=code, sps=2)
>>> sym = d.steps(x)                        # one prompt per period
>>> sym.dtype
dtype('complex64')
>>> round(float(np.mean(sym.real[-10:])), 1)  # despread to a clean +1
1.0
>>> round(d.code_rate, 3)                   # locked at nominal rate
1.0


function dp_dll_steps_max_out

size_t dp_dll_steps_max_out (
    dp_dll_state_t * state
) 

function dp_dll_symbol_period_ok

Whether dp_dll_set_symbol_period() takespartials_per_symbol as a period to turn the symbol aid ON.

int dp_dll_symbol_period_ok (
    size_t segments,
    double partials_per_symbol
) 

segments > 1, and a finite period from 2 up to 2^20 partials: the rings and hypotheses are allocated per period, and past 2^20 they are refused rather than sized. A composer that derives a period from its configuration checks it here at create, so the period it sets mid-stream is never refused (doppler#2103).

This predicate covers the period alone. dp_dll_set_symbol_period() also refuses a bandwidth whose loop gains overflow at the period's interval (the bandwidth domain, checked at that interval and at t = 1), so a composer that must never see a mid-stream refusal checks the gains with dp_loop_filter_params_ok() at partials_per_symbol / segments as well.

Returns:

1 if the period turns the aid on, 0 otherwise.


function dp_dll_take_error

Take the discriminator's running sum: the steers since the last take, their sum, both zeroed.

size_t dp_dll_take_error (
    dp_dll_state_t * state,
    double * sum
) 

Every steer adds its clamped discriminator to a running sum, coasting or not. A holder correcting a coasting loop on another clock (a searcher-timed receiver, design §12.22-12.24) reads the sum once per interval, divides by the count for the block mean, moves the loop by a gain times it (dp_dll_set_code_phase()), and the next interval starts from zero. The count is the number of steers one per epoch on the coherent full-epoch path, one per symbol window on the symbol-aided partial path so the mean is over the same updates the loop itself would have filtered. Zero steers leaves *sum at 0 and returns 0.

Parameters:

  • state DLL state. Must be non-NULL.
  • sum Written with the sum of the steers taken (non-NULL).

Returns:

The number of steers taken.

>>> import numpy as np
>>> from doppler.track import Dll
>>> rng = np.random.default_rng(3)
>>> code = rng.integers(0, 2, 63).astype(np.uint8)
>>> idx = (np.arange(63 * 4 * 200) // 4) % 63
>>> x = np.where(code[idx] & 1, -1.0, 1.0).astype(np.complex64)
>>> d = Dll(code, sps=4, init_chip=0.15, bn=0.005)   # 0.15 chip off
>>> _ = d.steps(x)                        # 200 epochs: the loop pulls in
>>> m = d.take_error_mean()               # the 200 steers' mean
>>> 0.0 < abs(m) < 0.5                    # the pull-in's transient
True
>>> import math
>>> math.isnan(d.take_error_mean())       # taken: nothing left
True


function dp_dll_take_error_mean

dp_dll_take_error() as one number: the mean of the steers taken, or NaN when none were the Python face of the primitive.

double dp_dll_take_error_mean (
    dp_dll_state_t * state
) 

The block-mean discriminator a holder corrects a coasting loop on (dp_dll_set_code_phase()), read once per interval; each read starts the next interval's sum from zero.

Parameters:

  • state DLL state. Must be non-NULL.

Returns:

The mean of the steers since the last take; NaN when there were none.

>>> import numpy as np
>>> from doppler.track import Dll
>>> rng = np.random.default_rng(3)
>>> code = rng.integers(0, 2, 63).astype(np.uint8)
>>> idx = (np.arange(63 * 4 * 200) // 4) % 63
>>> x = np.where(code[idx] & 1, -1.0, 1.0).astype(np.complex64)
>>> d = Dll(code, sps=4, init_chip=0.15, bn=0.005)   # 0.15 chip off
>>> _ = d.steps(x)                        # 200 epochs: the loop pulls in
>>> m = d.take_error_mean()               # the 200 steers' mean
>>> 0.0 < abs(m) < 0.5                    # the pull-in's transient
True
>>> import math
>>> math.isnan(d.take_error_mean())       # taken: nothing left
True


function dp_dll_tlm_flush

Emit the code loop's telemetry records for the epoch just closed.

void dp_dll_tlm_flush (
    const dp_dll_state_t * s
) 

Out-of-line on purpose: the emit machinery must not inline into the per-sample correlator 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 and call this once per code-epoch update. Records "<prefix>.e" (the E-L envelope discriminator — the loop stress), "<prefix>.rate" (the tracked code rate, chips per nominal chip), "<prefix>.lock" (the CFAR lock statistic R, refreshed every n_looks looks) and "<prefix>.locked" (the verify-counted lockdet decision, 0/1 — plotted against .lock it shows exactly where the declare/drop rule fired). A composing tracking channel (the DSSS despreader) calls this from its own per-epoch update.

Parameters:

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

Macro Definition Documentation

define DLL_AID_MAX_PERIOD

The longest symbol period, in partials, the symbol aid takes.

#define DLL_AID_MAX_PERIOD `1048576.0`

Its rings and hypotheses are allocated per period, so a period past this is refused rather than sized: at 2^20 the rings are already 128 MiB, and a NaN or an absurd period used to reach dp_xcalloc with 2^63 and abort the process (doppler#2103). No link comes near it: 2^20 partials is 16384 code epochs a symbol even at 64 segments. A composer that builds a chain from a symbol period bounds it here too (AsyncDsssReceiver).


define DLL_DISC_CLAMP

#define DLL_DISC_CLAMP `1.0`

define DLL_EPS

#define DLL_EPS `1e-12`

define DLL_OUT_LEVEL_PERIODS

#define DLL_OUT_LEVEL_PERIODS `32u`

define DLL_STATE_MAGIC

#define DLL_STATE_MAGIC `DP_FOURCC ('D','L','L',' ')`

define DLL_STATE_VERSION

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


The documentation for this class was generated from the following file native/inc/doppler/dll/dll_core.h