Skip to content

File dsss_receiver_core.h

FileList > dsss_receiver > dsss_receiver_core.h

Go to the source code of this file

Composed continuous DSSS receiver: Acquisition -> Costas(bn_fll) pre-despread carrier wipeoff -> Dll(segments) -> RateConverter -> MpskReceiver, one object. More...

  • #include "RateConverter/RateConverter_core.h"
  • #include "acq/acq_core.h"
  • #include "cic/cic_core.h"
  • #include "dll/dll_core.h"
  • #include "dp_state.h"
  • #include "hbdecim/hbdecim_core.h"
  • #include "mpsk_receiver/mpsk_receiver_core.h"
  • #include "resamp/resamp_core.h"
  • #include "resample/resample_core.h"
  • #include <complex.h>
  • #include <stddef.h>
  • #include "costas/costas_core.h"
  • #include "snr/snr_core.h"
  • #include "ber/ber_core.h"

Classes

Type Name
struct dsss_receiver_extra_t
struct dsss_receiver_state_t
Composed receiver state.

Public Functions

Type Name
int dsss_receiver_configure_chain_raw (dsss_receiver_state_t * state, size_t segments, size_t sps, int n)
Pin the despread/resample/demod grid directly, bypassing the create-time segments /sps defaults.
void dsss_receiver_configure_lock_raw (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)
Re-tune the embedded Dll's code-lock detector directly. Forwards to dll_configure_lock_raw() . Only meaningful once tracking has begun (dll is NULL before then); a no-op while searching. The detector is the hysteretic lockdet over the DLL's per-N-look CFAR statistic —up_thresh /down_thresh set the declare/drop levels andn_up /n_down the consecutive-look verify counts, trading declare latency against false-alarm rate.
int dsss_receiver_configure_search_raw (dsss_receiver_state_t * state, size_t doppler_bins, size_t n_noncoh)
Pin the embedded Acquisition's search grid directly. Forwards to acq_configure_search_raw() — the escape hatch under this object's ownsymbol_rate -driven auto-sizing, for a power user who wants a specific(doppler_bins, n_noncoh) instead. Only meaningful while searching (a no-op has already happened once tracking has begun; the acquisition search doesn't run again until the nextreset() ).
dsss_receiver_state_t * 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)
Create a DSSS receiver in the searching state.
void dsss_receiver_destroy (dsss_receiver_state_t * state)
Destroy a receiver and release all four children.
double dsss_receiver_get_chip_phase (const dsss_receiver_state_t * state)
Dll's live tracked code phase (chips); 0.0 while searching.
double dsss_receiver_get_cn0_dbhz_est (const dsss_receiver_state_t * state)
double dsss_receiver_get_code_rate (const dsss_receiver_state_t * state)
Dll's own tracking-quality indicator; 1.0 while searching.
double dsss_receiver_get_doppler_hz (const dsss_receiver_state_t * state)
double dsss_receiver_get_lock (const dsss_receiver_state_t * state)
MpskReceiver's carrier lock EMA; 0.0 while searching.
int dsss_receiver_get_n (const dsss_receiver_state_t * state)
double dsss_receiver_get_norm_freq (const dsss_receiver_state_t * state)
MpskReceiver's tracked carrier frequency; 0.0 while searching.
size_t dsss_receiver_get_segments (const dsss_receiver_state_t * state)
size_t dsss_receiver_get_sps (const dsss_receiver_state_t * state)
void dsss_receiver_get_state (const dsss_receiver_state_t * state, void * blob)
int dsss_receiver_get_tracking (const dsss_receiver_state_t * state)
void dsss_receiver_reset (dsss_receiver_state_t * state)
Return to the searching state. Resets the embedded Acquisition and frees dll /rc /rx (rebuilt from scratch on the next hit) — a receiver that has locked cannot be "reset back to tracking the same signal," only back to searching, matching every other object's reset() semantics in this codebase.
int dsss_receiver_set_state (dsss_receiver_state_t * state, const void * blob)
size_t dsss_receiver_state_bytes (const dsss_receiver_state_t * state)
size_t dsss_receiver_steps (dsss_receiver_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out)
Stream raw cf32 samples; emit demodulated symbols once locked.
size_t dsss_receiver_steps_max_out (dsss_receiver_state_t * state)

Macros

Type Name
define DSSS_RECEIVER_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('D', 'S', 'R', 'X')
define DSSS_RECEIVER_STATE_VERSION /* multi line expression */
define DSSS_RX_BN_CARRIER 0.01
define DSSS_RX_BN_FLL 0.03

Detailed Description

The single-object form of the chain validated across this repo's "continuous async-DSSS receiver" story (docs/gallery/async-dsss-receiver-spec.md, docs/gallery/dsss-acq-characterization.md, docs/gallery/dsss-receiver.md): a continuous, non-bursty spreading code whose data-symbol clock need not be synchronous to the code-epoch clock. steps() streams raw samples through whichever child is currently active:

  • searching (tracking() == 0): samples feed the embedded Acquisition. Nothing is emitted. On a hit, the carrier loop/ Dll/RateConverter/MpskReceiver are built from the hit's code phase and Doppler estimate (the exact dll_init_chip_from_acq phase-inversion and RateConverter-bridged sample-rate hand-off this repo's gallery pages validated by hand), and the unconsumed tail of the same steps() call is handed straight to them — no samples are dropped at the transition.
  • tracking (tracking() == 1): samples are first derotated by a pre-despread carrier loop (costas_wipeoff/costas_update, one update per code period, bn_fll-assisted removes BULK Doppler and its RATE OF CHANGE before the code loop ever sees it; a fixed/ bounded residual alone is fine downstream-only, per docs/design/async-dsss-receiver.md §3.4, but an unbounded Doppler RATE is not), one code period at a time (a small internal carry buffer holds any leftover partial-period tail across calls steps() still accepts any block size), then feed Dll -> RateConverter -> MpskReceiver in sequence the C-level equivalent of hand-composing those four objects (plus the new carrier stage) and demodulated symbols are emitted. This is a NEW composition living entirely in this object deliberately NOT a swap to the existing Despreader object (which fuses Costas+Dll per-sample), because Despreader embeds Dll via dll_init(), hardcoded to segments==1; it cannot carry this object's own segments>1 async-lookback tracking. dll_steps() itself is called completely unmodified.

Per [[feedback_despread_resample_demod_separation]] (this story's own hard-won lesson): segments (the despreader's own tracking parameter) and sps (the demodulator's own sample-rate need) are independently configurable and bridged by an explicit RateConverter, never coupled to each other.

// "Just works": only the signal's own physical parameters are required.
dsss_receiver_state_t *rx = dsss_receiver_create(
    code, code_len, 3.0e6, 2100.0,   // chip_rate, symbol_rate
    2, 2,                            // spc, m (BPSK)
    55.0, 1e-3, 0.9, 100.0,          // cn0_dbhz, pfa, pd,
                                     // doppler_uncertainty
    4, 8,                            // segments, sps
    0);                              // differential
float _Complex syms[4096];
size_t n = dsss_receiver_steps(rx, x, x_len, syms, 4096);
dsss_receiver_destroy(rx);

Public Functions Documentation

function dsss_receiver_configure_chain_raw

Pin the despread/resample/demod grid directly, bypassing the create-time segments /sps defaults.

int dsss_receiver_configure_chain_raw (
    dsss_receiver_state_t * state,
    size_t segments,
    size_t sps,
    int n
) 

The escape hatch for the one composition-specific knob this object adds beyond its children's own: segments (Dll's tracking parameter) and sps/n (MpskReceiver's sample-rate/carrier-arm parameters) are indepen­dently overridable here, still bridged by a freshly-sized RateConverter — never coupled to each other (see the module docstring). Rebuilds dll/rc/rx with every replacement allocated first, only freeing and adopting the old ones once every allocation has succeeded (mirrors Acquisition's own acq_regrid() discipline) — a failed pin leaves the receiver tracking on its prior grid, not half-destroyed. Only meaningful once tracking (the grid defaults still apply to create-time auto-sizing for the next hit while searching; call dsss_receiver_create() with different segments/sps for that, or re-pin here again after the next hit).

Parameters:

  • state The receiver.
  • segments Dll tracking segments per code period.
  • sps MpskReceiver samples per symbol (the resample target).
  • n MpskReceiver's carrier-arm count; must divide sps.

Returns:

0 on success, -1 on invalid grid or an allocation failure (the receiver is left usable at its prior grid on failure).

>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
>>> rx.configure_chain_raw(segments=6, sps=8, n=8)  # re-pin the chain
>>> rx.segments                       # tracking grid updated in place
6


function dsss_receiver_configure_lock_raw

Re-tune the embedded Dll's code-lock detector directly. Forwards to dll_configure_lock_raw() . Only meaningful once tracking has begun (dll is NULL before then); a no-op while searching. The detector is the hysteretic lockdet over the DLL's per-N-look CFAR statistic —up_thresh /down_thresh set the declare/drop levels andn_up /n_down the consecutive-look verify counts, trading declare latency against false-alarm rate.

void dsss_receiver_configure_lock_raw (
    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
) 

Parameters:

  • state Must be non-NULL.
  • up_thresh CFAR-statistic level to declare code lock (hit when the statistic exceeds it).
  • down_thresh Level below which a look is a miss; choose <= up_thresh for level hysteresis.
  • n_looks Looks per decision — the DLL's non-coherent integration depth feeding one statistic.
  • alpha EMA smoothing coefficient on the lock statistic (0..1); smaller is smoother/slower.
  • n_up Consecutive hits required to declare lock.
  • n_down Consecutive misses required to drop lock.
    >>> import numpy as np
    >>> from doppler.dsss import DsssReceiver
    >>> from doppler.wfm import Gold
    >>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
    >>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
    >>> rx.configure_lock_raw(up_thresh=0.4, down_thresh=0.2, n_looks=20,
    ...                       alpha=0.1, n_up=5, n_down=3)
    >>> rx.tracking                # a no-op until a hit builds the Dll
    0
    

function dsss_receiver_configure_search_raw

Pin the embedded Acquisition's search grid directly. Forwards to acq_configure_search_raw() — the escape hatch under this object's ownsymbol_rate -driven auto-sizing, for a power user who wants a specific(doppler_bins, n_noncoh) instead. Only meaningful while searching (a no-op has already happened once tracking has begun; the acquisition search doesn't run again until the nextreset() ).

int dsss_receiver_configure_search_raw (
    dsss_receiver_state_t * state,
    size_t doppler_bins,
    size_t n_noncoh
) 

Parameters:

  • state Must be non-NULL.
  • doppler_bins Number of Doppler window tiles to search (>= 1); capped by the create-time doppler_uncertainty span (one tile per code-epoch Doppler bin width).
  • n_noncoh Non-coherent looks accumulated per grid cell (1..256); more looks buys sensitivity at the cost of dwell, replacing the auto-sized count.

Returns:

0 on success, -1 on invalid grid (see acq_configure_search_raw).

>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
>>> rx.configure_search_raw(doppler_bins=1, n_noncoh=16)  # pin it
>>> rx.tracking                # still searching, on the pinned grid
0


function dsss_receiver_create

Create a DSSS receiver in the searching state.

dsss_receiver_state_t * 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
) 

Only code/chip_rate/symbol_rate describe the signal itself — everything else is a physically-motivated default a caller can override, not a requirement. Internally: the embedded Acquisition is built via acq_create_continuous() (this receiver is inherently continuous/streaming) always window-tiles, never coherently combines across epochs, sensitivity purely from an internally auto-sized non-coherent look count (see acq_core.h's file doc comment); Dll always uses bn=0.002 (this story's own validated stable loop bandwidth for a one-update-per-code-epoch geometry, not dll_create()'s own default of 0.01, which this story found unstable here) and zeta=0.707, spacing=0.5; MpskReceiver always uses pulse=iandd, bn_carrier=bn_timing=0.01, zeta=0.707 and lock_thresh=0.3 — this story's own validated values throughout. It also passed acq_to_track=1 and warmup_syms=30 until those were deleted (doppler#877, 1f417e97); the composed receiver now runs its one NDA discriminator here as everywhere else. lock_thresh=0.3 predates the lock statistic becoming a calibrated detector and is retained because it is validated here, but it now has a derivable reading: the carrier lock EMA's noise-only sd is 0.1132 at every M, so 0.3 is 2.65 noise sigmas, a per-look Pfa of ~4e-3 — looser than MpskReceiver's own 0.5 default (4.42 sigma, 5e-6) and still ~6 sigma clear of the +0.99 a locked BPSK constellation reads, which is why it holds. See carrier_nda_core.h. n (MpskReceiver's carrier-arm count) is derived from sps: the largest divisor of sps in {4, 2, 1}.

Parameters:

  • code Spreading code, one 0/1 chip per element (0 -> +1, 1 -> -1 BPSK; only the low bit is used, so pass 0/1, not +/-1).
  • code_len Chips in code (the spreading factor).
  • chip_rate Chip rate, Hz. Required.
  • symbol_rate Data-symbol rate, Hz. Required — passed straight to the embedded Acquisition's own symbol_rate (diagnostic there; see acq_create_continuous()).
  • spc Samples/chip (front-end oversample); default 2 (fs = 2x chip_rate).
  • m PSK order, 2/4/8; default 2 (BPSK).
  • cn0_dbhz Design C/N0 for acquisition sizing, dB-Hz; default 55.0.
  • pfa Acquisition false-alarm target; default 1e-3.
  • pd Acquisition detection-probability target; default 0.9.
  • doppler_uncertainty One-sided Doppler search half-range, Hz; default 100.0.
  • segments Dll's own non-coherent partial-correlation count per code epoch — its tracking- robustness parameter, independent of sps (see the module docstring); default 4, this story's own validated sweet spot.
  • sps MpskReceiver's samples/symbol, reached by an internal RateConverter bridging the despreader's own partial rate to this rate; default 8, MpskReceiver's own constructor default.
  • differential MpskReceiver's differential (rotation- invariant) demap; default 0 (coherent).
    >>> import numpy as np
    >>> from doppler.dsss import DsssReceiver
    >>> from doppler.wfm import Gold
    >>> sf, chip, sym, spc = 1023, 3.0e6, 2100.0, 2
    >>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym
    >>> code = np.asarray(Gold().generate(sf)).astype(np.uint8)
    >>> csign = np.where(code & 1, -1.0, 1.0)
    >>> rng = np.random.default_rng(6)
    >>> n = int(400 * tsym) + 2 * te            # 400 BPSK data symbols
    >>> idx = np.arange(n)
    >>> data = (rng.integers(0, 2, 404) * 2 - 1).astype(float)
    >>> si = np.clip((idx / tsym).astype(int), 0, 403)
    >>> spread = data[si] * csign[(idx // spc) % sf]        # DSSS chips
    >>> sig = spread * np.exp(2j * np.pi * (50.0 / fs) * idx)  # +50 Hz
    >>> pre = 3 * te                     # noise-only lead-in, pre-signal
    >>> sigma = np.sqrt(fs / 10 ** (90.0 / 10))            # ~90 dB-Hz C/N0
    >>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n)
    ...          + 1j * rng.standard_normal(pre + n))
    >>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64)
    ...      + noise.astype(np.complex64))
    >>> rx = DsssReceiver(code, chip_rate=chip, symbol_rate=sym, spc=spc,
    ...                   cn0_dbhz=55.0, doppler_uncertainty=100.0)
    >>> syms = [rx.steps(x[p:p + te]) for p in range(0, len(x) - te, te)]
    >>> syms = np.concatenate([s for s in syms if len(s)])
    >>> rx.tracking                  # acquired, now demodulating
    1
    >>> len(syms) > 300              # a few hundred symbols recovered
    True
    
    Nearly all the energy lands on I, so the BPSK phase is resolved:
    
    >>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2))
    True
    

function dsss_receiver_destroy

Destroy a receiver and release all four children.

void dsss_receiver_destroy (
    dsss_receiver_state_t * state
) 

Parameters:

  • state May be NULL.

function dsss_receiver_get_chip_phase

Dll's live tracked code phase (chips); 0.0 while searching.

double dsss_receiver_get_chip_phase (
    const dsss_receiver_state_t * state
) 


function dsss_receiver_get_cn0_dbhz_est

double dsss_receiver_get_cn0_dbhz_est (
    const dsss_receiver_state_t * state
) 

function dsss_receiver_get_code_rate

Dll's own tracking-quality indicator; 1.0 while searching.

double dsss_receiver_get_code_rate (
    const dsss_receiver_state_t * state
) 


function dsss_receiver_get_doppler_hz

double dsss_receiver_get_doppler_hz (
    const dsss_receiver_state_t * state
) 

function dsss_receiver_get_lock

MpskReceiver's carrier lock EMA; 0.0 while searching.

double dsss_receiver_get_lock (
    const dsss_receiver_state_t * state
) 


function dsss_receiver_get_n

int dsss_receiver_get_n (
    const dsss_receiver_state_t * state
) 

function dsss_receiver_get_norm_freq

MpskReceiver's tracked carrier frequency; 0.0 while searching.

double dsss_receiver_get_norm_freq (
    const dsss_receiver_state_t * state
) 


function dsss_receiver_get_segments

size_t dsss_receiver_get_segments (
    const dsss_receiver_state_t * state
) 

function dsss_receiver_get_sps

size_t dsss_receiver_get_sps (
    const dsss_receiver_state_t * state
) 

function dsss_receiver_get_state

void dsss_receiver_get_state (
    const dsss_receiver_state_t * state,
    void * blob
) 

function dsss_receiver_get_tracking

int dsss_receiver_get_tracking (
    const dsss_receiver_state_t * state
) 

function dsss_receiver_reset

Return to the searching state. Resets the embedded Acquisition and frees dll /rc /rx (rebuilt from scratch on the next hit) — a receiver that has locked cannot be "reset back to tracking the same signal," only back to searching, matching every other object's reset() semantics in this codebase.

void dsss_receiver_reset (
    dsss_receiver_state_t * state
) 

Parameters:

  • state Must be non-NULL.
    >>> import numpy as np
    >>> from doppler.dsss import DsssReceiver
    >>> from doppler.wfm import Gold
    >>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
    >>> rx = DsssReceiver(code, chip_rate=3.0e6, symbol_rate=2100.0, spc=2)
    >>> rx.reset()                 # abort any lock, hunt from scratch
    >>> (rx.tracking, rx.chip_phase)   # back to searching, all cleared
    (0, 0.0)
    

function dsss_receiver_set_state

int dsss_receiver_set_state (
    dsss_receiver_state_t * state,
    const void * blob
) 

function dsss_receiver_state_bytes

size_t dsss_receiver_state_bytes (
    const dsss_receiver_state_t * state
) 

function dsss_receiver_steps

Stream raw cf32 samples; emit demodulated symbols once locked.

size_t dsss_receiver_steps (
    dsss_receiver_state_t * state,
    const float _Complex * x,
    size_t x_len,
    float _Complex * out,
    size_t max_out
) 

While searching, samples feed the embedded Acquisition and nothing is emitted (0 return is normal, not an error). The moment a hit fires, Dll/RateConverter/MpskReceiver are built and seeded from it, and the unconsumed tail of THIS call — computed exactly from acq->samples_consumed, no samples dropped or double-fed — is handed straight to them in the same call. While tracking, samples feed Dll -> RateConverter -> MpskReceiver in sequence. Accepts any block size; state carries across calls (Acquisition/Dll/ RateConverter/MpskReceiver are all already block-size invariant, so this object needs no ring-buffering of its own).

Parameters:

  • state Must be non-NULL.
  • x Input cf32 samples.
  • x_len Number of input samples.
  • out Output symbols; caller provides max_out capacity.
  • max_out Output capacity.

Returns:

Number of symbols written (0 while searching, or while tracking with not yet a full symbol's worth of input).

>>> import numpy as np
>>> from doppler.dsss import DsssReceiver
>>> from doppler.wfm import Gold
>>> sf, chip, sym, spc = 1023, 3.0e6, 2100.0, 2
>>> fs, te, tsym = chip * spc, sf * spc, chip * spc / sym
>>> code = np.asarray(Gold().generate(sf)).astype(np.uint8)
>>> csign = np.where(code & 1, -1.0, 1.0)
>>> rng = np.random.default_rng(6)
>>> n = int(400 * tsym) + 2 * te            # 400 BPSK data symbols
>>> idx = np.arange(n)
>>> data = (rng.integers(0, 2, 404) * 2 - 1).astype(float)
>>> si = np.clip((idx / tsym).astype(int), 0, 403)
>>> spread = data[si] * csign[(idx // spc) % sf]        # DSSS chips
>>> sig = spread * np.exp(2j * np.pi * (50.0 / fs) * idx)  # +50 Hz
>>> pre = 3 * te                     # noise-only lead-in, pre-signal
>>> sigma = np.sqrt(fs / 10 ** (90.0 / 10))            # ~90 dB-Hz C/N0
>>> noise = (sigma / np.sqrt(2)) * (rng.standard_normal(pre + n)
...          + 1j * rng.standard_normal(pre + n))
>>> x = (np.concatenate([np.zeros(pre), sig]).astype(np.complex64)
...      + noise.astype(np.complex64))
>>> rx = DsssReceiver(code, chip_rate=chip, symbol_rate=sym, spc=spc,
...                   cn0_dbhz=55.0, doppler_uncertainty=100.0)
>>> syms = [rx.steps(x[p:p + te]) for p in range(0, len(x) - te, te)]
>>> syms = np.concatenate([s for s in syms if len(s)])
>>> rx.tracking                  # acquired and now demodulating
1
>>> len(syms) > 300              # a few hundred symbols recovered
True

Nearly all the energy lands on I, so the BPSK phase is resolved:

>>> bool(np.mean(syms.real**2) > 10 * np.mean(syms.imag**2))
True


function dsss_receiver_steps_max_out

size_t dsss_receiver_steps_max_out (
    dsss_receiver_state_t * state
) 

Macro Definition Documentation

define DSSS_RECEIVER_STATE_MAGIC

#define DSSS_RECEIVER_STATE_MAGIC `DP_FOURCC ('D', 'S', 'R', 'X')`

define DSSS_RECEIVER_STATE_VERSION

#define DSSS_RECEIVER_STATE_VERSION `2u /* v2: pre-despread costas_state_t car + carry buffer added */`

define DSSS_RX_BN_CARRIER

#define DSSS_RX_BN_CARRIER `0.01`

define DSSS_RX_BN_FLL

#define DSSS_RX_BN_FLL `0.03`


The documentation for this class was generated from the following file native/inc/dsss_receiver/dsss_receiver_core.h