Skip to content

File async_dsss_receiver_core.h

File List > async_dsss_receiver > async_dsss_receiver_core.h

Go to the documentation of this file

#ifndef ASYNC_DSSS_RECEIVER_CORE_H
#define ASYNC_DSSS_RECEIVER_CORE_H

#include "RateConverter/RateConverter_core.h"
#include "acq/acq_core.h"
#include "carrier_acq/carrier_acq_core.h"
#include "cic/cic_core.h"
#include "costas/costas_core.h"
#include "dll/dll_core.h"
#include "dp_state.h"
#include "hbdecim/hbdecim_core.h"
#include "lockdet/lockdet_core.h"
#include "mpsk_receiver/mpsk_receiver_core.h"
#include "resamp/resamp_core.h"
#include "resample/resample_core.h"
#include <complex.h>
#include <stdbool.h>
#include <stddef.h>
#include "psd/psd_core.h"
#include "detector/detector_core.h"
#include "detection/detection_core.h"
#include "spectral/spectral_core.h"
#include "corr/corr_core.h"
#include "fft/fft_core.h"
#include "acc_trace/acc_trace_core.h"
#include "ber/ber_core.h"
#include "doppler_channel/doppler_channel_core.h"

#ifdef __cplusplus
extern "C"
{
#endif

  /* SPEC-derived carrier-loop bandwidth for the pre-despread Costas
   * (hardcoded, not a public constructor param, matching this object's
   * "just works" philosophy). The refine-stage's frozen carrier never
   * calls costas_update(), so its own bn/zeta are irrelevant -- only its
   * tsamps (the wipe period length) and init_norm_freq matter.
   *
   * Wider than the 0.01 first used, for two reasons the narrow value could
   * not meet: (1) PULL-IN -- the refined seed still leaves ~tens of Hz of
   * residual (PSDMF accuracy + the ramp drifting the carrier over the
   * refine->track latency), and a per-code-period PLL at 0.01 has a pull-in
   * range far under that, so the residual wraps as a zero-mean sinusoid and
   * the loop never locks; (2) DYNAMICS -- tracking SPEC's 500 Hz/s ramp with
   * a per-period Type-II loop needs the bandwidth to keep the velocity lag
   * (phase error ~ ramp/omega_n^2) small. 0.04 pulls the seed in and rides
   * the ramp with a clean (~0 deg) despread constellation; the minimum that
   * works is ~0.03 (0.02 under-pulls). Because the phase detector is
   * |P|-normalized (unit-slope), this is the true noise bandwidth, so it
   * costs some phase noise at the 0-Doppler floor -- but the ~5 dB Es/N0
   * floor is still met.
   *
   * THE PULL-IN NUMBER IS NOW MEASURED, and the "~tens of Hz" above is the
   * assumption rather than the observation. `native/validation/costas_pullin.c`
   * sweeps this loop's acquisition range in multiples of the `bn/m` bound
   * (`m = 2`, the squaring discriminator) and re-derives it on every `make
   * test`. At bn = 0.04 over a 2046-sample code period that bound is
   * 9.775e-6 cyc/sample -- 60 Hz at SPEC's 6.138 MHz front end -- and the
   * loop acquires 100% inside it on BOTH signs, is still reliable at 2x, and
   * is dead by 4x. (Tighter than the MPSK carrier's measured 4x/5x envelope;
   * the two are separate sweeps for that reason.)
   *
   * What the assumption cost: doppler#982. The receiver's own hand-off
   * residual was measured at -54..+375 Hz, i.e. routinely 2-6x this bound,
   * so the loop is asked to acquire from outside its range on most draws and
   * the resulting failures read as an acquisition problem. Before widening
   * bn again, check the hand-off against the bound -- this value was tuned
   * empirically against the residual assumption that measurement contradicts.
   *
   * NO FLL: the pre-despread Costas always passes bn_fll = 0 (a pure PLL).
   * The FLL cross-product frequency discriminator is far too noisy on this
   * loop's data-modulated despread-symbol input. The phase discriminator is
   * NON-DATA-AIDED (squaring the emitted coherent-I&D partials) so it is
   * immune to the async data transitions that land inside nearly every code
   * period (see adr_track_period()); with that clean, transition-robust error
   * the plain PLL tracks the coupled Doppler (offset AND 500 Hz/s ramp)
   * pre-despread. So the FLL is not exposed here at all, not merely
   * defaulted off. */
#define ASYNC_DSSS_RX_BN_CARRIER 0.04
  /* Dll's own bn: the validated stable code-loop bandwidth for the
   * one-update-per-partial tracking geometry -- same value DsssReceiver's
   * own Dll uses, not dll_create()'s own default of 0.01. (A wider 0.005 was
   * tried to chase sustained code-rate Doppler, but with the FLL removed the
   * carrier-driven slips are gone and the narrower 0.002 keeps its noise
   * immunity at the low-Es/N0 floor.) */
#define ASYNC_DSSS_RX_DLL_BN 0.002

  /* Symbol-lock detector on the emitted symbols. The lock signal is the
   * BPSK phase-lock statistic (I^2 - Q^2)/(I^2 + Q^2) = cos(2*phi) per
   * symbol (locked -> +1, 45deg-rotated/noise -> 0). The lock METRIC is that
   * signal integrated proportionally to SNR -- a power-weighted running mean,
   * EMA(I^2-Q^2)/EMA(I^2+Q^2), so high-|symbol| (high-SNR) symbols dominate
   * and noisy ones contribute little -- over a dwell of LOCK_DWELL symbols
   * (>= 30). A hysteretic lockdet (lockdet_core.h) then declares `locked`
   * after LOCK_N_UP consecutive symbols with the metric >= LOCK_UP and drops
   * it after LOCK_N_DOWN below LOCK_DOWN. */
#define ASYNC_DSSS_RX_REFINE_MIN_BLOCKS 7u
#define ASYNC_DSSS_RX_LOCK_DWELL 30u
#define ASYNC_DSSS_RX_LOCK_UP 0.5
#define ASYNC_DSSS_RX_LOCK_DOWN 0.3
#define ASYNC_DSSS_RX_LOCK_N_UP 30u
#define ASYNC_DSSS_RX_LOCK_N_DOWN 15u

/* The state machine's values (`state` below, also the first byte of a
 * serialized blob's extra record). Searching -> refining -> tracking is the
 * searching flavor's path; hand-off mode starts at idle and seed() puts it
 * at refining; lost is reached from tracking by the release rule and left
 * only by reset(). */
#define ASYNC_DSSS_RX_SEARCHING 0
#define ASYNC_DSSS_RX_REFINING 1
#define ASYNC_DSSS_RX_TRACKING 2
#define ASYNC_DSSS_RX_IDLE 3
#define ASYNC_DSSS_RX_LOST 4

  typedef struct
  {
    acq_state_t *acq; 
    /* Refine stage: a frozen-carrier collection Dll feeding
     * CarrierAcquisition via a RateConverter. Rebuilt on every real
     * acquisition hit and on reset(); otherwise untouched. */
    costas_state_t         car_frozen;
    dll_state_t           *refine_dll;
    RateConverter_state_t *refine_rc;
    carrier_acq_state_t   *ca;
    size_t refine_segments; 
    uint64_t refine_samples_fed; 
    /* Scratch, not state -- sized once per refine-chain (re)build (no
     * allocation in the steps() hot path): refine_dll's own emitted
     * partials (capacity refine_segments), then refine_rc's resampled
     * output (capacity refine_segments*refine_rc->rate + margin). */
    float _Complex *refine_dll_out_buf;
    size_t         refine_dll_out_cap;
    float _Complex *refine_rc_out_buf;
    size_t         refine_rc_out_cap;

    /* Track stage: DsssReceiver's own composition. costas_init()'s tsamps is
     * one whole code period, and the pre-despread carrier loop updates once
     * per period from a non-data-aided (squaring) combine of that period's
     * coherent-I&D partials (see adr_track_period() in the .c file). */
    costas_state_t         car;
    dll_state_t           *dll;
    RateConverter_state_t *rc;
    mpsk_receiver_state_t *rx;
    /* Scratch, not state -- sized at every track-chain (re)build and never
     * in the steps() hot path (#1192, the section 11.5 rule): the live
     * Dll's partials for ONE code period, then rc's resampled output for
     * it. The chain runs a period at a time, so both are fixed by tsamps
     * and rc->rate; they only ever grow. */
    float _Complex *track_dll_out_buf;
    size_t         track_dll_out_cap;
    float _Complex *track_rc_out_buf;
    size_t         track_rc_out_cap;

    /* Shared carrier-wipe scratch/carry -- refine and track stages never
     * run concurrently and both wipe in whole `tsamps`-sample periods, so
     * one buffer set serves either. */
    size_t         tsamps; 
    float _Complex *car_wiped_buf;
    float _Complex *car_carry_buf;
    size_t         car_carry_len;

    /* Own copy of the spreading code (same rationale as DsssReceiver's
     * own copy -- neither acq_create_continuous() nor dll_create()'s
     * borrow-vs-copy semantics are part of their public contract). */
    uint8_t *code;
    size_t   code_len;

    /* Config carried across every chain rebuild. */
    size_t spc;
    int    m;
    int    differential;
    size_t segments; 
    size_t sps;      
    int    n;        
    double chip_rate;
    double symbol_rate;
    double cn0_dbhz; 
    double pfa;      
    double pd;       
    /* Refine-stage tuning, fixed at construction (see objects/
     * async_dsss_receiver.toml for the rationale behind each default --
     * all mirror freq_refine.refine_seed_carrier_acq()'s own already-
     * validated defaults verbatim). */
    double refine_max_error_db;
    size_t refine_samples_per_symbol;
    double refine_design_margin_db;
    size_t refine_n_fft;
    size_t refine_zero_pad;
    bool   refine_sequential;
    size_t refine_max_n_blocks;
    size_t refine_min_blocks; 
    double carrier_freq_hz; 
    int state; 
    double   lost_confirm_s;       
    uint64_t lost_confirm_samples; 
    uint64_t state_samples;        
    uint64_t both_down_samples;    
    double   seed_chip_phase;     
    double   seed_doppler_hz_est; 
    double   doppler_hz_est;      
    double   cn0_dbhz_est;        
    uint64_t samples_fed;         
    /* Symbol-lock detector running state (see the ASYNC_DSSS_RX_LOCK_*
     * defines). lock_num/lock_den are the power-weighted EMAs of I^2-Q^2 and
     * I^2+Q^2; lock_metric = lock_num/lock_den = cos(2*phi); sym_lockdet is
     * the hysteretic up/down declare. Reset when the track chain is (re)built
     * (fresh lock per pass). */
    double          lock_num;
    double          lock_den;
    double          lock_metric;
    double          lock_alpha; 
    lockdet_state_t sym_lockdet;
  } async_dsss_receiver_state_t;

  async_dsss_receiver_state_t *async_dsss_receiver_create (
      const uint8_t *code, size_t code_len, double chip_rate,
      double symbol_rate, size_t spc, int m, double cn0_dbhz, double pfa,
      double pd, double doppler_uncertainty, size_t segments, size_t sps,
      int differential, double refine_max_error_db,
      size_t refine_samples_per_symbol, double refine_design_margin_db,
      size_t refine_n_fft, size_t refine_zero_pad, bool refine_sequential,
      size_t refine_max_n_blocks, double carrier_freq_hz,
      double lost_confirm_s);

  async_dsss_receiver_state_t *async_dsss_receiver_create_handoff (
      const uint8_t *code, size_t code_len, double chip_rate,
      double symbol_rate, size_t spc, int m, double cn0_dbhz, double pfa,
      double pd, size_t segments, size_t sps, int differential,
      double refine_max_error_db, size_t refine_samples_per_symbol,
      double refine_design_margin_db, size_t refine_n_fft,
      size_t refine_zero_pad, bool refine_sequential,
      size_t refine_max_n_blocks, double carrier_freq_hz,
      double lost_confirm_s);

  void async_dsss_receiver_destroy (async_dsss_receiver_state_t *state);

  void async_dsss_receiver_reset (async_dsss_receiver_state_t *state);

  size_t async_dsss_receiver_steps_max_out (async_dsss_receiver_state_t *state);

  size_t async_dsss_receiver_steps (async_dsss_receiver_state_t *state,
                                    const float _Complex *x, size_t x_len,
                                    float _Complex *out, size_t max_out);

  int async_dsss_receiver_seed (async_dsss_receiver_state_t *state,
                                double chip_phase, double doppler_hz_est,
                                double cn0_dbhz_est);

  typedef struct
  {
    int state; 
    double doppler_hz; 
    double chip_phase; 
    double code_rate;  
    double cn0_dbhz_est; 
    int    code_locked;  
    int    locked;       
    double lock_metric;  
    double lock_threshold; 
    double car_last_error; 
    double mpsk_last_error; 
    uint64_t state_samples; 
    uint64_t both_down_samples; 
  } async_dsss_receiver_status_t;

  async_dsss_receiver_status_t async_dsss_receiver_status (
      const async_dsss_receiver_state_t *state);

  int async_dsss_receiver_get_idle (const async_dsss_receiver_state_t *state);

  int async_dsss_receiver_get_lost (const async_dsss_receiver_state_t *state);

  int async_dsss_receiver_configure_search_raw (
      async_dsss_receiver_state_t *state, size_t doppler_bins,
      size_t n_noncoh);

  int async_dsss_receiver_set_refine_min_blocks (
      async_dsss_receiver_state_t *state, size_t n_blocks);

  void async_dsss_receiver_configure_lock_raw (
      async_dsss_receiver_state_t *state, double up_thresh,
      double down_thresh, size_t n_looks, double alpha, uint32_t n_up,
      uint32_t n_down);

  int async_dsss_receiver_configure_chain_raw (
      async_dsss_receiver_state_t *state, size_t segments, size_t sps,
      int n);

  int    async_dsss_receiver_get_tracking (
      const async_dsss_receiver_state_t *state);
  int    async_dsss_receiver_get_refining (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_doppler_hz (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_cn0_dbhz_est (
      const async_dsss_receiver_state_t *state);
  size_t async_dsss_receiver_get_segments (
      const async_dsss_receiver_state_t *state);
  size_t async_dsss_receiver_get_sps (const async_dsss_receiver_state_t *state);
  int    async_dsss_receiver_get_n (const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_chip_phase (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_code_rate (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_lock (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_norm_freq (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_nco_freq (
      const async_dsss_receiver_state_t *state);
  int async_dsss_receiver_get_locked (
      const async_dsss_receiver_state_t *state);
  int async_dsss_receiver_get_code_locked (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_car_last_error (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_car_nco_freq (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_mpsk_last_error (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_lock_metric (
      const async_dsss_receiver_state_t *state);
  double async_dsss_receiver_get_lock_threshold (
      const async_dsss_receiver_state_t *state);

  /* ── Serializable state (standard bytes interface; see dp_state.h) ──────
   * Composition: acq + car_frozen + refine_dll + refine_rc + ca + car +
   * dll + rc + rx, always all nine in the searching flavor and the eight
   * without acq in hand-off mode (a fixed shape per flavor, DsssReceiver's
   * own rationale). segments/sps/n/refine_segments and the flavor are the
   * layout key. */

  typedef struct
  {
    uint8_t  state;
    uint8_t  handoff; 
    uint8_t  _pad[6];
    double   seed_chip_phase;
    double   seed_doppler_hz_est;
    double   doppler_hz_est;
    double   cn0_dbhz_est;
    uint64_t segments;
    uint64_t sps;
    uint64_t n;
    uint64_t refine_segments;
    uint64_t refine_samples_fed;
    uint64_t car_carry_len;
    uint64_t state_samples;
    uint64_t both_down_samples;
    double   lock_num;  
    double   lock_den;  
    double   lock_metric;         
    lockdet_state_t sym_lockdet;  
  } async_dsss_receiver_extra_t;

#define ASYNC_DSSS_RECEIVER_STATE_MAGIC DP_FOURCC ('A', 'D', 'R', 'X')
#define ASYNC_DSSS_RECEIVER_STATE_VERSION 3u

  size_t async_dsss_receiver_state_bytes (
      const async_dsss_receiver_state_t *state);
  void   async_dsss_receiver_get_state (
      const async_dsss_receiver_state_t *state, void *blob);
  int async_dsss_receiver_set_state (async_dsss_receiver_state_t *state,
                                     const void *blob);

#ifdef __cplusplus
}
#endif

#endif /* ASYNC_DSSS_RECEIVER_CORE_H */