Skip to content

File despreader_core.h

FileList > despreader > despreader_core.h

Go to the source code of this file

Continuous DSSS despreader — Costas carrier loop + DLL code loop. More...

  • #include "clib_common.h"
  • #include "costas/costas_core.h"
  • #include "detection/detection_core.h"
  • #include "dll/dll_core.h"
  • #include "dp_state.h"
  • #include "jm_perf.h"
  • #include "lo/lo_core.h"
  • #include "lockdet/lockdet_core.h"
  • #include "loop_filter/loop_filter_core.h"
  • #include "dp_tlm/dp_tlm_core.h"
  • #include <complex.h>
  • #include "telemetry/telemetry_core.h"

Classes

Type Name
struct despreader_state_t
Despreader state.

Public Functions

Type Name
size_t despreader_bits (despreader_state_t * state, const float _Complex * x, size_t x_len, uint8_t * out, size_t max_out)
Despread a CF32 block and bit-sync the prompts into hard data bits.
size_t despreader_bits_max_out (despreader_state_t * state)
void despreader_configure_carrier_lock (despreader_state_t * state, double up_thresh, double down_thresh, uint32_t n_up, uint32_t n_down)
Re-tune the embedded carrier loop's lock detector directly.
int despreader_configure_code_lock (despreader_state_t * state, double pfa, size_t n_looks, double ref_snr_db)
Re-tune the embedded code loop's lock detector.
despreader_state_t * despreader_create (const uint8_t * code, size_t code_len, size_t sps, double init_norm_freq, double init_chip, double bn_carrier, double bn_code, double bn_fll, double zeta, double spacing, size_t periods_per_bit)
Create a continuous DSSS despreader (COPIES code ).
void despreader_destroy (despreader_state_t * state)
Destroy a despreader and release all memory.
size_t despreader_get_bit_phase (const despreader_state_t * state)
double despreader_get_bn_carrier (const despreader_state_t * state)
double despreader_get_bn_code (const despreader_state_t * state)
int despreader_get_carrier_locked (const despreader_state_t * state)
Carrier lock decision (1 = locked): the embedded Costas loop's verify-counted detector on its lock-metric EMA (see costas_configure_lock).
int despreader_get_code_locked (const despreader_state_t * state)
Code lock decision (1 = locked): the embedded DLL's verify-counted CFAR detector (see dll_configure_lock); live in composition — the despreader runs the same always-on detector dll_steps does.
double despreader_get_code_phase (const despreader_state_t * state)
double despreader_get_code_rate (const despreader_state_t * state)
double despreader_get_lock_metric (const despreader_state_t * state)
double despreader_get_norm_freq (const despreader_state_t * state)
void despreader_get_state (const despreader_state_t * state, void * blob)
void despreader_init (despreader_state_t * ch, const uint8_t * code, size_t code_len, size_t sps, double init_norm_freq, double init_chip, double bn_carrier, double bn_code, double bn_fll, double zeta, double spacing, size_t periods_per_bit)
Initialise a despreader in place; BORROWS code .
void despreader_reset (despreader_state_t * state)
Re-seed both loops to the create-time frequency/phase; keep config.
void despreader_set_bn_carrier (despreader_state_t * state, double val)
void despreader_set_bn_code (despreader_state_t * state, double val)
void despreader_set_norm_freq (despreader_state_t * state, double val)
int despreader_set_state (despreader_state_t * state, const void * blob)
int despreader_set_telemetry (despreader_state_t * state, dp_tlm_t * tlm, const char * prefix, uint32_t decim)
Attach (or detach) a telemetry context across the despreader. Pure forwarder — the despreader registers no probes of its own: the carrier loop registers "<prefix>.car.lock" / ".e" / ".freq" / ".locked" and the code loop registers "<prefix>.code.e" / ".rate" / ".lock" / ".locked" (the ".locked" pair are the loops' verify-counted lockdet decisions, 0/1) — eight probes, all thinned by decim and emitted once per code period (the despreader flushes both loops at its per-period update). Passing NULL detaches both loops. Setup path, never hot; the context is borrowed and must outlive the attachment (SPSC rules indp_tlm/dp_tlm_core.h ).
size_t despreader_state_bytes (const despreader_state_t * state)
size_t despreader_steps (despreader_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out)
Track carrier and code and despread a CF32 block, one prompt symbol per code period.
size_t despreader_steps_max_out (despreader_state_t * state)

Macros

Type Name
define DESPREADER_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('D', 'S', 'P', 'R')
define DESPREADER_STATE_VERSION /* multi line expression */

Detailed Description

A complete continuous despreader for a DSSS-BPSK signal: it composes a costas_state_t carrier loop and a dll_state_t code loop on a single shared per-sample integrate-and-dump. Per sample it wipes the carrier (costas_wipeoff, integer NCO) and feeds the de-rotated sample to the DLL's early/prompt/late correlators (dll_accumulate); per code period it dumps the prompt and updates both loops — the code loop on the early/late envelopes, the carrier loop on the same prompt symbol. steps() emits one prompt per period; bits() bit-syncs the prompts into hard data bits (a data bit spans periods_per_bit code periods).

It is seeded by acquisition (the FFT search supplies the coarse carrier frequency + code phase); the loops then track the residual. Set bn_fll > 0 for FLL-assisted carrier pull-in.

Lifecycle: despreader_create -> (steps / bits / reset)* -> despreader_destroy.

uint8_t code[127] = { ... };  // one code period, 0/1 chips
despreader_state_t *ch = despreader_create(code, 127, 8, 0.0, 0.0,
                                      0.05, 0.005, 0.0, 0.707, 0.5, 1);
float _Complex sym[64];
size_t k = despreader_steps(ch, rx, rx_len, sym, 64);  // prompt per period
despreader_destroy(ch);

Public Functions Documentation

function despreader_bits

Despread a CF32 block and bit-sync the prompts into hard data bits.

size_t despreader_bits (
    despreader_state_t * state,
    const float _Complex * x,
    size_t x_len,
    uint8_t * out,
    size_t max_out
) 

The same tracking kernel as despreader_steps(), followed by bit synchronisation: the per-period prompts are coherently summed across each detected bit boundary (a data bit spans periods_per_bit code periods) and one hard 0/1 bit is emitted per data bit. The bit boundary is estimated on-line from the prompt sign-flip histogram, so the phase is a BPSK ambiguity — a globally inverted decision stream is equally correct.

Parameters:

  • state Must be non-NULL.
  • x Input CF32 samples.
  • x_len Number of input samples.
  • out Output buffer for hard bits, 0/1 (>= max_out).
  • max_out Capacity of out in bits.

Returns:

Number of data bits written into out.

>>> import numpy as np
>>> from doppler.dsss import Despreader
>>> rng = np.random.default_rng(3)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)
>>> chips = np.where(code & 1, -1.0, 1.0)
>>> bits = rng.integers(0, 2, 40).astype(np.uint8)
>>> syms = np.where(bits == 1, -1.0, 1.0)
>>> rx = np.concatenate(
...     [s * np.repeat(chips, 4) for s in syms]).astype(np.complex64)
>>> d = Despreader(code=code, sps=4)
>>> data = d.bits(rx)                       # hard data bits
>>> e = np.mean(data != bits[:data.size])   # up to a BPSK sign flip
>>> round(float(min(e, 1.0 - e)), 4)
0.0


function despreader_bits_max_out

size_t despreader_bits_max_out (
    despreader_state_t * state
) 

function despreader_configure_carrier_lock

Re-tune the embedded carrier loop's lock detector directly.

void despreader_configure_carrier_lock (
    despreader_state_t * state,
    double up_thresh,
    double down_thresh,
    uint32_t n_up,
    uint32_t n_down
) 

Thin forwarder to costas_configure_lock() on the embedded Costas loop — symmetric with despreader_get_carrier_locked() exposing its state: state is readable, so config should be writable too, rather than forcing a caller who needs this control to drop to raw Dll+Costas composition instead of Despreader. See costas_configure_lock() for the parameter semantics.

Parameters:

  • state Must be non-NULL.
  • up_thresh Declare threshold on the lock-metric EMA.
  • down_thresh Drop threshold (<= up_thresh for level hysteresis).
  • n_up Consecutive above-threshold symbols to declare.
  • n_down Consecutive below-threshold symbols to drop.
    >>> import numpy as np
    >>> from doppler.dsss import Despreader
    >>> d = Despreader(code=np.zeros(31, dtype=np.uint8), sps=2)
    >>> d.configure_carrier_lock(0.9, 0.8, 4, 16)  # tighter declare/drop
    

function despreader_configure_code_lock

Re-tune the embedded code loop's lock detector.

int despreader_configure_code_lock (
    despreader_state_t * state,
    double pfa,
    size_t n_looks,
    double ref_snr_db
) 

Thin forwarder to dll_configure_lock() on the embedded DLL — the derived (pfa-style) entry point, matching Despreader's role as the "easy" composed API (Dll's raw escape hatch, dll_configure_lock_raw(), stays a Dll-only control for a caller that composes Dll+Costas directly). See dll_configure_lock() for the parameter semantics.

Parameters:

  • 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 (see dll_configure_lock()).

Returns:

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

>>> import numpy as np
>>> from doppler.dsss import Despreader
>>> d = Despreader(code=np.zeros(31, dtype=np.uint8), sps=2)
>>> d.configure_code_lock(1e-3, 20)
>>> d.code_locked
False
>>> d.configure_code_lock(2.0, 20)
Traceback (most recent call last):
    ...
ValueError: configure_code_lock failed (rc=-4)


function despreader_create

Create a continuous DSSS despreader (COPIES code ).

despreader_state_t * despreader_create (
    const uint8_t * code,
    size_t code_len,
    size_t sps,
    double init_norm_freq,
    double init_chip,
    double bn_carrier,
    double bn_code,
    double bn_fll,
    double zeta,
    double spacing,
    size_t periods_per_bit
) 

A complete tracking despreader for a continuous DSSS-BPSK stream: it composes a Costas carrier loop and an early/prompt/late DLL code loop over a single shared per-sample integrate-and-dump. Seed it from acquisition (the coarse carrier frequency and code phase) and the loops track the residual; steps() emits one prompt symbol per code period, and bits() bit-syncs those prompts into hard data bits (a data bit spans periods_per_bit code periods).

Parameters:

  • code Spreading code (0/1 chips), one period; copied.
  • code_len Code length (chips per period); >= 1.
  • sps Samples per chip.
  • init_norm_freq Seed carrier frequency, cycles/sample (the acquisition estimate).
  • init_chip Seed code phase, chips (the acquisition estimate).
  • bn_carrier Carrier loop noise bandwidth, normalized to the code-period (symbol) rate.
  • bn_code Code loop noise bandwidth, normalized to the code-period rate.
  • bn_fll Carrier FLL-assist bandwidth (0 = pure PLL); set > 0 for FLL-assisted carrier pull-in.
  • zeta Damping factor shared by both second-order loops.
  • spacing DLL early/late correlator tap offset, chips.
  • periods_per_bit Code periods per data bit (1 = one bit per period).

Returns:

Heap-allocated state, or NULL on allocation failure.

Note:

Caller must call despreader_destroy() when done.

>>> import numpy as np
>>> from doppler.dsss import Despreader
>>> rng = np.random.default_rng(3)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)   # one code period
>>> chips = np.where(code & 1, -1.0, 1.0)    # 0 -> +1, 1 -> -1
>>> bits = rng.integers(0, 2, 40).astype(np.uint8)  # 1 bit/period
>>> syms = np.where(bits == 1, -1.0, 1.0)
>>> rx = np.concatenate(
...     [s * np.repeat(chips, 4) for s in syms]).astype(np.complex64)
>>> d = Despreader(code, sps=4)          # seed a fresh tracking loop
>>> data = d.bits(rx)                        # hard data bits, 1/period
>>> e = np.mean(data != bits[:data.size])    # up to a global BPSK flip
>>> round(float(min(e, 1.0 - e)), 4)
0.0


function despreader_destroy

Destroy a despreader and release all memory.

void despreader_destroy (
    despreader_state_t * state
) 

Parameters:

  • state May be NULL.

function despreader_get_bit_phase

size_t despreader_get_bit_phase (
    const despreader_state_t * state
) 

function despreader_get_bn_carrier

double despreader_get_bn_carrier (
    const despreader_state_t * state
) 

function despreader_get_bn_code

double despreader_get_bn_code (
    const despreader_state_t * state
) 

function despreader_get_carrier_locked

Carrier lock decision (1 = locked): the embedded Costas loop's verify-counted detector on its lock-metric EMA (see costas_configure_lock).

int despreader_get_carrier_locked (
    const despreader_state_t * state
) 


function despreader_get_code_locked

Code lock decision (1 = locked): the embedded DLL's verify-counted CFAR detector (see dll_configure_lock); live in composition — the despreader runs the same always-on detector dll_steps does.

int despreader_get_code_locked (
    const despreader_state_t * state
) 


function despreader_get_code_phase

double despreader_get_code_phase (
    const despreader_state_t * state
) 

function despreader_get_code_rate

double despreader_get_code_rate (
    const despreader_state_t * state
) 

function despreader_get_lock_metric

double despreader_get_lock_metric (
    const despreader_state_t * state
) 

function despreader_get_norm_freq

double despreader_get_norm_freq (
    const despreader_state_t * state
) 

function despreader_get_state

void despreader_get_state (
    const despreader_state_t * state,
    void * blob
) 

function despreader_init

Initialise a despreader in place; BORROWS code .

void despreader_init (
    despreader_state_t * ch,
    const uint8_t * code,
    size_t code_len,
    size_t sps,
    double init_norm_freq,
    double init_chip,
    double bn_carrier,
    double bn_code,
    double bn_fll,
    double zeta,
    double spacing,
    size_t periods_per_bit
) 

The by-value counterpart to despreader_create(): the caller retains ownership of code (it is not copied or freed). Seeds the carrier NCO at init_norm_freq and the code phase at init_chip (the acquisition estimate). The carrier loop's update period is one code period (code_len * sps samples).

Parameters:

  • ch State to initialise. Must be non-NULL.
  • code Spreading code (0/1 chips), one period; borrowed.
  • code_len Code length (chips per period); >= 1.
  • sps Samples per chip.
  • init_norm_freq Seed carrier frequency, cycles/sample.
  • init_chip Seed code phase, chips.
  • bn_carrier Carrier loop noise bandwidth.
  • bn_code Code loop noise bandwidth.
  • bn_fll Carrier FLL-assist bandwidth (0 = pure PLL).
  • zeta Damping factor for both loops.
  • spacing DLL early/late tap offset, chips.
  • periods_per_bit Code periods per data bit (1 = one bit per period).

function despreader_reset

Re-seed both loops to the create-time frequency/phase; keep config.

void despreader_reset (
    despreader_state_t * state
) 

Restores the carrier NCO to init_norm_freq and the code phase to init_chip, zeroes the loop-filter accumulators and the bit-sync histogram, and clears the lock detectors — the spreading code and every configured bandwidth are preserved. Use it to re-run the same despreader over an independent stream and get a fresh instance's result.

Parameters:

  • state Must be non-NULL.
    >>> import numpy as np
    >>> from doppler.dsss import Despreader
    >>> rng = np.random.default_rng(3)
    >>> code = rng.integers(0, 2, 31).astype(np.uint8)
    >>> chips = np.where(code & 1, -1.0, 1.0)
    >>> syms = np.where(rng.integers(0, 2, 40) == 1, -1.0, 1.0)
    >>> rx = np.concatenate(
    ...     [s * np.repeat(chips, 4) for s in syms]).astype(np.complex64)
    >>> d = Despreader(code=code, sps=4)
    >>> first = d.bits(rx)
    >>> d.reset()                          # re-seed to acquisition
    >>> np.array_equal(first, d.bits(rx))  # same result as a fresh object
    True
    

function despreader_set_bn_carrier

void despreader_set_bn_carrier (
    despreader_state_t * state,
    double val
) 

function despreader_set_bn_code

void despreader_set_bn_code (
    despreader_state_t * state,
    double val
) 

function despreader_set_norm_freq

void despreader_set_norm_freq (
    despreader_state_t * state,
    double val
) 

function despreader_set_state

int despreader_set_state (
    despreader_state_t * state,
    const void * blob
) 

function despreader_set_telemetry

Attach (or detach) a telemetry context across the despreader. Pure forwarder — the despreader registers no probes of its own: the carrier loop registers "<prefix>.car.lock" / ".e" / ".freq" / ".locked" and the code loop registers "<prefix>.code.e" / ".rate" / ".lock" / ".locked" (the ".locked" pair are the loops' verify-counted lockdet decisions, 0/1) — eight probes, all thinned by decim and emitted once per code period (the despreader flushes both loops at its per-period update). Passing NULL detaches both loops. Setup path, never hot; the context is borrowed and must outlive the attachment (SPSC rules indp_tlm/dp_tlm_core.h ).

int despreader_set_telemetry (
    despreader_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. "ch0".
  • decim Emit every decim-th code period; >= 1.

Returns:

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

>>> import numpy as np
>>> from doppler.dsss import Despreader
>>> from doppler.telemetry import Telemetry
>>> tlm = Telemetry(1 << 12)
>>> code = (np.arange(31) % 2).astype(np.uint8)
>>> ch = Despreader(code=code, sps=4)
>>> ch.set_telemetry(tlm, "ch0")
>>> names = sorted(tlm.probe_names)
>>> names[:4]
['ch0.car.e', 'ch0.car.freq', 'ch0.car.lock', 'ch0.car.locked']
>>> names[4:]
['ch0.code.e', 'ch0.code.lock', 'ch0.code.locked', 'ch0.code.rate']
>>> chips = 1.0 - 2.0 * (np.arange(31) % 2)
>>> x = np.tile(np.repeat(chips, 4), 40).astype(np.complex64)
>>> _ = ch.steps(x)
>>> recs = tlm.read()   # eight records per code period
>>> len(recs) > 0 and len(recs) % 8 == 0
True


function despreader_state_bytes

size_t despreader_state_bytes (
    const despreader_state_t * state
) 

function despreader_steps

Track carrier and code and despread a CF32 block, one prompt symbol per code period.

size_t despreader_steps (
    despreader_state_t * state,
    const float _Complex * x,
    size_t x_len,
    float _Complex * out,
    size_t max_out
) 

The continuous kernel: per input sample it wipes the carrier (Costas NCO) and correlates the de-rotated sample against the early/prompt/late code taps (DLL); per code period it dumps the prompt integrate-and-dump, updates the code loop on the early/late envelopes and the carrier loop on the same prompt, and emits that prompt. A partial period is carried in state across calls, so a long stream can be fed in blocks. Each emitted symbol's sign is the BPSK decision; its phase and magnitude are the soft information.

Parameters:

  • state Must be non-NULL.
  • x Input CF32 samples.
  • x_len Number of input samples.
  • out Output buffer for prompt symbols (>= max_out).
  • max_out Capacity of out in symbols.

Returns:

Number of prompt symbols written into out.

>>> import numpy as np
>>> from doppler.dsss import Despreader
>>> rng = np.random.default_rng(3)
>>> code = rng.integers(0, 2, 31).astype(np.uint8)  # one code period
>>> chips = np.where(code & 1, -1.0, 1.0)            # 0 -> +1, 1 -> -1
>>> bits = rng.integers(0, 2, 40).astype(np.uint8)  # 1 bit / period
>>> syms = np.where(bits == 1, -1.0, 1.0)
>>> rx = np.concatenate(
...     [s * np.repeat(chips, 4) for s in syms]).astype(np.complex64)
>>> d = Despreader(code=code, sps=4)
>>> prompt = d.steps(rx)                 # one prompt per code period
>>> hard = (prompt.real < 0).astype(np.uint8)
>>> e = np.mean(hard != bits[:hard.size])   # payload recovered
>>> round(float(min(e, 1.0 - e)), 4)
0.0


function despreader_steps_max_out

size_t despreader_steps_max_out (
    despreader_state_t * state
) 

Macro Definition Documentation

define DESPREADER_STATE_MAGIC

#define DESPREADER_STATE_MAGIC `DP_FOURCC ('D', 'S', 'P', 'R')`

define DESPREADER_STATE_VERSION

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


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