Skip to content

File dll_core.h

File List > dll > dll_core.h

Go to the documentation of this file

#ifndef DP_DLL_CORE_H
#define DP_DLL_CORE_H

#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"
#ifdef __cplusplus
extern "C" {
#endif

/* Numerical guard on the early+late envelope sum (not tunable). */
#define DLL_EPS 1e-12

/* The `segments > 1` output's level: the code periods its running mean of
 * the look-back magnitude spans (doppler#2495). Each period's partials are
 * divided by that slowly tracked level, so the output sits near unit
 * amplitude at any input gain -- the level every timing detector
 * downstream assumes, which MpskReceiver's AGC, 20x slower than its own
 * loops, cannot set from scratch -- and never by the period's own
 * magnitude, whose noise is the period's own. The number is a choice
 * inside a band, both ends set by a measurement or a count:
 *   - slow enough: the current period enters the level with weight 1/N,
 *     and the in-phase error is compressed against the quadrature as that
 *     weight grows. Measured by test_dll_core.c's section 11 harness (a
 *     period SNR of 3 dB, 20,000 periods), the compression is -2.29 dB at
 *     N = 1 (the period's own magnitude, which cost the receiver 0.2 dB of
 *     error rate), -1.18 at 2, -0.54 at 4, -0.23 at 8, -0.08 at 16 and
 *     -0.01 at 32: about halving with each doubling. Holding it under
 *     0.15 dB, about the counting noise of that measurement, needs
 *     N >= 16.
 *   - fast enough: after a step in level the error decays as (1 - 1/N)^n,
 *     to 1% at n = ln 100 / -ln(1 - 1/N), about 4.6 N periods. Settling
 *     inside a third of the 185 ms data-free block (555 periods at SPEC's
 *     3.069 Mcps), and far ahead of MpskReceiver's AGC, needs N <= 40.
 * N = 32 is inside the band 16..40, a power of two; the 1% settle is then
 * 146 periods, 49 ms at SPEC. Until N periods have accrued the level is
 * their plain mean, so the first output already carries a level, but not
 * a slow one: the first period is divided by its own magnitude, and the
 * current period weighs 1/n, so the compression the band bounds holds
 * only once about 16 periods have accrued, 5 ms at SPEC. */
#define DLL_OUT_LEVEL_PERIODS 32u

/* Clamp on the discriminator output |e| before it reaches the loop filter.
 * The old magnitude-domain discriminator (|E|-|L|)/(|E|+|L|) was always
 * inherently bounded in [-1, 1] (|E|-|L| <= |E|+|L| identically); the
 * power-domain 0.5*(Ep-Lp)/Pp form is NOT -- a data transition landing
 * badly enough to collapse the prompt power Pp near zero (the lookback
 * failing to find a clean candidate that epoch, a real, reachable case,
 * not a hypothetical one -- observed directly while porting this design)
 * can blow e up arbitrarily, injecting a huge phase nudge that cascades
 * into a runaway. Clamping restores the old design's inherent safety
 * property without changing behaviour in the overwhelming common case
 * (|e| well under 1 at any reasonable lock). */
#define DLL_DISC_CLAMP 1.0

typedef struct {
    dp_tlm_t *ctx;     
    int32_t id_e;      
    int32_t id_rate;   
    int32_t id_lock;   
    int32_t id_locked; 
    int32_t id_nco;    
} dll_tlm_t;

typedef struct {
    dp_loop_filter_state_t lf;  
    dp_nco_state_t code_nco;    
    const uint8_t *code;     
    size_t sf;               
    size_t sps;              
    double inv_sps;          
    double inv_tsamps;       
    double inv_tsamps2;      
    double inv_tsamps_sf;    
    double inv_upd;          
    double spacing;          
    double chip_pos;         
    double code_rate;        
    double rate_aid;         
    int coast;               
    uint32_t held_inc;       
    dp_loop_filter_state_t held_lf; 
    /* The steer's gain table: how the loop filter's integrator and
       proportional term reach phase_inc (cycles per sample) and code_rate
       (a ratio), set once by segments in set_segments(). ONE steer,
       dll_steer(), applies it on both paths; the two tables record the
       measured, separately pinned gains each path shipped with (the
       coherent full-epoch loop treats the integrator as a rate ratio,
       the partial-correlation loop treats the filter's output as chips
       over sps) -- reconciling them is a certified-behaviour change,
       tracked, not folded in here. */
    double ctrl_i;           
    double ctrl_p;           
    double rate_i;           
    double rate_p;           
    double   err_sum;        
    uint64_t err_n;          
    double seed_chip;        
    double bn;               
    double zeta;             
    float _Complex acc_e;     
    float _Complex acc_p;     
    float _Complex acc_l;     
    double last_error;       
    size_t segments;         
    double seg_chips;        
    double seg_norm;         
    size_t seg_idx;          
    int wrap_pending;        
    int wrap_await;          
    /* ── segments>1 chunked output + one-epoch-deep lookback (heap-owned,
     *    length `segments`; NULL when segments==1 -- dp_dll_init()'s embedded/
     *    borrowed path is always segments==1, so this never needs a
     *    deinit contract there, same lifecycle class as `code`/owns_code).
     *    This is the direct C port of the coupled-despreader
     *    prototype's `find_max_power()`/`get_window()` (also
     *    `docs/design/async-dsss-receiver.md` §3.6's own reference
     *    pseudocode) -- see dll_steps_impl()'s segments>1 branch, which
     *    builds the SAME named artifacts (`sums`, `backward_sums`,
     *    `correlations`) in the same order so it can be checked directly
     *    against that Python function line for line. */
    float _Complex *chunk_p;         
    float _Complex *chunk_e;         
    float _Complex *chunk_l;         
    float _Complex *sums;            
    float _Complex *last_backward_p; 
    float _Complex *last_e;          
    float _Complex *last_l;          
    int have_prev_epoch;     
    uint32_t out_level_n;    
    double   out_level;      
    /* ── lock detector (always on): offset-tap CFAR noise ref + N-look test  */
    float _Complex acc_o;     
    double off_chips;        
    /* The taps' offsets as PHASE WORDS (fractions of one period in 32 bits,
       the code NCO's unit), DERIVED from sf, spacing and off_chips by
       dll_taps_refresh(), from draw_offset() (which init, create and reset
       reach by seed(), after configure_geometry() sets sf and spacing) and
       from set_state -- so the per-sample taps add words and never divide
       (dll_tap_base(), doppler#2397). */
    uint32_t tap_grid;       
    uint32_t tap_spacing;    
    uint32_t tap_off;        
    double noise_guard;      
    uint32_t rng;            
    double noise_ema;        
    double lock_alpha;       
    double lock_sum;         
    size_t lock_count;       
    size_t n_looks;          
    double lock_stat;        
    size_t lock_nz;          
    dp_lockdet_state_t lock;    
    /* ── symbol-timing aid (segments>1 only; period 0 = off).
     *    The same max-power search the per-epoch look-back runs, lifted to
     *    the SYMBOL scale: with the data-symbol period known in partials,
     *    `aid_nhyp = ceil(period)` boundary-phase hypotheses each own a
     *    coherent window of `aid_len` partials per symbol; the hypothesis
     *    whose windows carry the most power (an EMA over symbols) is the
     *    symbol timing, and ITS windows are both the lock detector's looks
     *    and the code discriminator's windows -- coherent over most of a
     *    symbol instead of a quarter-epoch partial, and the loop steers
     *    once per symbol on them (the filter re-timed to that interval,
     *    `inv_upd`). See dp_dll_set_symbol_period(). Rings are heap-owned,
     *    sized at set time, packed field-wise in the state blob like the
     *    chunk buffers. */
    double  sym_period;      
    size_t  aid_len;         
    size_t  aid_nhyp;        
    size_t  aid_ring;        
    size_t  aid_best;        
    uint64_t aid_count;      
    uint64_t aid_last_end;   
    double  aid_alpha;       
    float _Complex *aid_ring_p; 
    float _Complex *aid_ring_o; 
    float _Complex *aid_ring_e; 
    float _Complex *aid_ring_l; 
    double        *aid_power;  
    int owns_code;           
    dll_tlm_t tlm;           
} dp_dll_state_t;

JM_FORCEINLINE float
dll_chip_sign(uint8_t c)
{
    return (c & 1u) ? -1.0f : 1.0f;
}

JM_FORCEINLINE float
dll_replica_word(const dp_dll_state_t *s, uint32_t w)
{
    uint32_t     frac;
    const size_t i  = nco_word_to_grid(w, (uint32_t)(2u * s->sf), &frac);
    size_t       c1 = (i + 1) >> 1; /* the next point's chip */
    if (c1 == s->sf)
        c1 = 0;
    const float mu = nco_frac_to_weight(frac);
    const float v0 = dll_chip_sign(s->code[i >> 1]);
    const float v1 = dll_chip_sign(s->code[c1]);
    return v0 + mu * (v1 - v0);
}

/* Cycles -> u32 phase delta: nco_norm_freq_to_inc() (native/inc/doppler/nco/nco_core.h)
 * is the ONE shared primitive for this conversion -- do not grow a
 * private copy here (a prior copy of this exact formula existed under
 * the name dll_cycles_to_phase_delta() and has been consolidated away;
 * see nco_norm_freq_to_inc()'s own doc comment for why duplicates of this
 * conversion keep drifting). */

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);

JM_FORCEINLINE uint32_t
dll_tap_base(const dp_dll_state_t *s)
{
    return nco_dwell_center(&s->code_nco) - s->tap_grid;
}

JM_FORCEINLINE JM_HOT int
dll_accumulate(dp_dll_state_t *s, float _Complex d)
{
    const uint32_t p = dll_tap_base(s);
    s->acc_p += d * dll_replica_word(s, p);
    s->acc_e += d * dll_replica_word(s, p + s->tap_spacing);
    s->acc_l += d * dll_replica_word(s, p - s->tap_spacing);
    /* nco_step_u32_ovf() (native/inc/doppler/nco/nco_core.h) is the ONE shared
       primitive for "advance one sample, report whether it wrapped" --
       this used to be a private inline reimplementation of exactly that. */
    double sfd = (double)s->sf;
    uint8_t carry;
    (void)nco_step_u32_ovf(&s->code_nco, &carry);
    s->chip_pos = ((double)s->code_nco.phase / 4294967296.0) * sfd;
    return carry;
}

JM_FORCEINLINE JM_HOT void
dll_lock_accumulate(dp_dll_state_t *s, float _Complex d)
{
    s->acc_o += d * dll_replica_word(s, dll_tap_base(s) + s->tap_off);
}

void dp_dll_lock_look(dp_dll_state_t *s, double norm);

void dp_dll_lock_epoch(dp_dll_state_t *s);

JM_FORCEINLINE JM_HOT void
dll_steer(dp_dll_state_t *s, double ep, double lp, double pp)
{
    double e = 0.5 * (ep - lp) / (pp + DLL_EPS);
    if (e > DLL_DISC_CLAMP)
        e = DLL_DISC_CLAMP;
    else if (e < -DLL_DISC_CLAMP)
        e = -DLL_DISC_CLAMP;
    s->last_error = e;
    s->err_sum += e;
    s->err_n++;
    if (s->coast)
        return; /* held: the discriminator read (last_error, the probe, the
                   sum) but not filtered, phase_inc as it stands
                   (dp_dll_set_coast) -- a holder coasting on another clock
                   reads where the signal sits against the held phase, which
                   is what it corrects on */
    (void)dp_loop_filter_step(&s->lf, e);
    double integ = s->lf.integ;
    double pe    = s->lf.kp * e;
    double u     = s->inv_upd;
    double ctrl  = u * (s->ctrl_i * integ + s->ctrl_p * pe);
    s->code_rate = 1.0 + u * (s->rate_i * integ + s->rate_p * pe);
    s->code_nco.phase_inc
        = nco_norm_freq_to_inc(s->inv_tsamps * (1.0 + s->rate_aid) + ctrl);
}

JM_FORCEINLINE JM_HOT void
dll_update(dp_dll_state_t *s)
{
    float me = cabsf(s->acc_e), ml = cabsf(s->acc_l), mp = cabsf(s->acc_p);
    dll_steer(s, (double)me * me, (double)ml * ml, (double)mp * mp);
}

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);

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

#define DLL_AID_MAX_PERIOD 1048576.0

int dp_dll_symbol_period_ok(size_t segments, double partials_per_symbol);

size_t dp_dll_lookback_segments(size_t tsamps, double max_error_db);

void dp_dll_destroy(dp_dll_state_t *state);

void dp_dll_reset(dp_dll_state_t *state);

size_t dp_dll_steps_max_out(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);

int dp_dll_configure(dp_dll_state_t *state, double bn, double zeta);
double dp_dll_get_bn(const dp_dll_state_t *state);
int dp_dll_set_bn(dp_dll_state_t *state, double val);

void dp_dll_set_rate_aid(dp_dll_state_t *state, double rate_aid);

void dp_dll_set_coast(dp_dll_state_t *state, int coast);

void dp_dll_hold_here(dp_dll_state_t *state);

int dp_dll_set_symbol_period(dp_dll_state_t *state, double partials_per_symbol);

size_t dp_dll_get_symbol_window(const dp_dll_state_t *state);

int dp_dll_set_lock_verify(dp_dll_state_t *state, uint32_t n_up, uint32_t n_down);
double dp_dll_get_code_phase(const dp_dll_state_t *state);

void dp_dll_set_code_phase(dp_dll_state_t *state, double chips);
double dp_dll_get_code_rate(const dp_dll_state_t *state);

size_t dp_dll_take_error(dp_dll_state_t *state, double *sum);

double dp_dll_take_error_mean(dp_dll_state_t *state);
double dp_dll_get_last_error(const dp_dll_state_t *state);
size_t dp_dll_get_segments(const dp_dll_state_t *state);

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

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);

int dp_dll_get_locked(const dp_dll_state_t *state);

double dp_dll_get_lock_stat(const dp_dll_state_t *state);

double dp_dll_get_noise_est(const dp_dll_state_t *state);

void dp_dll_tlm_flush(const dp_dll_state_t *s);

int dp_dll_set_telemetry(dp_dll_state_t *state, dp_tlm_t * tlm, const char * prefix, uint32_t decim);

/* ── Serializable state (standard bytes interface; see dp_state.h) ──────────
 * composition+field-wise: loop_filter child (POD-embedded) + embedded NCO
 * (POD) + running correlators/loop/lock state; borrowed `code` pointer
 * restored by create; the segments>1 chunk/lookback buffers (heap-owned,
 * pointers, NOT part of the whole-struct snapshot) are packed/restored
 * field-wise when segments > 1. */
#define DLL_STATE_MAGIC DP_FOURCC ('D','L','L',' ')
#define DLL_STATE_VERSION 15u /* v15: the output level, out_level/out_level_n (#2495); v14: the taps' phase words (#2397); v13: wrap_pending/wrap_await (#1287); v12: the gain table and err_sum/err_n (#1280); v11: coast (#1271); v10: aid_last_end (#1264); v9: the aid's early/late rings + inv_upd
                                (the loop steers once per symbol on the
                                aided window).
                                v8: symbol-period aid fields + rings;
                                v7: `rate_aid` carrier-aiding field added
                                (whole-struct snapshot, so the blob grew).
                                v6: `sums` scratch field added to the
                                struct (pure epoch-local scratch, not
                                serialized -- grows sizeof(dp_dll_state_t)
                                regardless, so the version marks the
                                layout change) (v5: precomputed
                                inv_tsamps/inv_tsamps2/inv_tsamps_sf
                                fields added to the struct; v4: fixed-
                                point code_nco + segments>1 chunked
                                lookback buffers; see dll_core.c) */
size_t dp_dll_state_bytes (const dp_dll_state_t *state);
void dp_dll_get_state (const dp_dll_state_t *state, void *blob);
int dp_dll_set_state (dp_dll_state_t *state, const void *blob);

#ifdef __cplusplus
}
#endif

#endif /* DLL_CORE_H */