File dll_core.h¶
File List > dll > dll_core.h
Go to the documentation of this file
#ifndef DLL_CORE_H
#define DLL_CORE_H
#include "clib_common.h"
#include "dp_state.h"
#include "jm_perf.h"
#include "lockdet/lockdet_core.h"
#include "loop_filter/loop_filter_core.h"
#include "nco/nco_core.h"
#include "dp_tlm/dp_tlm_core.h"
#include <complex.h>
#include <math.h>
#include "detection/detection_core.h"
#include "telemetry/telemetry_core.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Numerical guard on the early+late envelope sum (not tunable). */
#define DLL_EPS 1e-12
/* 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;
} dll_tlm_t;
typedef struct {
loop_filter_state_t lf;
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;
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;
/* ── segments>1 chunked output + one-epoch-deep lookback (heap-owned,
* length `segments`; NULL when segments==1 -- 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;
/* ── lock detector (always on): offset-tap CFAR noise ref + N-look test */
float _Complex acc_o;
double off_chips;
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;
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 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;
} dll_state_t;
JM_FORCEINLINE float
dll_chip_sign(uint8_t c)
{
return (c & 1u) ? -1.0f : 1.0f;
}
JM_FORCEINLINE float
dll_replica(const dll_state_t *s, double c)
{
double sfd2 = 2.0 * (double)s->sf;
double p = dp_fmod_pos(c * 2.0 - 0.5, sfd2);
size_t i = (size_t)p;
double mu = p - (double)i;
size_t j = (i + 1 >= (size_t)sfd2) ? 0 : i + 1;
float v0 = dll_chip_sign(s->code[i >> 1]);
float v1 = dll_chip_sign(s->code[j >> 1]);
return (float)((1.0 - mu) * v0 + mu * v1);
}
/* Cycles -> u32 phase delta: nco_norm_freq_to_inc() (native/inc/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 dll_init(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 double
dll_dwell_center_chip_pos(const dll_state_t *s)
{
uint32_t mid = s->code_nco.phase + (s->code_nco.phase_inc >> 1);
return ((double)mid / 4294967296.0) * (double)s->sf;
}
JM_FORCEINLINE JM_HOT int
dll_accumulate(dll_state_t *s, float _Complex d)
{
double sfd = (double)s->sf;
double cp = dll_dwell_center_chip_pos(s);
double ce = cp + s->spacing;
if (ce >= sfd)
ce -= sfd;
double cl = cp - s->spacing;
if (cl < 0.0)
cl += sfd;
s->acc_p += d * dll_replica(s, cp);
s->acc_e += d * dll_replica(s, ce);
s->acc_l += d * dll_replica(s, cl);
/* nco_step_u32_ovf() (native/inc/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. */
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(dll_state_t *s, float _Complex d)
{
double co = dll_dwell_center_chip_pos(s) + s->off_chips;
if (co >= (double)s->sf)
co -= (double)s->sf;
s->acc_o += d * dll_replica(s, co);
}
void dll_lock_look(dll_state_t *s, double norm);
void dll_lock_epoch(dll_state_t *s);
JM_FORCEINLINE JM_HOT void
dll_update(dll_state_t *s)
{
float me = cabsf(s->acc_e), ml = cabsf(s->acc_l), mp = cabsf(s->acc_p);
double ep = (double)me * me, lp = (double)ml * ml, pp = (double)mp * mp;
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;
loop_filter_step(&s->lf, e);
/* Pure control deviation: the integrator alone, PLUS the
proportional term spread smoothly over the whole next period
rather than kicked directly into `phase` (see the comment
above) -- kp*e chips of total correction over sf*sps samples is
kp*e/(sf*sf*sps) extra cycles per sample, the same total
chip-domain correction the original double-accumulator design
applied as `chip_pos += kp*e`. Neither term involves "1.0", and
neither divides -- inv_tsamps/inv_tsamps_sf are precomputed once
at construction (configure_geometry()), never here. */
double ctrl = s->lf.integ * s->inv_tsamps + s->lf.kp * e * s->inv_tsamps_sf;
s->code_rate = 1.0 + s->lf.integ; /* public ratio observable only */
/* rate_aid (0 = off): a fixed carrier-aiding rate bias, scaled by the
nominal per-sample rate so it sums into the sample-and-hold phase_inc
as a continuous adjustment across the epoch, not a phase pulse. */
s->code_nco.phase_inc
= nco_norm_freq_to_inc(s->inv_tsamps * (1.0 + s->rate_aid) + ctrl);
}
dll_state_t *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);
size_t dll_lookback_segments(size_t tsamps, double max_error_db);
void dll_destroy(dll_state_t *state);
void dll_reset(dll_state_t *state);
size_t dll_steps_max_out(dll_state_t *state);
size_t dll_steps(dll_state_t *state, const float _Complex *x, size_t x_len, float _Complex *out, size_t max_out);
void dll_configure(dll_state_t *state, double bn, double zeta);
double dll_get_bn(const dll_state_t *state);
void dll_set_bn(dll_state_t *state, double val);
void dll_set_rate_aid(dll_state_t *state, double rate_aid);
int dll_set_symbol_period(dll_state_t *state, double partials_per_symbol);
size_t dll_get_symbol_window(const dll_state_t *state);
int dll_set_lock_verify(dll_state_t *state, uint32_t n_up, uint32_t n_down);
double dll_get_code_phase(const dll_state_t *state);
double dll_get_code_rate(const dll_state_t *state);
double dll_get_last_error(const dll_state_t *state);
size_t dll_get_segments(const dll_state_t *state);
int dll_configure_lock(dll_state_t *state, double pfa, size_t n_looks, double ref_snr_db);
void dll_configure_lock_raw(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 dll_get_locked(const dll_state_t *state);
double dll_get_lock_stat(const dll_state_t *state);
double dll_get_noise_est(const dll_state_t *state);
void dll_tlm_flush(const dll_state_t *s);
int dll_set_telemetry(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 10u /* 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(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 dll_state_bytes (const dll_state_t *state);
void dll_get_state (const dll_state_t *state, void *blob);
int dll_set_state (dll_state_t *state, const void *blob);
#ifdef __cplusplus
}
#endif
#endif /* DLL_CORE_H */